From 57983096b18abce8f59f6cdd8daa47a5a78880e8 Mon Sep 17 00:00:00 2001 From: trypton Date: Wed, 28 Jan 2026 22:11:54 +0100 Subject: [PATCH] Primer commit de los tres flujos: Raspipeso, Servidor y Gestor de semaforos --- flows.json | 10133 +++++++++++++++- flows_cred.json | 7 +- uibuilder/.config/sioMiddleware.js-template | 53 + uibuilder/.config/sioMsgOut.js-template | 22 + uibuilder/.config/sioUse.js-template | 43 + uibuilder/.config/uibMiddleware.js-template | 26 + uibuilder/bascula/LICENSE | 178 + uibuilder/bascula/README.md | 77 + uibuilder/bascula/api/.keep | 0 uibuilder/bascula/dist/.keep | 0 uibuilder/bascula/eslint.config.mjs | 121 + uibuilder/bascula/package.json | 35 + uibuilder/bascula/routes/.keep | 0 uibuilder/bascula/src/index.css | 140 + uibuilder/bascula/src/index.html | 41 + uibuilder/bascula/src/index.js | 70 + uibuilder/bascula/tsconfig.json | 14 + uibuilder/bascula/types/uibuilder.d.ts | 12 + uibuilder/bascula/types/uibuilder.module.d.ts | 647 + uibuilder/common/images/node-blue.ico | Bin 0 -> 1150 bytes uibuilder/package.json | 19 + uibuilder/package.json.bak | 19 + uibuilder/semaforos/LICENSE | 178 + uibuilder/semaforos/README.md | 77 + uibuilder/semaforos/api/.keep | 0 uibuilder/semaforos/ayuda.pdf | Bin 0 -> 112831 bytes uibuilder/semaforos/dist/.keep | 0 uibuilder/semaforos/eslint.config.mjs | 121 + uibuilder/semaforos/package.json | 35 + uibuilder/semaforos/routes/.keep | 0 uibuilder/semaforos/src/ayuda.html | 3 + uibuilder/semaforos/src/ayuda.pdf | Bin 0 -> 112831 bytes uibuilder/semaforos/src/images/image1.png | Bin 0 -> 7923 bytes uibuilder/semaforos/src/images/image2.png | Bin 0 -> 12433 bytes uibuilder/semaforos/src/index.css | 540 + uibuilder/semaforos/src/index.html | 317 + uibuilder/semaforos/src/index.js | 494 + uibuilder/semaforos/tsconfig.json | 14 + uibuilder/semaforos/types/uibuilder.d.ts | 12 + .../semaforos/types/uibuilder.module.d.ts | 647 + uibuilder/supervisor/LICENSE | 178 + uibuilder/supervisor/README.md | 77 + uibuilder/supervisor/api/.keep | 0 uibuilder/supervisor/dist/.keep | 0 uibuilder/supervisor/eslint.config.mjs | 121 + uibuilder/supervisor/package.json | 35 + uibuilder/supervisor/routes/.keep | 0 uibuilder/supervisor/src/ayuda.html | 110 + uibuilder/supervisor/src/images/image1.png | Bin 0 -> 7923 bytes uibuilder/supervisor/src/images/image2.png | Bin 0 -> 12433 bytes uibuilder/supervisor/src/index.css | 540 + uibuilder/supervisor/src/index.html | 312 + uibuilder/supervisor/src/index.js | 459 + uibuilder/supervisor/tsconfig.json | 18 + uibuilder/supervisor/types/uibuilder.d.ts | 47 + .../supervisor/types/uibuilder.module.d.ts | 774 ++ uibuilder/totem/LICENSE | 178 + uibuilder/totem/README.md | 77 + uibuilder/totem/api/.keep | 0 uibuilder/totem/dist/.keep | 0 uibuilder/totem/eslint.config.mjs | 121 + uibuilder/totem/package.json | 35 + uibuilder/totem/routes/.keep | 0 uibuilder/totem/src/ayuda.html | 110 + uibuilder/totem/src/images/image1.png | Bin 0 -> 7923 bytes uibuilder/totem/src/images/image2.png | Bin 0 -> 12433 bytes uibuilder/totem/src/index.css | 626 + uibuilder/totem/src/index.html | 162 + uibuilder/totem/src/index.js | 185 + uibuilder/totem/tsconfig.json | 14 + uibuilder/totem/types/uibuilder.d.ts | 12 + uibuilder/totem/types/uibuilder.module.d.ts | 647 + 72 files changed, 18931 insertions(+), 2 deletions(-) create mode 100644 uibuilder/.config/sioMiddleware.js-template create mode 100644 uibuilder/.config/sioMsgOut.js-template create mode 100644 uibuilder/.config/sioUse.js-template create mode 100644 uibuilder/.config/uibMiddleware.js-template create mode 100644 uibuilder/bascula/LICENSE create mode 100644 uibuilder/bascula/README.md create mode 100644 uibuilder/bascula/api/.keep create mode 100644 uibuilder/bascula/dist/.keep create mode 100644 uibuilder/bascula/eslint.config.mjs create mode 100644 uibuilder/bascula/package.json create mode 100644 uibuilder/bascula/routes/.keep create mode 100644 uibuilder/bascula/src/index.css create mode 100644 uibuilder/bascula/src/index.html create mode 100644 uibuilder/bascula/src/index.js create mode 100644 uibuilder/bascula/tsconfig.json create mode 100644 uibuilder/bascula/types/uibuilder.d.ts create mode 100644 uibuilder/bascula/types/uibuilder.module.d.ts create mode 100644 uibuilder/common/images/node-blue.ico create mode 100644 uibuilder/package.json create mode 100644 uibuilder/package.json.bak create mode 100644 uibuilder/semaforos/LICENSE create mode 100644 uibuilder/semaforos/README.md create mode 100644 uibuilder/semaforos/api/.keep create mode 100644 uibuilder/semaforos/ayuda.pdf create mode 100644 uibuilder/semaforos/dist/.keep create mode 100644 uibuilder/semaforos/eslint.config.mjs create mode 100644 uibuilder/semaforos/package.json create mode 100644 uibuilder/semaforos/routes/.keep create mode 100644 uibuilder/semaforos/src/ayuda.html create mode 100644 uibuilder/semaforos/src/ayuda.pdf create mode 100644 uibuilder/semaforos/src/images/image1.png create mode 100644 uibuilder/semaforos/src/images/image2.png create mode 100644 uibuilder/semaforos/src/index.css create mode 100644 uibuilder/semaforos/src/index.html create mode 100644 uibuilder/semaforos/src/index.js create mode 100644 uibuilder/semaforos/tsconfig.json create mode 100644 uibuilder/semaforos/types/uibuilder.d.ts create mode 100644 uibuilder/semaforos/types/uibuilder.module.d.ts create mode 100644 uibuilder/supervisor/LICENSE create mode 100644 uibuilder/supervisor/README.md create mode 100644 uibuilder/supervisor/api/.keep create mode 100644 uibuilder/supervisor/dist/.keep create mode 100644 uibuilder/supervisor/eslint.config.mjs create mode 100644 uibuilder/supervisor/package.json create mode 100644 uibuilder/supervisor/routes/.keep create mode 100644 uibuilder/supervisor/src/ayuda.html create mode 100644 uibuilder/supervisor/src/images/image1.png create mode 100644 uibuilder/supervisor/src/images/image2.png create mode 100644 uibuilder/supervisor/src/index.css create mode 100644 uibuilder/supervisor/src/index.html create mode 100644 uibuilder/supervisor/src/index.js create mode 100644 uibuilder/supervisor/tsconfig.json create mode 100644 uibuilder/supervisor/types/uibuilder.d.ts create mode 100644 uibuilder/supervisor/types/uibuilder.module.d.ts create mode 100644 uibuilder/totem/LICENSE create mode 100644 uibuilder/totem/README.md create mode 100644 uibuilder/totem/api/.keep create mode 100644 uibuilder/totem/dist/.keep create mode 100644 uibuilder/totem/eslint.config.mjs create mode 100644 uibuilder/totem/package.json create mode 100644 uibuilder/totem/routes/.keep create mode 100644 uibuilder/totem/src/ayuda.html create mode 100644 uibuilder/totem/src/images/image1.png create mode 100644 uibuilder/totem/src/images/image2.png create mode 100644 uibuilder/totem/src/index.css create mode 100644 uibuilder/totem/src/index.html create mode 100644 uibuilder/totem/src/index.js create mode 100644 uibuilder/totem/tsconfig.json create mode 100644 uibuilder/totem/types/uibuilder.d.ts create mode 100644 uibuilder/totem/types/uibuilder.module.d.ts diff --git a/flows.json b/flows.json index 0637a08..ae08685 100644 --- a/flows.json +++ b/flows.json @@ -1 +1,10132 @@ -[] \ No newline at end of file +[ + { + "id": "3fbc028716abf7a9", + "type": "tab", + "label": "RaspiPeso", + "disabled": false, + "info": "", + "env": [] + }, + { + "id": "744bdf3a0e08d754", + "type": "tab", + "label": "Supervisor", + "disabled": false, + "info": "", + "env": [] + }, + { + "id": "576f3807f61f5098", + "type": "tab", + "label": "Gestion Semaforos", + "disabled": false, + "info": "", + "env": [] + }, + { + "id": "aadbe4b53903ecda", + "type": "group", + "z": "3fbc028716abf7a9", + "name": "Bascula3", + "style": { + "label": true, + "fill": "#ffbfbf", + "fill-opacity": "0.42", + "stroke": "#000000", + "color": "#000000" + }, + "nodes": [ + "03b120c9713eb333", + "14fbc044898db247", + "f4cda14c866fa39a", + "fad252cfbd6688a3", + "eb3ddc79e17c0fd3", + "6dbef4dc022becea", + "2d9d86f7a82a37eb", + "9673d34b6f177b10", + "1381985c88fc5dbb", + "34291c27b4f82d26", + "ac8abf3f7ec09ac9", + "f89e176a87f0d923", + "a8d75f0609d7f8ed", + "6731bec53435c2d5", + "1457adeb1e1238af", + "5fe3f27656425fc8", + "f178e2126d397565", + "183a0ae1b9315d02" + ], + "x": 54, + "y": 799, + "w": 1992, + "h": 322 + }, + { + "id": "3c69eb8420f499d7", + "type": "group", + "z": "3fbc028716abf7a9", + "name": "Bascula1", + "style": { + "label": true, + "color": "#000000", + "stroke": "#000000", + "fill": "#ffffbf", + "fill-opacity": "0.52" + }, + "nodes": [ + "2d26d00c4530ee1f", + "9e837a6339e80936", + "7a6fc5c1767b38b0", + "8a0cc762a8039217", + "bdad189ae1181c82", + "f0bede4dc0ffef68", + "a65499ac760347b5", + "47f84415577e771f", + "4c97abf1728d0d77", + "760d3fa03416427f", + "5191ba50d8f57c1f", + "aacff5572bb6eb47", + "69b2b79597b10613", + "81d1e1eceb078857", + "b3bf54cc42094984", + "9dbc4cf2cb9ad616", + "5704a222c2e6219e", + "4d122b4d7ef323dd" + ], + "x": 54, + "y": 139, + "w": 1992, + "h": 302 + }, + { + "id": "414eb1569dd15f60", + "type": "group", + "z": "3fbc028716abf7a9", + "name": "Bascula2", + "style": { + "label": true, + "fill": "#ffefbf", + "fill-opacity": "0.62", + "stroke": "#000000", + "color": "#000000" + }, + "nodes": [ + "c03cc7b1afedcdad", + "5aa551b29b8cdca3", + "a979e217b6375370", + "745217e56d674e38", + "c90a867aaba4b730", + "af2b7b939331dbb9", + "c9a6ce7c276cd0cf", + "e45740d360367781", + "92f2a1b83feac9b1", + "69c5f45de39463f0", + "91bdf33233cd6dc8", + "751f20d5500f0560", + "662de6fd08e45fb2", + "c7ac3675f9f951fe", + "fa67b799f5270de3", + "e59e292827957f03", + "a567f5be5995d73c", + "4c71a5b6fc6bd505" + ], + "x": 54, + "y": 459, + "w": 1992, + "h": 322 + }, + { + "id": "21cb55110b1bae57", + "type": "group", + "z": "3fbc028716abf7a9", + "name": "Bascula4", + "style": { + "label": true, + "fill": "#bfdbef", + "fill-opacity": "0.37", + "stroke": "#000000", + "color": "#000000" + }, + "nodes": [ + "83659a5fa5422105", + "d03daa5335e2514c", + "5cf1088cefafd3a7", + "b8228cdac41b95c1", + "5d3d7b9e427ebd33", + "d216a46466968eae", + "b0ed51791aa894e6", + "631799398328acdf", + "c5d5fe49f9134079", + "061c9a2b5eb5e8b5", + "6d6370556efac085", + "6680d1b3b7ad8918", + "63c34626c74e9d80", + "12fbd1e2e0da99f5", + "55d810bcd402f1c0", + "d6e380c9415bfb72", + "98c9a64ec995f9d2", + "a1792e1b8fb6e6be" + ], + "x": 54, + "y": 1139, + "w": 1992, + "h": 282 + }, + { + "id": "e3bc37824e09b774", + "type": "group", + "z": "3fbc028716abf7a9", + "name": "CONFIGURACION CON UIBUILDER", + "style": { + "stroke": "#0070c0", + "fill-opacity": "0.75", + "label": true, + "fill": "#bfdbef", + "color": "#000000" + }, + "nodes": [ + "2ec53aa1784087c1", + "b8fdc502b1bf35b7", + "be843dd4381d45d5", + "88b9b4d1c5aa7359", + "7a4c757e6f6ddfcd", + "6d39924d1d2081dd", + "882ee24d1489b06c", + "9667a055f9c967a7", + "7058b9e632aa584b", + "ba5e6b2b40fba02b", + "1ba8bfe271072a91" + ], + "x": 2174, + "y": 799, + "w": 892, + "h": 322 + }, + { + "id": "05b4bc2ccdd08ee9", + "type": "group", + "z": "3fbc028716abf7a9", + "name": "SERVIDOR API PARA TEST", + "style": { + "stroke": "#ff0000", + "fill": "#ffffbf", + "fill-opacity": "0.24", + "label": true, + "color": "#000000" + }, + "nodes": [ + "960b4b6685af7406", + "f806cc253d96d34a", + "363afa81160f118e" + ], + "x": 2714, + "y": 1199, + "w": 352, + "h": 142 + }, + { + "id": "9a733b496ecde31c", + "type": "group", + "z": "3fbc028716abf7a9", + "name": "RECEPTOR DE MOVIMIENTOS VIA MQTT", + "style": { + "stroke": "#3f93cf", + "fill": "#bfdbef", + "fill-opacity": "0.27", + "label": true, + "color": "#000000" + }, + "nodes": [ + "87154c99372a5f9c", + "bfe9a33e26aa7cf4", + "56b300da80743113", + "d4174078d4cb9375", + "93bfa84a0f29e988" + ], + "x": 2174, + "y": 1199, + "w": 512, + "h": 222 + }, + { + "id": "f4b1c07e498c11be", + "type": "group", + "z": "3fbc028716abf7a9", + "name": "PANTALLA ON / OFF", + "style": { + "stroke": "#ff0000", + "fill-opacity": "0.16", + "label": true, + "color": "#000000", + "fill": "#a4a4a4" + }, + "nodes": [ + "780c6c61.a0cb04", + "49500a91.3971e4", + "97ec61bc.7e2aa", + "afb26e9306481251", + "2c73d02c99e64b37" + ], + "x": 2174, + "y": 139, + "w": 432, + "h": 202 + }, + { + "id": "bfec5f4fae03e5b4", + "type": "group", + "z": "3fbc028716abf7a9", + "name": "envio websocket", + "style": { + "label": true + }, + "nodes": [ + "b47df6182a4c503b", + "a96c9a3676aad816", + "6537522d1838f799", + "3e6f6384f2ec9846" + ], + "x": 2164, + "y": 599, + "w": 642, + "h": 142 + }, + { + "id": "663dc04a15a0be0c", + "type": "group", + "z": "3fbc028716abf7a9", + "name": "archivo configuracion", + "style": { + "label": true + }, + "nodes": [ + "f450a2dee7246e4b", + "e9767bab493f578b", + "aa13d9f112f46a30", + "d7b0f75b37ad9f53" + ], + "x": 2174, + "y": 399, + "w": 632, + "h": 122 + }, + { + "id": "610fa44452654078", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "CONFIGURACION CON UIBUILDER", + "style": { + "stroke": "#0070c0", + "fill-opacity": "0.75", + "label": true, + "fill": "#bfdbef", + "color": "#000000" + }, + "nodes": [ + "57ecdac37231f743", + "b75f8eb3486aed8d", + "4657b0f0cefa89d7", + "7698b09169dc1c99", + "7004d0202704343a", + "58bb0cd40f89ba0d", + "fcce285ce4cc49d0", + "17547c6cf2cc8bdf", + "e4f5cd90a27363a0", + "0f3cd31839136f10", + "9559a621baa2e0e5", + "9a0817ab9b58c626", + "eaf1c8163a641d84", + "22a6d78769eca8a4", + "38d2c6565b5ca0bf", + "c8010702a8431461", + "4ae8af66b4ef6232", + "508d57d90e013805", + "7e099ddf6bfcb9fb", + "dd15174f0db1d58c" + ], + "x": 3734, + "y": 479, + "w": 1012, + "h": 482 + }, + { + "id": "68b92de7bc4c82f6", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "SINCRONIZA REGISTROS ANTERIORES", + "style": { + "stroke": "#0070c0", + "fill": "#bfdbef", + "fill-opacity": "0.25", + "label": true, + "color": "#000000" + }, + "nodes": [ + "34bf8ec19b0e78a7", + "950c8115e364a493", + "f41c4b5c61a58ea2", + "b1c81347b429091a", + "911f1b379de9ba3a", + "76d19764677ebc8a", + "3c3867c2a2185f63", + "2b9158681d5eb09b", + "d4a053997921cfc0", + "89f64c532d3d78e7", + "e944fa11afc97160", + "f4c6152e8ed7f5a6", + "a42d3e0da2d9dbda", + "7b7da2e1e640ca0a", + "1387a80aca617b59", + "fdddf9cbc2fa6622", + "9eda24ef14d3892c", + "867c5654842a1210", + "d144432c1ab94daf" + ], + "x": 1994, + "y": 1059, + "w": 2172, + "h": 488 + }, + { + "id": "6ec963a99751b141", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "GENERACION DE MENU PARA UNA MATRICULA REGISTRADA", + "style": { + "stroke": "#0070c0", + "fill": "#bfdbef", + "fill-opacity": "0.31", + "label": true, + "color": "#000000" + }, + "nodes": [ + "f0815d4d5c1e13e0", + "8013dc387b26381b", + "cb22a118da74ca79" + ], + "x": 1988, + "y": 1553, + "w": 2044, + "h": 614 + }, + { + "id": "f968efec7b96ec13", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "REVISA SI QR / TARJETA CORRESPONDE A UN CONTRATO", + "style": { + "stroke": "#ff0000", + "fill": "#ffffff", + "fill-opacity": "0.94", + "label": true, + "color": "#000000" + }, + "nodes": [ + "9b47806ebf017d2c", + "7269e082a5989305", + "8e56735bbb37a598", + "ea115b9b38cdaea1", + "ed04082db0d29b5b", + "52c76e5f8d275432", + "57fd039f5dadf4fb", + "8558335b01c7e441" + ], + "x": 1994, + "y": 2279, + "w": 942, + "h": 182 + }, + { + "id": "0a78f1a176dd7ad5", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "RECEPCION DE TABLAS VIA API", + "style": { + "stroke": "#92d04f", + "fill": "#ffffff", + "label": true, + "color": "#000000" + }, + "nodes": [ + "a8e9253184936edf", + "24855820a3fadbb8", + "20976aa7738e9650", + "a091c51915be9f0a", + "9d2b6d3b968d3fcf", + "c95d9408ac6b1595", + "5cd2d2a7d0b81e81", + "e87018791b56046f", + "ae259a894fbedeb4", + "eca4e2e077d64f16" + ], + "x": 1994, + "y": 2559, + "w": 1112, + "h": 202 + }, + { + "id": "75dd57faa07f33b9", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "ENVIA DATOS AL SERVIDOR - ACTUALIZA BD Y ARCHIVOS DIARIAMENTE - SIRVE DATOS DE MOVIMIENTOS", + "style": { + "stroke": "#ffC000", + "fill": "#ffefbf", + "fill-opacity": "0.19", + "label": true + }, + "nodes": [ + "0b16f0b9cb1c5ab9", + "c858b37cb0eeba4f", + "e192c564f2050e94", + "1bdaeab49913082e", + "b8029f9a251a232e", + "fd9524c0921c0990", + "8778197111d2ab0d", + "d47200e5f67c772a", + "b4caaec341e453e0", + "d19507c7bbe4bb32", + "e2fca1c2499902fa", + "6a2b1526103aa3b0", + "26f04e07495817eb", + "d5ce4cb6b3abd807", + "49bed5a59768fbef", + "a58ecf0f246aff6a", + "4e6d5242d6531d79", + "732068c66d8e0a25", + "e716da8ff5c458c9", + "10affb7590fa95f1", + "ee2f281460418d2a", + "6455539b9fac2063", + "7ff54b99b5b933b2", + "08bf0c9897d6e815", + "65b220fc46d55c96" + ], + "x": 2004, + "y": 459, + "w": 1462, + "h": 562 + }, + { + "id": "800484a81d9dc5ee", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "CONFIGURACION", + "style": { + "stroke": "#9363b7", + "fill": "#dbcbe7", + "fill-opacity": "0.22", + "label": true, + "color": "#000000" + }, + "nodes": [ + "1279bc86f850f792", + "c18ea33c86d877db", + "9b375376ea8f2a66", + "844805b92ed8fc5c", + "b1eb9bccca87f0f9", + "29b2dd9b01235f99", + "0471bc0a0beb44fd", + "c5a3e80cc446b610", + "323c2ee052338182", + "a8218646952839ba", + "8435634f3b8cf017", + "f372e14a6921bc3a", + "cc890d832924b5d5" + ], + "x": 614, + "y": 999, + "w": 992, + "h": 229.5 + }, + { + "id": "8aa65df7a757f075", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "RECEPCION DE MOVIMIENTOS TEMPORALES", + "style": { + "stroke": "#92d04f", + "fill": "#e3f3d3", + "fill-opacity": "0.39", + "label": true, + "color": "#000000" + }, + "nodes": [ + "73752ba2ae415953", + "182712df6587926f", + "de78dc5fb92df251", + "6703fb916aef8f4b", + "b264dc44d4344d72", + "4e1f0ff54eef4e7b", + "2dc5c2942966b097", + "3d7afdefd9a5d353", + "138317044e960cfa", + "4f30b9755ed381bb", + "66630f04db34f483", + "2d857220b76dda21", + "8a7201a76e53897f", + "1b2d9af6c1e13e5f", + "5c4679ed20761208", + "c61c445897350722", + "9fa72afe7d97828e", + "dc3fe6bd2494cab0" + ], + "x": 614, + "y": 1319, + "w": 1052, + "h": 442 + }, + { + "id": "a57600b79a898710", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "GENERA MOVIMIENTOS CERRADOS", + "style": { + "stroke": "#7fb7df", + "stroke-opacity": "0.42", + "fill": "#bfdbef", + "fill-opacity": "0.34", + "label": true, + "color": "#000000" + }, + "nodes": [ + "c674aac5bef7594a", + "405f569c2efbf22d", + "417e71d002f16583", + "b1b9ac8e8b543dbb", + "0ba07aef43c4a226", + "cc229c10e3f4e423", + "65bfea0283dfab05", + "a1c2fb59c438c1c5", + "2e4cf9271d9a78d8", + "dd4decb988977915" + ], + "x": 614, + "y": 1779, + "w": 1082, + "h": 242 + }, + { + "id": "6fd6ce44bb173af7", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "NODOS PARA PRUEBAS EN BD", + "style": { + "stroke": "#ff0000", + "fill": "#ffffbf", + "fill-opacity": "0.86", + "label": true, + "color": "#000000" + }, + "nodes": [ + "81f447b69bd2c603", + "d2082aabd7021a2e", + "1d44274163bba745", + "17b55fb17b52609b", + "f769af64083450ed", + "a550322a7e6cafd2", + "9e4bc4c1c4148e95", + "e22e7f0f1cea9cb5" + ], + "x": 614, + "y": 2179, + "w": 792, + "h": 242 + }, + { + "id": "e03fd8b52121427a", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "PANTALLA ON / OFF", + "style": { + "stroke": "#ff0000", + "fill-opacity": "0.16", + "label": true, + "color": "#000000", + "fill": "#a4a4a4" + }, + "nodes": [ + "6a066066436f56ef", + "245c965f41b35b43", + "bad546e70128edf8", + "ff7604277bbc1810", + "0a768660b1756f9a", + "1ca5e534fa4962de" + ], + "x": 614, + "y": 2479, + "w": 432, + "h": 222 + }, + { + "id": "1e7a7eecbf88e674", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "PRUEBAS DEL API", + "style": { + "label": true + }, + "nodes": [ + "422ffdb7a80b308a", + "5c03e30bd6a7dd59", + "150a34e02c2def17", + "bebcf89f4daa2d2c", + "ecd2ede4ce450c22" + ], + "x": 1694, + "y": 319, + "w": 912, + "h": 102 + }, + { + "id": "6c0376d0b4669415", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "SALIDA WEBSOCKET", + "style": { + "label": true + }, + "nodes": [ + "cb6c28fe05c25229", + "d7d43c051dffd484", + "e5d5709dcfce538e" + ], + "x": 4114, + "y": 1619, + "w": 372, + "h": 202 + }, + { + "id": "3276a28141ee1362", + "type": "group", + "z": "576f3807f61f5098", + "name": "LAZOS (L1 L2 L3)", + "style": { + "stroke": "#999", + "fill": "#fff3e0", + "fill-opacity": "0.6", + "label": true, + "label-position": "nw" + }, + "nodes": [ + "e4e47d1f029304cb", + "20a7ce11f2e8216a", + "e4a843aa2e13ce4d", + "46f02d9f8d41ee03", + "d4986d948f5f65a7", + "15c2a5fd76e103ac", + "69f5087384c48122", + "f38d6d27851d0e2b", + "be21a62a431574d0", + "0dde5e928354be6c", + "fb6d5180204e6bce", + "918df2f63a69c6c9", + "c4159a812594fc62" + ], + "x": 884, + "y": 299, + "w": 682, + "h": 322 + }, + { + "id": "f4f4ca47869551e3", + "type": "group", + "z": "576f3807f61f5098", + "name": "PESO (histeresis)", + "style": { + "stroke": "#999", + "fill": "#e8f5e9", + "fill-opacity": "0.6", + "label": true, + "label-position": "nw" + }, + "nodes": [ + "a7c59fef860efa52", + "e0b22a42652448d3", + "29994ee556e17a85", + "74ea72530de212a4", + "29b8ed91354ac1e2", + "b0c185b2e9fb99d4", + "e616701e86d299ad" + ], + "x": 894, + "y": 679, + "w": 682, + "h": 182 + }, + { + "id": "6457918bac63ba59", + "type": "group", + "z": "576f3807f61f5098", + "name": "CONTROL (FSM + INIT)", + "style": { + "stroke": "#999", + "fill": "#e1f5fe", + "fill-opacity": "0.6", + "label": true, + "label-position": "nw" + }, + "nodes": [ + "04796f17dd930bad", + "c9d524190cbe7f96", + "e7754eeae28a6256", + "0e30fb1a5ee58746", + "67e977418231666d", + "798cfaee0987cdca", + "b017ceea4285eb76" + ], + "x": 1634, + "y": 299, + "w": 602, + "h": 189.5 + }, + { + "id": "e51291e8f6dc7282", + "type": "group", + "z": "576f3807f61f5098", + "name": "SCHEDULER (WRR 2>3>1)", + "style": { + "stroke": "#999", + "fill": "#ede7f6", + "fill-opacity": "0.6", + "label": true, + "label-position": "nw" + }, + "nodes": [ + "606dc6f9453f5e0d", + "891cfa9c8d6e464d", + "15131de35b7d0425" + ], + "x": 1644, + "y": 539, + "w": 462, + "h": 82 + }, + { + "id": "fdde9120f66c6c06", + "type": "group", + "z": "576f3807f61f5098", + "name": "TIMERS (T_i y G_i)", + "style": { + "stroke": "#999", + "fill": "#fce4ec", + "fill-opacity": "0.6", + "label": true, + "label-position": "nw" + }, + "nodes": [ + "00ce5615a6247bec", + "e4543a6e7a7f0ddb", + "2dc673e9f13e0f0b" + ], + "x": 1644, + "y": 679, + "w": 442, + "h": 82 + }, + { + "id": "e928e581c722fe25", + "type": "group", + "z": "576f3807f61f5098", + "name": "GESTION DE LOS SEMAFOROS (salida)", + "style": { + "stroke": "#999", + "fill": "#f3e5f5", + "fill-opacity": "0.6", + "label": true + }, + "nodes": [ + "8da927c794b40546", + "bc4418d271c32af0", + "e6e4d33e9470fbca", + "2f0ca87e8afc0d73", + "06c0e45f61e0d364", + "49a1dfc2d343bdbb", + "4878484f33b6ea38", + "d8112ca72e0a342d", + "b6317c2d3d6d070c", + "dd43a77de63f2443", + "217697ee4e884321", + "967aada409fe59f3" + ], + "x": 1638, + "y": 839, + "w": 968, + "h": 448 + }, + { + "id": "4a6a83419193ac56", + "type": "group", + "z": "576f3807f61f5098", + "name": "DEBUG", + "style": { + "stroke": "#999", + "fill": "#eeeeee", + "fill-opacity": "0.6", + "label": true, + "label-position": "nw" + }, + "nodes": [ + "d95551b3e1091e71", + "3e36b8139450735b" + ], + "x": 2174, + "y": 679, + "w": 387, + "h": 82 + }, + { + "id": "350484a2e764ef67", + "type": "group", + "z": "576f3807f61f5098", + "name": "CONFIGURACION CON UIBUILDER", + "style": { + "stroke": "#0070c0", + "fill-opacity": "0.75", + "label": true, + "fill": "#bfdbef", + "color": "#000000" + }, + "nodes": [ + "c42bdd023acc8dc3", + "85b34266c740174d", + "4333f08dfc27e390", + "ad67876e3fd4b4bb", + "adbe70916e20904f", + "1e52a0895d377969", + "2f80166bb47a752f", + "6a6d080c74f2da43", + "081628af33270569", + "49e2b8c24d0ee245", + "a4992bd24a1acc12", + "4847a19f86d704fc", + "711b20eac053af7f", + "37b8b001f2c455d2", + "7aa2adada5ca928e", + "77eb0df85abf7f16", + "c398ee4ceb30937f", + "136e09e5233e209b", + "d6c429592c6cfdab", + "764c5454d21fc52b" + ], + "x": 2684, + "y": 299, + "w": 1012, + "h": 482 + }, + { + "id": "6f7ca09a49c974d3", + "type": "group", + "z": "576f3807f61f5098", + "name": "CARGA CONFIGURACION", + "style": { + "stroke": "#ff0000", + "fill-opacity": "0.12", + "label": true, + "color": "#000000", + "fill": "#92d04f" + }, + "nodes": [ + "a2fcbe68dc4e70bd", + "6b89fc341280ad29", + "e02d2079825430db", + "53019ab29a07f8f5", + "7c6babaed4914e41", + "4232fc1ec89537af" + ], + "x": 2694, + "y": 839, + "w": 632, + "h": 142 + }, + { + "id": "fdddf9cbc2fa6622", + "type": "group", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "ENVIO MQTT", + "style": { + "stroke": "#ff0000", + "fill": "#ffbfbf", + "fill-opacity": "0.2", + "label": true, + "color": "#000000" + }, + "nodes": [ + "ca758e5b6e2d880e", + "4a2f600ba9ca7a15", + "a62dedb280979227", + "d5ced8b11f124c67", + "6d9ec47623128a2d", + "1d3c5c1f2e5be4b0", + "0a4223f208ebf170", + "fc3b9b3c65b9e540" + ], + "x": 2994, + "y": 1219, + "w": 552, + "h": 302 + }, + { + "id": "f0815d4d5c1e13e0", + "type": "group", + "z": "744bdf3a0e08d754", + "g": "6ec963a99751b141", + "name": "ACTUALIZACION DE TABLAS DE MENUS", + "style": { + "stroke": "#92d04f", + "fill": "#ffffff", + "label": true, + "color": "#000000" + }, + "nodes": [ + "d683e9cccedc561f", + "0c144d19f803250f", + "105c677a40b6461b", + "f0f98e25ebd44ac3", + "67e7ede89ac08b87", + "36b9bf6834913595", + "c7a7b3e8c093ae1e", + "afd34cb6f61a6475", + "ac9d1ea0c4c9dfbe", + "b9b6a469329290a5", + "76a88549a38a751d", + "e0c3a3c33ed7aaca", + "7aee3c3ea10fd9a5", + "aa870f8d73e4f504", + "1dae889e8697bf15" + ], + "x": 2014, + "y": 1579, + "w": 1712, + "h": 322 + }, + { + "id": "8013dc387b26381b", + "type": "group", + "z": "744bdf3a0e08d754", + "g": "6ec963a99751b141", + "name": "REVISA SI MATRICULA EXISTE EN BASE DE DATOS", + "style": { + "stroke": "#ff0000", + "fill": "#ffffff", + "fill-opacity": "0.94", + "label": true, + "color": "#000000" + }, + "nodes": [ + "1d9e5518c4689f75", + "a1ac4bf3b8c14b21", + "7f282cc6ba963043", + "c470404997683059", + "008f41b960a6d13f", + "9a65971441f7579f", + "9c65121589e6726e", + "b76ed973b1299bb9", + "a16351341e1a09da", + "22f142c6e6d19334", + "ceba75f09293b8f4" + ], + "x": 2014, + "y": 1939, + "w": 972, + "h": 202 + }, + { + "id": "cb22a118da74ca79", + "type": "group", + "z": "744bdf3a0e08d754", + "g": "6ec963a99751b141", + "name": "GENERACION DE LA LISTA DEL MENU", + "style": { + "stroke": "#ff0000", + "stroke-opacity": "0.9", + "label": true, + "color": "#000000", + "fill": "#ffffff", + "fill-opacity": "0.97" + }, + "nodes": [ + "dcd65882403b8019", + "39ce9cfd3bee5c07", + "eb466c2601840e18", + "3605e72173f34f32", + "4526132d69e6c210", + "b49f7a7bb3272707", + "96d638d71a74e652", + "ce3029f2688a0aba", + "6ef121d9eb28bd91", + "74375df4238da5a5", + "4f214eb96e82b18b", + "dfdc2a0fceee62b1" + ], + "x": 2994, + "y": 1939, + "w": 1012, + "h": 202 + }, + { + "id": "e716da8ff5c458c9", + "type": "group", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "ENVIO MQTT", + "style": { + "stroke": "#ff0000", + "fill": "#ffbfbf", + "fill-opacity": "0.21", + "label": true, + "color": "#000000" + }, + "nodes": [ + "32e555ac8f46a0f1", + "e36bb58515b89053", + "6b1ae2b4cdb257fb", + "7ef59a8ac4d8a7f3", + "aba034491220fa28", + "ac94ab2b132fab52", + "774b2334aae0188c", + "9a5e603ea67d2991", + "9606dbbee4d79399" + ], + "x": 2074, + "y": 599, + "w": 552, + "h": 282 + }, + { + "id": "967aada409fe59f3", + "type": "group", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "TEST", + "style": { + "stroke": "#0070c0", + "fill": "#7fb7df", + "fill-opacity": "0.19", + "label": true + }, + "nodes": [ + "a4b79d82d1b9e587", + "b5cb8dc6dff15a6f", + "63ef47eeab45cd59", + "406737da0cea85cd", + "0626ad89ffb60829", + "18202cf1d5012bd6" + ], + "x": 1664, + "y": 979, + "w": 192, + "h": 282 + }, + { + "id": "f928d3e1f28c1f5b", + "type": "serial-port", + "z": "3fbc028716abf7a9", + "name": "", + "serialport": "/dev/ttyAMA1", + "serialbaud": "9600", + "databits": "8", + "parity": "none", + "stopbits": "1", + "waitfor": "", + "dtr": "none", + "rts": "none", + "cts": "none", + "dsr": "none", + "newline": "0", + "bin": "bin", + "out": "interbyte", + "addchar": "", + "responsetimeout": "10000" + }, + { + "id": "f962fbcc832a11c1", + "type": "ui_tab", + "name": "PESOS", + "icon": "dashboard", + "order": 1, + "disabled": false, + "hidden": true + }, + { + "id": "4eb9ee9b8ea93b8a", + "type": "mqtt-broker", + "name": "", + "broker": "trypton02.vservers.es", + "port": 1883, + "clientid": "", + "autoConnect": true, + "usetls": false, + "protocolVersion": 4, + "keepalive": 60, + "cleansession": true, + "autoUnsubscribe": true, + "birthTopic": "", + "birthQos": "0", + "birthRetain": "false", + "birthPayload": "", + "birthMsg": {}, + "closeTopic": "", + "closeQos": "0", + "closeRetain": "false", + "closePayload": "", + "closeMsg": {}, + "willTopic": "", + "willQos": "0", + "willRetain": "false", + "willPayload": "", + "willMsg": {}, + "userProps": "", + "sessionExpiry": "" + }, + { + "id": "e18837acff3382fd", + "type": "websocket-listener", + "path": "/", + "wholemsg": "false" + }, + { + "id": "cff29c99ac572574", + "type": "ui_group", + "name": "PESOS", + "tab": "f962fbcc832a11c1", + "order": 1, + "disp": false, + "width": "14", + "collapse": false, + "className": "{{msg.payload.bascula}}" + }, + { + "id": "97828a64326a9969", + "type": "ui_base", + "theme": { + "name": "theme-dark", + "lightTheme": { + "default": "#0094CE", + "baseColor": "#0094CE", + "baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif", + "edited": true, + "reset": false + }, + "darkTheme": { + "default": "#097479", + "baseColor": "#097479", + "baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif", + "edited": true, + "reset": false + }, + "customTheme": { + "name": "Untitled Theme 1", + "default": "#4B7930", + "baseColor": "#4B7930", + "baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif" + }, + "themeState": { + "base-color": { + "default": "#097479", + "value": "#097479", + "edited": false + }, + "page-titlebar-backgroundColor": { + "value": "#097479", + "edited": false + }, + "page-backgroundColor": { + "value": "#111111", + "edited": false + }, + "page-sidebar-backgroundColor": { + "value": "#333333", + "edited": false + }, + "group-textColor": { + "value": "#0eb8c0", + "edited": false + }, + "group-borderColor": { + "value": "#555555", + "edited": false + }, + "group-backgroundColor": { + "value": "#333333", + "edited": false + }, + "widget-textColor": { + "value": "#eeeeee", + "edited": false + }, + "widget-backgroundColor": { + "value": "#097479", + "edited": false + }, + "widget-borderColor": { + "value": "#333333", + "edited": false + }, + "base-font": { + "value": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif" + } + }, + "angularTheme": { + "primary": "indigo", + "accents": "blue", + "warn": "red", + "background": "grey", + "palette": "light" + } + }, + "site": { + "name": "Node-RED Dashboard", + "hideToolbar": "true", + "allowSwipe": "false", + "lockMenu": "false", + "allowTempTheme": "true", + "dateFormat": "DD/MM/YYYY", + "sizes": { + "sx": 48, + "sy": 48, + "gx": 6, + "gy": 6, + "cx": 6, + "cy": 6, + "px": 0, + "py": 0 + } + } + }, + { + "id": "da59cb3b7385573e", + "type": "mqtt-broker", + "name": "", + "broker": "trypton02.vservers.es", + "port": 1883, + "clientid": "", + "autoConnect": true, + "usetls": false, + "protocolVersion": 4, + "keepalive": 60, + "cleansession": true, + "autoUnsubscribe": true, + "birthTopic": "", + "birthQos": "0", + "birthRetain": "false", + "birthPayload": "", + "birthMsg": {}, + "closeTopic": "", + "closeQos": "0", + "closeRetain": "false", + "closePayload": "", + "closeMsg": {}, + "willTopic": "", + "willQos": "0", + "willRetain": "false", + "willPayload": "", + "willMsg": {}, + "userProps": "", + "sessionExpiry": "" + }, + { + "id": "79757467241b200a", + "type": "sqlitedb", + "db": "/home/trypton/sqlite/totemdb", + "mode": "RWC" + }, + { + "id": "93f25b867f297ffe", + "type": "websocket-listener", + "path": "/", + "wholemsg": "false" + }, + { + "id": "a8e74bb3a1610500", + "type": "sqlitedb", + "db": "/home/trypton/sqlite/supervisordb", + "mode": "RWC" + }, + { + "id": "3966bb218232f6b3", + "type": "mqtt-broker", + "name": "", + "broker": "trypton02.vservers.es", + "port": 1883, + "clientid": "", + "autoConnect": true, + "usetls": false, + "protocolVersion": 4, + "keepalive": 60, + "cleansession": true, + "autoUnsubscribe": true, + "birthTopic": "", + "birthQos": "0", + "birthRetain": "false", + "birthPayload": "", + "birthMsg": {}, + "closeTopic": "", + "closeQos": "0", + "closeRetain": "false", + "closePayload": "", + "closeMsg": {}, + "willTopic": "", + "willQos": "0", + "willRetain": "false", + "willPayload": "", + "willMsg": {}, + "userProps": "", + "sessionExpiry": "" + }, + { + "id": "790906edd33b7f43", + "type": "websocket-listener", + "path": "/", + "wholemsg": "false" + }, + { + "id": "1c8613764b7f685e", + "type": "MySQLdatabase", + "name": "supervisor", + "host": "127.0.0.1", + "port": "3306", + "db": "supervisordb", + "tz": "", + "charset": "UTF8" + }, + { + "id": "a00b83865450b74d", + "type": "modbus-client", + "name": "lazos", + "clienttype": "tcp", + "bufferCommands": true, + "stateLogEnabled": false, + "queueLogEnabled": false, + "failureLogEnabled": true, + "tcpHost": "192.168.11.200", + "tcpPort": 502, + "tcpType": "DEFAULT", + "serialPort": "/dev/ttyUSB", + "serialType": "RTU-BUFFERD", + "serialBaudrate": 9600, + "serialDatabits": 8, + "serialStopbits": 1, + "serialParity": "none", + "serialConnectionDelay": 100, + "serialAsciiResponseStartDelimiter": "0x3A", + "unit_id": 1, + "commandDelay": 1, + "clientTimeout": 1000, + "reconnectOnTimeout": true, + "reconnectTimeout": 2000, + "parallelUnitIdsAllowed": true, + "showErrors": true, + "showWarnings": true, + "showLogs": true + }, + { + "id": "f450a2dee7246e4b", + "type": "file in", + "z": "3fbc028716abf7a9", + "g": "663dc04a15a0be0c", + "name": "read-file", + "filename": "/home/trypton/.node-red/projects/config_raspeso_test.txt", + "filenameType": "str", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 2480, + "y": 440, + "wires": [ + [ + "d7b0f75b37ad9f53" + ] + ] + }, + { + "id": "e9767bab493f578b", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "663dc04a15a0be0c", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "1.5", + "topic": "", + "payload": "1", + "payloadType": "num", + "x": 2270, + "y": 440, + "wires": [ + [ + "f450a2dee7246e4b" + ] + ] + }, + { + "id": "aa13d9f112f46a30", + "type": "link in", + "z": "3fbc028716abf7a9", + "g": "663dc04a15a0be0c", + "name": "link in 2", + "links": [ + "1ba8bfe271072a91" + ], + "x": 2355, + "y": 480, + "wires": [ + [ + "f450a2dee7246e4b" + ] + ] + }, + { + "id": "b47df6182a4c503b", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "bfec5f4fae03e5b4", + "name": "ENVIO WEBSOCKET PESO ", + "func": "// Función Node-RED para WebSocket con lógica avanzada de envío condicional\n\nlet payload = msg.payload;\n\n// Validación\nif (typeof payload !== \"object\" || payload === null) {\n node.error(\"Payload no es un objeto JSON válido\", msg);\n return null;\n}\n\n// === Extraer datos ===\nlet peso = Number(payload.peso);\nlet estable = payload.estable;\nlet bascula = payload.bascula || \"\";\nlet totem_id='*';\n\nif (!Number.isFinite(peso) || !bascula.startsWith(\"Bascula\")) return null;\n\nlet numeroBascula = bascula.substring(7);\nlet vial = parseInt(numeroBascula);\n\n// === Claves por báscula ===\nlet keyUltimoPeso = `ws_ultimoPeso_${bascula}`;\nlet keyUltimoEstable = `ws_ultimoEstable_${bascula}`;\n\nlet ultimoPeso = flow.get(keyUltimoPeso);\nlet ultimoEstable = flow.get(keyUltimoEstable);\n\n// === Lógica de envío ===\nlet debeEnviar = false;\n\nif (estable) {\n // Si es estable, solo lo envío si es diferente del último estable enviado\n if (peso !== ultimoEstable) {\n flow.set(keyUltimoEstable, peso);\n flow.set(keyUltimoPeso, peso); // Actualiza también el último peso general\n debeEnviar = true;\n }\n} else {\n // No es estable → enviar solo si cambió respecto al último peso enviado\n if (peso !== ultimoPeso) {\n flow.set(keyUltimoPeso, peso);\n debeEnviar = true;\n }\n}\n\n// === Preparar y enviar si corresponde ===\n\n//node.warn(\"Debe Enviar:\" + debeEnviar);\nif (debeEnviar) {\n let nuevoPayload = {\n totem_id: totem_id,\n numero_vial: vial,\n peso: peso,\n peso_estable: estable\n };\n msg.payload = JSON.stringify(nuevoPayload);\n return msg;\n}\n\nreturn null; // No cumple condiciones de envío\n", + "outputs": 2, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2440, + "y": 640, + "wires": [ + [ + "6537522d1838f799", + "33449eadf7bfd3da", + "3e6f6384f2ec9846" + ], + [] + ] + }, + { + "id": "969714a9a2b1ef72", + "type": "link out", + "z": "3fbc028716abf7a9", + "name": "websocket1", + "mode": "link", + "links": [ + "a96c9a3676aad816" + ], + "x": 2125, + "y": 640, + "wires": [] + }, + { + "id": "a96c9a3676aad816", + "type": "link in", + "z": "3fbc028716abf7a9", + "g": "bfec5f4fae03e5b4", + "name": "WEBSOCKET", + "links": [ + "969714a9a2b1ef72" + ], + "x": 2205, + "y": 640, + "wires": [ + [ + "b47df6182a4c503b" + ] + ] + }, + { + "id": "6537522d1838f799", + "type": "debug", + "z": "3fbc028716abf7a9", + "g": "bfec5f4fae03e5b4", + "name": "debug 69", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2700, + "y": 700, + "wires": [] + }, + { + "id": "d7b0f75b37ad9f53", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "663dc04a15a0be0c", + "name": "config edit", + "func": "const config = JSON.parse(msg.payload);\nmsg ={}; // clear msg object\nlet read;\nlet config_general = []\nlet config_parametros = []\nlet config_rs232 = []\n\n//read = config.peso;\n\nconfig.general = toObject(config.general);\nconfig.parametros = toObject(config.parametros);\nconfig.rs232 = toObject(config.rs232);\n\n\n\n\n/************************************************NUEVOS***********************************************************************************/\n\nread = config.general.numeroBasculas;\nconfig_parametros.push({ \"parametro\": \"numeroBasculas\", \"valor\": read }) \nflow.set(\"numeroBasculas\", read); // crea variable numero de Basculas\n\n\n\nread = config.general.numeroTrama;\nconfig_parametros.push({ \"parametro\": \"numeroTrama\", \"valor\": read }) \nflow.set(\"numeroTrama\", read); // crea variable numero de trama\n//especificar las basculas que queremos\n\n//*********************BASCULA 1**************************************\nread = config.parametros.nombreBascula1;\nconfig_parametros.push({ \"parametro\": \"nombreBascula1\", \"valor\": read }) \nflow.set(\"nombreBascula1\", read); // crea variable nombreBascula1\n\nread = config.parametros.tcp_flag_Bascula1;\nconfig_parametros.push({ \"parametro\": \"tcp_flag_Bascula1\", \"valor\": read }) \nflow.set(\"tcp_flag_Bascula1\", read); // crea variable tcp_flag_bascula1\n\nread = config.parametros.InicioTramaBascula1;\nconfig_parametros.push({ \"parametro\": \"InicioTramaBascula1\", \"valor\": read }) \nflow.set(\"InicioTramaBascula1\", read); // crea variable InicioTramaBascula1\n\nread = config.parametros.FinalTramaBascula1;\nconfig_parametros.push({ \"parametro\": \"FinalTramaBascula1\", \"valor\": read }) \nflow.set(\"FinalTramaBascula1\", read); // crea variable FinalTramaBascula1\n\nread = config.parametros.LongitudTramaBascula1;\nconfig_parametros.push({ \"parametro\": \"LongitudTramaBascula1\", \"valor\": read }) \nflow.set(\"LongitudTramaBascula1\", read); // crea variable LongitudTramaBascula1\n\nread = config.parametros.InicioPesoBascula1;\nconfig_parametros.push({ \"parametro\": \"InicioPesoBascula1\", \"valor\": read }) \nflow.set(\"InicioPesoBascula1\", read); // crea variable InicioPesoBascula1\n\nread = config.parametros.LongitudPesoBascula1;\nconfig_parametros.push({ \"parametro\": \"LongitudPesoBascula1\", \"valor\": read }) \nflow.set(\"LongitudPesoBascula1\", read); // crea variable LongitudPesoBascula1\n\nread = config.parametros.hostTCPBascula1;\nconfig_parametros.push({ \"parametro\": \"hostTCPBascula1\", \"valor\": read }) \nflow.set(\"hostTCPBascula1\", read); // crea variable InicioTrama\n\nread = config.parametros.puertoTCPBascula1;\nconfig_parametros.push({ \"parametro\": \"puertoTCPBascula1\", \"valor\": read }) \nflow.set(\"puertoTCPBascula1\", read); // crea variable InicioTrama\n\n\n//*********************BASCULA 2**************************************\n\n\nread = config.parametros.nombreBascula2;\nconfig_parametros.push({ \"parametro\": \"nombreBascula2\", \"valor\": read }) \nflow.set(\"nombreBascula2\", read); // crea variable nombreBascula2\n\nread = config.parametros.tcp_flag_Bascula2;\nconfig_parametros.push({ \"parametro\": \"tcp_flag_Bascula2\", \"valor\": read }) \nflow.set(\"tcp_flag_Bascula2\", read); // crea variable tcp_flag_bascula2\n\nread = config.parametros.InicioTramaBascula2;\nconfig_parametros.push({ \"parametro\": \"InicioTramaBascula2\", \"valor\": read }) \nflow.set(\"InicioTramaBascula2\", read); // crea variable InicioTramaBascula2\n\nread = config.parametros.FinalTramaBascula2;\nconfig_parametros.push({ \"parametro\": \"FinalTramaBascula2\", \"valor\": read }) \nflow.set(\"FinalTramaBascula2\", read); // crea variable FinalTramaBascula2\n\nread = config.parametros.LongitudTramaBascula2;\nconfig_parametros.push({ \"parametro\": \"LongitudTramaBascula2\", \"valor\": read }) \nflow.set(\"LongitudTramaBascula2\", read); // crea variable LongitudTramaBascula2\n\nread = config.parametros.InicioPesoBascula2;\nconfig_parametros.push({ \"parametro\": \"InicioPesoBascula2\", \"valor\": read }) \nflow.set(\"InicioPesoBascula2\", read); // crea variable InicioPesoBascula2\n\nread = config.parametros.LongitudPesoBascula2;\nconfig_parametros.push({ \"parametro\": \"LongitudPesoBascula2\", \"valor\": read }) \nflow.set(\"LongitudPesoBascula2\", read); // crea variable LongitudPesoBascula2\n\nread = config.parametros.hostTCPBascula2;\nconfig_parametros.push({ \"parametro\": \"hostTCPBascula2\", \"valor\": read }) \nflow.set(\"hostTCPBascula2\", read); // crea variable InicioTrama\n\nread = config.parametros.puertoTCPBascula2;\nconfig_parametros.push({ \"parametro\": \"puertoTCPBascula2\", \"valor\": read }) \nflow.set(\"puertoTCPBascula2\", read); // crea variable InicioTrama\n\n//*********************BASCULA 3**************************************\n\n\nread = config.parametros.nombreBascula3;\nconfig_parametros.push({ \"parametro\": \"nombreBascula3\", \"valor\": read }) \nflow.set(\"nombreBascula3\", read); // crea variable nombreBascula3\n\nread = config.parametros.tcp_flag_Bascula3;\nconfig_parametros.push({ \"parametro\": \"tcp_flag_Bascula3\", \"valor\": read }) \nflow.set(\"tcp_flag_Bascula3\", read); // crea variable tcp_flag_bascula3\n\nread = config.parametros.InicioTramaBascula3;\nconfig_parametros.push({ \"parametro\": \"InicioTramaBascula3\", \"valor\": read }) \nflow.set(\"InicioTramaBascula3\", read); // crea variable InicioTramaBascula3\n\nread = config.parametros.FinalTramaBascula3;\nconfig_parametros.push({ \"parametro\": \"FinalTramaBascula3\", \"valor\": read }) \nflow.set(\"FinalTramaBascula3\", read); // crea variable FinalTramaBascula3\n\nread = config.parametros.LongitudTramaBascula3;\nconfig_parametros.push({ \"parametro\": \"LongitudTramaBascula3\", \"valor\": read })\nflow.set(\"LongitudTramaBascula3\", read); // crea variable LongitudTramaBascula3\n\nread = config.parametros.InicioPesoBascula3;\nconfig_parametros.push({ \"parametro\": \"InicioPesoBascula3\", \"valor\": read }) \nflow.set(\"InicioPesoBascula3\", read); // crea variable InicioPesoBascula3\n\nread = config.parametros.LongitudPesoBascula3;\nconfig_parametros.push({ \"parametro\": \"LongitudPesoBascula3\", \"valor\": read }) \nflow.set(\"LongitudPesoBascula3\", read); // crea variable LongitudPesoBascula3\n\nread = config.parametros.hostTCPBascula3;\nconfig_parametros.push({ \"parametro\": \"hostTCPBascula3\", \"valor\": read }) \nflow.set(\"hostTCPBascula3\", read); // crea variable InicioTrama3\n\nread = config.parametros.puertoTCPBascula3;\nconfig_parametros.push({ \"parametro\": \"puertoTCPBascula3\", \"valor\": read }) \nflow.set(\"puertoTCPBascula3\", read); // crea variable InicioTram3\n//*********************BASCULA 4**************************************\n\nread = config.parametros.nombreBascula4;\nconfig_parametros.push({ \"parametro\": \"nombreBascula4\", \"valor\": read }) \nflow.set(\"nombreBascula4\", read); // crea variable nombreBascula3\n\nread = config.parametros.tcp_flag_Bascula4;\nconfig_parametros.push({ \"parametro\": \"tcp_flag_Bascula4\", \"valor\": read }) \nflow.set(\"tcp_flag_Bascula4\", read); // crea variable tcp_flag_bascula2\n\nread = config.parametros.InicioTramaBascula4;\nconfig_parametros.push({ \"parametro\": \"InicioTramaBascula4\", \"valor\": read }) \nflow.set(\"InicioTramaBascula4\", read); // crea variable InicioTramaBascula4\n\nread = config.parametros.FinalTramaBascula4;\nconfig_parametros.push({ \"parametro\": \"FinalTramaBascula4\", \"valor\": read }) \nflow.set(\"FinalTramaBascula4\", read); // crea variable FinalTramaBascula4\n\nread = config.parametros.LongitudTramaBascula4;\nconfig_parametros.push({ \"parametro\": \"LongitudTramaBascula4\", \"valor\": read })\nflow.set(\"LongitudTramaBascula4\", read); // crea variable LongitudTramaBascula4\n\nread = config.parametros.InicioPesoBascula4;\nconfig_parametros.push({ \"parametro\": \"InicioPesoBascula4\", \"valor\": read }) \nflow.set(\"InicioPesoBascula4\", read); // crea variable InicioPesoBascula3\n\nread = config.parametros.LongitudPesoBascula4;\nconfig_parametros.push({ \"parametro\": \"LongitudPesoBascula4\", \"valor\": read }) \nflow.set(\"LongitudPesoBascula4\", read); // crea variable LongitudPesoBascula3\n\nread = config.parametros.hostTCPBascula4;\nconfig_parametros.push({ \"parametro\": \"hostTCPBascula4\", \"valor\": read }) \nflow.set(\"hostTCPBascula4\", read); // crea variable InicioTrama3\n\nread = config.parametros.puertoTCPBascula4;\nconfig_parametros.push({ \"parametro\": \"puertoTCPBascula4\", \"valor\": read }) \nflow.set(\"puertoTCPBascula4\", read); // crea variable InicioTram3\n\n\nread = config.rs232.puertoSerie1;\nconfig_rs232.push({ \"parametro\": \"puertoSerie1\", \"valor\": read }) \nflow.set(\"puertoSerie1\", read); // crea variable puertoSerie1\n\nread = config.rs232.velocidadBaudiosSerie1;\nconfig_rs232.push({ \"parametro\": \"velocidadBaudiosSerie1\", \"valor\": read }) \nflow.set(\"velocidadBaudiosSerie1\", read); // crea variable velocidadBaudiosSerie1\n\nread = config.rs232.databitsSerie1;\nconfig_rs232.push({ \"parametro\": \"databitsSerie1\", \"valor\": read }) \nflow.set(\"databitsSerie1\", read); // crea variable databitsSerie1\n\nread =config.rs232.paridadSerie1;\nconfig_rs232.push({ \"parametro\": \"paridadSerie1\", \"valor\": read }) \nflow.set(\"paridadSerie1\", read); // crea variable paridadSerie1\n\nread = config.rs232.stopbitsSerie1;\nconfig_rs232.push({ \"parametro\": \"stopbitsSerie1\", \"valor\": read }) \nflow.set(\"stopbitsSerie1\", read); // crea variable stopbitsSerie1\n\nread = config.rs232.puertoHabilitadoSerie1;\nconfig_rs232.push({ \"parametro\": \"puertoHabilitadoSerie1\", \"valor\": read }) \nflow.set(\"puertoHabilitadoSerie1\", read); // crea variable puertoHabilitadoSerie1\n\n\nread = config.rs232.puertoSerie2;\nconfig_rs232.push({ \"parametro\": \"puertoSerie2\", \"valor\": read }) \nflow.set(\"puertoSerie2\", read); // crea variable puertoSerie2\n\nread = config.rs232.velocidadBaudiosSerie2;\nconfig_rs232.push({ \"parametro\": \"velocidadBaudiosSerie2\", \"valor\": read }) \nflow.set(\"velocidadBaudiosSerie2\", read); // crea variable velocidadBaudiosSerie2\n\nread = config.rs232.databitsSerie2;\nconfig_rs232.push({ \"parametro\": \"databitsSerie2\", \"valor\": read }) \nflow.set(\"databitsSerie2\", read); // crea variable databitsSerie2\n\nread = config.rs232.paridadSerie2;\nconfig_rs232.push({ \"parametro\": \"paridadSerie2\", \"valor\": read }) \nflow.set(\"paridadSerie2\", read); // crea variable paridadSerie2\n\nread = config.rs232.stopbitsSerie2;\nconfig_rs232.push({ \"parametro\": \"stopbitsSerie2\", \"valor\": read }) \nflow.set(\"stopbitsSerie2\", read); // crea variable stopbitsSerie2\n\nread = config.rs232.puertoHabilitadoSerie2;\nconfig_rs232.push({ \"parametro\": \"puertoHabilitadoSerie2\", \"valor\": read }) \nflow.set(\"puertoHabilitadoSerie2\", read); // crea variable puertoHabilitadoSerie2\n\n\nread = config.rs232.puertoSerie3;\nconfig_rs232.push({ \"parametro\": \"puertoSerie3\", \"valor\": read }) \nflow.set(\"puertoSerie3\", read); // crea variable puertoSerie3\n\nread = config.rs232.velocidadBaudiosSerie3;\nconfig_rs232.push({ \"parametro\": \"velocidadBaudiosSerie3\", \"valor\": read }) \nflow.set(\"velocidadBaudiosSerie3\", read); // crea variable velocidadBaudiosSerie3\n\nread = config.rs232.databitsSerie3;\nconfig_rs232.push({ \"parametro\": \"databitsSerie3\", \"valor\": read }) \nflow.set(\"databitsSerie3\", read); // crea variable databitsSerie3\n\nread =config.rs232.paridadSerie3;\nconfig_rs232.push({ \"parametro\": \"paridadSerie3\", \"valor\": read }) \nflow.set(\"paridadSerie3\", read); // crea variable paridadSerie3\n\nread = config.rs232.stopbitsSerie3;\nconfig_rs232.push({ \"parametro\": \"stopbitsSerie3\", \"valor\": read }) \nflow.set(\"stopbitsSerie3\", read); // crea variable stopbitsSerie3\n\nread = config.rs232.puertoHabilitadoSerie3;\nconfig_rs232.push({ \"parametro\": \"puertoHabilitadoSerie3\", \"valor\": read }) \nflow.set(\"puertoHabilitadoSerie3\", read); // crea variable puertoHabilitadoSerie3\n\n\n\nread = config.rs232.puertoSerie4;\nconfig_rs232.push({ \"parametro\": \"puertoSerie4\", \"valor\": read }) \nflow.set(\"puertoSerie4\", read); // crea variable puertoSerie4\n\nread = config.rs232.velocidadBaudiosSerie4;\nconfig_rs232.push({ \"parametro\": \"velocidadBaudiosSerie4\", \"valor\": read }) \nflow.set(\"velocidadBaudiosSerie4\", read); // crea variable velocidadBaudiosSerie4\n\nread = config.rs232.databitsSerie4;\nconfig_rs232.push({ \"parametro\": \"databitsSerie4\", \"valor\": read }) \nflow.set(\"databitsSerie4\", read); // crea variable databitsSerie4\n\nread = config.rs232.paridadSerie4;\nconfig_rs232.push({ \"parametro\": \"paridadSerie4\", \"valor\": read }) \nflow.set(\"paridadSerie4\", read); // crea variable paridadSerie4\n\nread = config.rs232.stopbitsSerie4;\nconfig_rs232.push({ \"parametro\": \"stopbitsSerie4\", \"valor\": read }) \nflow.set(\"stopbitsSerie4\", read); // crea variable stopbitsSerie4\n\nread = config.rs232.puertoHabilitadoSerie4;\nconfig_rs232.push({ \"parametro\": \"puertoHabilitadoSerie4\", \"valor\": read }) \nflow.set(\"puertoHabilitadoSerie4\", read); // crea variable puertoHabilitadoSerie4\n\n\n\n\n//***************************pantalla configuracion***********************//\n\nread = config.general.auth_pin;\nconfig_general.push({ \"parametro\": \"auth_pin\", \"valor\": read }) \nflow.set(\"auth_pin\", read)\n\nread = config.general.numero_clicks; // numero de clicks necesarios en la ventana de tiempo para poder acceder a la pantalla de mantenimiento \nconfig_general.push({ \"parametro\": \"numero_clicks\", \"valor\": read }) \nflow.set(\"numero_clicks\", read)\n\nread = config.general.mante_window_time; // tiempo de duracion de la ventana de espera del numero de clicks para autorizar el pase a la pantalla de mantenimiento\nconfig_general.push({ \"parametro\": \"mante_window_time\", \"valor\": read }) \nflow.set(\"mante_window_time\", read)\n\nread = config.general.pin_timeout; // timeout de espera del pin\nconfig_general.push({ \"parametro\": \"pin_timeout\", \"valor\": read }) \nflow.set(\"pin_timeout\", read)\n\nread = config.general.mante_timeout; // timeout de la pantalla de mantenimiento\nconfig_general.push({ \"parametro\": \"mante_timeout\", \"valor\": read }) \nflow.set(\"mante_timeout\", read)\n\nread = config.general.sw_bkgnd_on; // timeout de la pantalla de mantenimiento\nconfig_general.push({ \"parametro\": \"sw_bkgnd_on\", \"valor\": read }) \nflow.set(\"sw_bkgnd_on\", read)\n\nread = config.general.sw_bkgnd_off; // timeout de la pantalla de mantenimiento\nconfig_general.push({ \"parametro\": \"sw_bkgnd_off\", \"valor\": read }) \nflow.set(\"sw_bkgnd_off\", read)\n\nread = config.general.titulo_param_color; // timeout de la pantalla de mantenimiento\nconfig_general.push({ \"parametro\": \"titulo_param_color\", \"valor\": read }) \nflow.set(\"titulo_param_color\", read)\n\nread = config.general.value_param_color; // timeout de la pantalla de mantenimiento\nconfig_general.push({ \"parametro\": \"value_param_color\", \"valor\": read }) \nflow.set(\"value_param_color\", read)\n\nread = config.general.namefixedparams_color; // timeout de la pantalla de namefixedparams_color\nconfig_general.push({ \"parametro\": \"namefixedparams_color\", \"valor\": read }) \nflow.set(\"namefixedparams_color\", read)\n\n\nflow.set(\"config_general\", config_general)\nflow.set(\"config_parametros\", config_parametros)\nflow.set(\"config_rs232\", config_rs232)\n\nreturn;\n\n//*********************************************************** FUNCIONES **************************************************/\nfunction toObject(array) {\n const obj = {};\n array.forEach(el => {\n obj[el.parametro] = el.valor;\n });\n return obj;\n}", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2710, + "y": 440, + "wires": [ + [] + ] + }, + { + "id": "03b120c9713eb333", + "type": "ui_template", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "group": "cff29c99ac572574", + "name": "PesoBascula3", + "order": 8, + "width": "7", + "height": "2", + "format": "\n\n\n\n\t
\n\t \n \t\t

\n\t\t\t{{msg.payload.pesoString}}

{{msg.payload.mensaje}}
\n\t\t

\n\t
\n", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 1940, + "y": 980, + "wires": [ + [] + ] + }, + { + "id": "14fbc044898db247", + "type": "ui_text", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "group": "cff29c99ac572574", + "order": 6, + "width": "7", + "height": "2", + "name": "Bascula3", + "label": "", + "format": "{{msg.payload.bascula}}", + "layout": "row-left", + "className": "", + "x": 1920, + "y": 1020, + "wires": [] + }, + { + "id": "f4cda14c866fa39a", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "envio WEBSOCKET, TRAMA,PESO Bascula3", + "func": "// ======= Parámetros =======\nconst muestras = flow.get(\"muestras\") || 5;\nconst escalon = flow.get(\"escalon\") || 20;\nconst minPesoVal = flow.get(\"minPesoVal\") || 0;\nconst TIMEOUT_MS = flow.get(\"pesoTimeoutMs\") || 5000;\n\n// ======= Entradas =======\nlet bascula = flow.get(\"nombreBascula3\") || \"Bascula3\";\nlet payloadIn = msg.payload || {};\nlet comunicacion = msg.topic;\nlet trama = payloadIn.trama;\nlet mensaje = payloadIn.mensaje;\n\n// ======= Timeout por báscula =======\nconst keyTimer = `b3_${bascula}_timer`;\nlet t = context.get(keyTimer);\nif (t) clearTimeout(t);\nt = setTimeout(() => {\n const aviso = {\n payload: {\n bascula,\n peso: null,\n pesoString: \"\",\n peso_estable: null,\n estable: false,\n comunicacion,\n trama: \"\",\n mensaje: \"No se han recibido datos en los últimos 5 segundos\",\n statuscolor: \"yellow\"\n }\n };\n flow.set(\"peso3\",\"---\")\n node.send([aviso, null]);\n}, TIMEOUT_MS);\ncontext.set(keyTimer, t);\n\n// ======= Normaliza el peso =======\nconst tienePeso = Object.prototype.hasOwnProperty.call(payloadIn, \"peso\");\nconst rawPeso = payloadIn.peso;\nconst vacio = (rawPeso === \"\" || rawPeso === null || typeof rawPeso === \"undefined\");\nconst pesoNum = Number(rawPeso);\nconst esNumero = Number.isFinite(pesoNum);\n\n// ======= Caso SIN lectura válida -> mensaje, NO 0 =======\nif (!tienePeso || vacio || !esNumero || pesoNum < minPesoVal) {\n const aviso = {\n payload: {\n bascula,\n peso: null,\n pesoString: \"\",\n peso_estable: null,\n estable: false,\n comunicacion,\n trama: trama || \"\",\n mensaje: (typeof mensaje === \"string\" ? mensaje : \"NO LLEGA PESO\"),\n statuscolor: \"yellow\"\n }\n };\n flow.set(\"peso3\",\"---\")\n return [aviso, null];\n}\n\n// ======= Lectura válida =======\nlet peso = pesoNum;\nlet pesoString = String(peso);\nlet colorStatus = peso <= 500 ? \"lime\" : \"yellow\";\n\n// ======= Estado por báscula (claves únicas) =======\nconst keyBase = `b3_${bascula}`;\nconst keyUltimo = `${keyBase}_ultimoPeso`;\nconst keyUltimoEst = `${keyBase}_ultimoPesoEstable`;\nconst keyBuffer = `${keyBase}_buffer`;\n\n// ======= Buffer circular de lecturas (solo válidas) =======\nlet buffer = flow.get(keyBuffer) || [];\nbuffer.unshift(peso);\nif (buffer.length > muestras) buffer.pop();\nflow.set(keyBuffer, buffer);\n\n// ======= Evaluación de estabilidad =======\nlet esEstable = buffer.length === muestras &&\n buffer.slice(1).every(v => Math.abs(buffer[0] - v) <= escalon);\n\nlet pesoEstable = esEstable ? buffer[0] : null;\n\n// ======= Control de repeticiones =======\nlet ultimoEst = flow.get(keyUltimoEst);\nif (esEstable && ultimoEst !== pesoEstable) {\n flow.set(keyUltimoEst, pesoEstable);\n}\n\n// ======= Salida =======\nflow.set(keyUltimo, peso);\n\nreturn [{\n payload: {\n bascula,\n peso,\n pesoString,\n peso_estable: pesoEstable,\n estable: esEstable,\n comunicacion,\n trama,\n mensaje: (typeof mensaje === \"string\" ? mensaje : \"\"),\n statuscolor: colorStatus\n }\n}, null];\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1540, + "y": 1020, + "wires": [ + [ + "969714a9a2b1ef72", + "14fbc044898db247", + "03b120c9713eb333" + ] + ] + }, + { + "id": "fad252cfbd6688a3", + "type": "tcp-client", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "action": "action", + "actionType": "msg", + "host": "host", + "hostType": "msg", + "port": "port", + "portType": "msg", + "datamode": "stream", + "datatype": "buffer", + "newline": "", + "write": " ", + "writeType": "str", + "maxRetries": "", + "retryDelay": "", + "indefiniteRetries": true, + "topic": "basculaTCP3", + "name": "BASCULATCP3", + "debug": "info", + "x": 380, + "y": 900, + "wires": [ + [ + "6dbef4dc022becea", + "1457adeb1e1238af" + ] + ] + }, + { + "id": "eb3ddc79e17c0fd3", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "Enviar Datos", + "props": [ + { + "p": "host", + "v": "hostTCPBascula3", + "vt": "flow" + }, + { + "p": "port", + "v": "puertoTCPBascula3", + "vt": "flow" + }, + { + "p": "action", + "v": "connect", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "2", + "topic": "", + "x": 190, + "y": 900, + "wires": [ + [ + "fad252cfbd6688a3" + ] + ] + }, + { + "id": "6dbef4dc022becea", + "type": "change", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "TCPBascula3", + "rules": [ + { + "t": "set", + "p": "topic", + "pt": "msg", + "to": "TCP", + "tot": "str" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 600, + "y": 900, + "wires": [ + [ + "34291c27b4f82d26" + ] + ] + }, + { + "id": "2d9d86f7a82a37eb", + "type": "change", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "RS-232Bascula3", + "rules": [ + { + "t": "set", + "p": "topic", + "pt": "msg", + "to": "RS-232", + "tot": "str" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 570, + "y": 1020, + "wires": [ + [ + "ac8abf3f7ec09ac9" + ] + ] + }, + { + "id": "9673d34b6f177b10", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "TRAMABascula3", + "func": "\nvar payload = msg.payload; // Obtener los datos del mensaje\nvar buffer = Buffer.from(payload); // Convertir a Buffer para manipulación de bytes\nconst stx = flow.get(\"InicioTramaBascula3\"); // Suponiendo que STX es 0x02 (ajusta según tu protocolo)\nconst etx = flow.get(\"FinalTramaBascula3\"); // Suponiendo que ETX es 0x03 (ajusta según tu protocolo)\nvar frameStart = 0;\nvar frames = [];\nvar remainingData = flow.get(\"remainingData3\") || Buffer.alloc(0); // Inicializar o usar el buffer restante de la llamada anterior\n\n// Concatenar los datos restantes de la llamada anterior con los nuevos datos\nbuffer = Buffer.concat([remainingData, buffer]);\n\nfor (var i = 0; i < buffer.length; i++) {\n if (buffer[i] === stx) {\n frameStart = i;\n } else if (buffer[i] === etx) {\n frames.push(buffer.slice(frameStart, i + 1));\n buffer = buffer.slice(i + 1); // Eliminar la trama procesada\n i = -1; // Reiniciar el índice para el siguiente bucle\n }\n}\n\n// Si queda un fragmento de trama al final, almacenarlo para la próxima llamada\nflow.set(\"remainingData3\", buffer);\nif(frames.length != 0){\n msg.payload = frames;\n node.send(msg)\n}\nreturn ;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1010, + "y": 1020, + "wires": [ + [ + "1381985c88fc5dbb" + ] + ] + }, + { + "id": "1381985c88fc5dbb", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "PESOBascula3", + "func": "\nvar payload = msg.payload; // Obtener las tramas (array de buffers)\nconst LongitudTrama = flow.get(\"LongitudTramaBascula3\"); // Longitud esperada de la trama (ajusta según tu caso)\nconst InicioPeso = flow.get(\"InicioPesoBascula3\"); // Posición inicial del peso en la trama (ajusta según tu caso)\nconst LongitudPeso = flow.get(\"LongitudPesoBascula3\"); // Longitud del valor del peso en la trama (ajusta según tu caso)\nlet tramaString='';\nvar processedFrames = [];\n\n// Obtener el temporizador previamente almacenado\nlet timeoutId = flow.get(\"timeoutIdBascula3\") || null;\nconst timeoutDuration = 5000; // Tiempo de espera en milisegundos\n\n// Limpiar el temporizador anterior si existe\nif (timeoutId) {\n clearTimeout(timeoutId);\n}\n\n// Establecer un nuevo temporizador para enviar el mensaje vacío si no hay actividad\ntimeoutId = setTimeout(() => {\n node.warn(\"No se han recibido datos en los últimos 5 segundos.\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': '',\n 'mensaje': 'No se han recibido datos en los últimos 5 segundos'\n };\n node.send(msg);\n}, timeoutDuration);\n\n// Guardar el nuevo temporizador en el contexto de flujo para futuras referencias\nflow.set(\"timeoutIdBascula3\", timeoutId);\n\nfor (var i = 0; i < payload.length; i++) {\n var frame = payload[i];\n tramaString += payload[i].toString();\n\n if (frame.length === LongitudTrama) {\n // Extraer el valor del peso y convertirlo a string\n var pesoBuffer = frame.slice(InicioPeso, InicioPeso + LongitudPeso);\n var pesoString = pesoBuffer.toString('utf8'); // Asumiendo que el peso es un string UTF-8\n\n // Agregar la trama procesada al array de salida\n processedFrames.push({\n trama: frame,\n peso: pesoString,\n pesoBuffer: pesoBuffer\n });\n\n // Enviar el mensaje con datos válidos\n msg.payload = {\n 'peso': parseInt(pesoString),\n 'pesoString': pesoString,\n 'trama': tramaString,\n 'mensaje': ''\n };\n flow.set(\"peso3\",pesoString)\n node.send(msg);\n\n // Reiniciar el temporizador al recibir datos válidos\n clearTimeout(timeoutId);\n timeoutId = setTimeout(() => {\n node.warn(\"No se han recibido datos en los últimos 5 segundos.\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': '',\n 'mensaje': 'No se han recibido datos en los últimos 5 segundos'\n };\n // node.send(msg);\n let msg_close = {action:\"close\", host:flow.get(\"hostTCPBascula3\"), port:flow.get(\"puertoTCPBascula3\")}\n node.send([msg, msg_close]);\n }, timeoutDuration);\n flow.set(\"timeoutIdBascula3\", timeoutId);\n } else {\n // Manejamos la trama inválida\n node.warn(\"Trama inválida\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': tramaString,\n 'mensaje': 'TRAMA INVALIDA'\n };\n node.send(msg);\n }\n}\n", + "outputs": 2, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1240, + "y": 1020, + "wires": [ + [ + "f4cda14c866fa39a" + ], + [ + "6731bec53435c2d5" + ] + ] + }, + { + "id": "34291c27b4f82d26", + "type": "switch", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "TCPBascula3", + "property": "tcp_flag_Bascula3", + "propertyType": "flow", + "rules": [ + { + "t": "true" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 800, + "y": 900, + "wires": [ + [ + "9673d34b6f177b10" + ] + ] + }, + { + "id": "ac8abf3f7ec09ac9", + "type": "switch", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "RS-232 Bascula3", + "property": "tcp_flag_Bascula3", + "propertyType": "flow", + "rules": [ + { + "t": "false" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 790, + "y": 1020, + "wires": [ + [ + "9673d34b6f177b10" + ] + ] + }, + { + "id": "f89e176a87f0d923", + "type": "serial in", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "", + "serial": "f928d3e1f28c1f5b", + "x": 370, + "y": 1020, + "wires": [ + [ + "2d9d86f7a82a37eb" + ] + ] + }, + { + "id": "a8d75f0609d7f8ed", + "type": "serial control", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "", + "serial": "f928d3e1f28c1f5b", + "x": 370, + "y": 1080, + "wires": [ + [] + ] + }, + { + "id": "6731bec53435c2d5", + "type": "link out", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "tcp3close_out", + "mode": "link", + "links": [ + "f178e2126d397565" + ], + "x": 1385, + "y": 1060, + "wires": [] + }, + { + "id": "1457adeb1e1238af", + "type": "link out", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "bascula1", + "mode": "link", + "links": [ + "9667a055f9c967a7" + ], + "x": 575, + "y": 840, + "wires": [] + }, + { + "id": "5fe3f27656425fc8", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "function 3", + "func": "/*msg.action = close\nmsg.host = flow.get(\"hostTCPBascula1\")\nmsg.port = flow.get(\"puertoTCPBascula1\")\n*/\nnode.send(msg)\nsetTimeout(() => {\n let msg_close = {action:\"connect\", host:flow.get(\"hostTCPBascula3\"), port:flow.get(\"puertoTCPBascula3\")}\n node.send(msg_close);\n }, 2000);\nreturn;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 360, + "y": 980, + "wires": [ + [ + "fad252cfbd6688a3" + ] + ] + }, + { + "id": "f178e2126d397565", + "type": "link in", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "tcp3close", + "links": [ + "6731bec53435c2d5" + ], + "x": 215, + "y": 1020, + "wires": [ + [ + "5fe3f27656425fc8" + ] + ] + }, + { + "id": "183a0ae1b9315d02", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "aadbe4b53903ecda", + "name": "Enviar Datos", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "3", + "topic": "", + "payload": "{ \"serialport\": puertoSerie3, \"serialbaud\": velocidadBaudiosSerie3, \"databits\": databitsSerie3, \"parity\": paridadSerie3, \"stopbits\": stopbitsSerie3, \"enabled\": puertoHabilitadoSerie3 }", + "payloadType": "str", + "x": 170, + "y": 1080, + "wires": [ + [ + "a8d75f0609d7f8ed" + ] + ] + }, + { + "id": "2d26d00c4530ee1f", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "envio WEBSOCKET, TRAMA,PESO Bascula1", + "func": "// ======= Parámetros =======\nconst muestras = flow.get(\"muestras\") || 5;\nconst escalon = flow.get(\"escalon\") || 20;\nconst minPesoVal = flow.get(\"minPesoVal\") || 0; // < a esto = sin peso\nconst TIMEOUT_MS = flow.get(\"pesoTimeoutMs\") || 5000; // 5s sin dato => aviso\n\n// ======= Entradas =======\nlet bascula = flow.get(\"nombreBascula1\") || \"Bascula1\";\nlet payloadIn = msg.payload || {};\nlet comunicacion = msg.topic;\nlet trama = payloadIn.trama;\nlet mensaje = payloadIn.mensaje;\n\n// ======= Watchdog por báscula (5s) =======\nconst keyTimer = `b1_${bascula}_timer`;\nlet t = context.get(keyTimer);\nif (t) clearTimeout(t);\nt = setTimeout(() => {\n const aviso = {\n payload: {\n bascula,\n peso: null, // 👈 nunca '' para no caer en 0\n pesoString: \"\",\n peso_estable: null,\n estable: false,\n comunicacion,\n trama: \"\",\n mensaje: \"No se han recibido datos en los últimos 5 segundos\",\n statuscolor: \"yellow\"\n }\n };\n flow.set(\"peso1\",\"---\")\n node.send([aviso, null]);\n}, TIMEOUT_MS);\ncontext.set(keyTimer, t);\n\n// ======= Normaliza el peso (evita Number('') === 0) =======\nlet tienePeso = Object.prototype.hasOwnProperty.call(payloadIn, \"peso\");\nlet rawPeso = payloadIn.peso;\nlet vacio = (rawPeso === \"\" || rawPeso === null || typeof rawPeso === \"undefined\");\nlet pesoNum = Number(rawPeso);\nlet esNumero = Number.isFinite(pesoNum);\n\n// ======= Caso SIN lectura válida -> mensaje (no 0) =======\nif (!tienePeso || vacio || !esNumero || pesoNum < minPesoVal) {\n let aviso = {\n payload: {\n bascula: bascula,\n peso: null,\n pesoString: \"\",\n peso_estable: null,\n estable: false,\n comunicacion: comunicacion,\n trama: trama || \"\",\n mensaje: (typeof mensaje === \"string\" ? mensaje : \"NO LLEGA PESO\"),\n statuscolor: \"yellow\"\n }\n };\n flow.set(\"peso1\",\"---\")\n return [aviso, null];\n}\n\n// ======= Lectura válida =======\nlet peso = pesoNum;\nlet pesoString = String(peso);\nlet colorStatus = peso <= 500 ? 'lime' : 'yellow';\n\n// ======= Estado por báscula (claves únicas) =======\nconst keyBase = `b1_${bascula}`;\nconst keyUltimo = `${keyBase}_ultimoPeso`;\nconst keyUltimoEst = `${keyBase}_ultimoPesoEstable`;\nconst keyBuffer = `${keyBase}_buffer`;\n\n// ======= Buffer circular de lecturas (solo válidas) =======\nlet buffer = flow.get(keyBuffer) || [];\nbuffer.unshift(peso);\nif (buffer.length > muestras) buffer.pop();\nflow.set(keyBuffer, buffer);\n\n// ======= Evaluación de estabilidad =======\nlet esEstable = buffer.length === muestras &&\n buffer.slice(1).every(v => Math.abs(buffer[0] - v) <= escalon);\n\nlet pesoEstable = esEstable ? buffer[0] : null;\n\n// ======= Control de repeticiones =======\nlet ultimoEst = flow.get(keyUltimoEst);\nif (esEstable && ultimoEst !== pesoEstable) {\n flow.set(keyUltimoEst, pesoEstable);\n}\n\n// ======= Mensaje final (lectura válida) =======\nlet mensajeComun = {\n payload: {\n bascula: bascula,\n peso: peso,\n pesoString: pesoString,\n peso_estable: pesoEstable,\n estable: esEstable,\n comunicacion: comunicacion,\n trama: trama,\n mensaje: (typeof mensaje === \"string\" ? mensaje : \"\"),\n statuscolor: colorStatus\n }\n};\n\n// ======= Salida =======\nflow.set(keyUltimo, peso); // actualizar último peso válido\nreturn [mensajeComun, null];\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1580, + "y": 300, + "wires": [ + [ + "760d3fa03416427f", + "9e837a6339e80936", + "969714a9a2b1ef72" + ] + ] + }, + { + "id": "9e837a6339e80936", + "type": "ui_template", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "group": "cff29c99ac572574", + "name": "PesoBascula1", + "order": 3, + "width": "7", + "height": "2", + "format": "\n\n\n\n\t
\n\t \n \t\t

\n\t\t\t{{msg.payload.pesoString}}

\n\t\t\t
{{msg.payload.mensaje}}
\n\t\t\n\t
\n", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 1940, + "y": 300, + "wires": [ + [] + ] + }, + { + "id": "7a6fc5c1767b38b0", + "type": "tcp-client", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "action": "action", + "actionType": "msg", + "host": "host", + "hostType": "msg", + "port": "port", + "portType": "msg", + "datamode": "stream", + "datatype": "buffer", + "newline": "", + "write": "", + "writeType": "str", + "maxRetries": "5", + "retryDelay": "3000", + "indefiniteRetries": true, + "topic": "basculaTCP1", + "name": "BASCULATCP1", + "debug": "info", + "x": 380, + "y": 220, + "wires": [ + [ + "8a0cc762a8039217", + "4d122b4d7ef323dd" + ] + ] + }, + { + "id": "8a0cc762a8039217", + "type": "change", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "TCPBascula1", + "rules": [ + { + "t": "set", + "p": "topic", + "pt": "msg", + "to": "TCP", + "tot": "str" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 600, + "y": 240, + "wires": [ + [ + "47f84415577e771f" + ] + ] + }, + { + "id": "bdad189ae1181c82", + "type": "change", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "RS-232Bascula1", + "rules": [ + { + "t": "set", + "p": "topic", + "pt": "msg", + "to": "RS-232", + "tot": "str" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 550, + "y": 340, + "wires": [ + [ + "4c97abf1728d0d77" + ] + ] + }, + { + "id": "f0bede4dc0ffef68", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "TRAMA Bascula1", + "func": "var payload = msg.payload; // Obtener los datos del mensaje\nvar buffer = Buffer.from(payload); // Convertir a Buffer para manipulación de bytes\nconst stx = flow.get(\"InicioTramaBascula1\"); // Suponiendo que STX es 0x02 (ajusta según tu protocolo)\nconst etx = flow.get(\"FinalTramaBascula1\"); // Suponiendo que ETX es 0x03 (ajusta según tu protocolo)\nvar frameStart = 0;\nvar frames = [];\nvar remainingData = flow.get(\"remainingData1\") || Buffer.alloc(0); // Inicializar o usar el buffer restante de la llamada anterior\n\n// Concatenar los datos restantes de la llamada anterior con los nuevos datos\nbuffer = Buffer.concat([remainingData, buffer]);\n\nfor (var i = 0; i < buffer.length; i++) {\n if (buffer[i] === stx) {\n frameStart = i;\n } else if (buffer[i] === etx) {\n frames.push(buffer.slice(frameStart, i + 1));\n buffer = buffer.slice(i + 1); // Eliminar la trama procesada\n i = -1; // Reiniciar el índice para el siguiente bucle\n }\n}\n\n// Si queda un fragmento de trama al final, almacenarlo para la próxima llamada\nflow.set(\"remainingData1\", buffer);\nif(frames.length != 0){\n msg.payload = frames;\n node.send(msg)\n}\n\nreturn ;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1030, + "y": 300, + "wires": [ + [ + "a65499ac760347b5" + ] + ] + }, + { + "id": "a65499ac760347b5", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "PESO Bascula1", + "func": "var payload = msg.payload;\nconst LongitudTrama = flow.get(\"LongitudTramaBascula1\");\nconst InicioPeso = flow.get(\"InicioPesoBascula1\");\nconst LongitudPeso = flow.get(\"LongitudPesoBascula1\");\nlet tramaString = '';\nvar processedFrames = [];\n\n// Obtener el temporizador previamente almacenado\nlet timeoutId = flow.get(\"timeoutIdBascula1\") || null;\nconst timeoutDuration = 5000; // Tiempo de espera en milisegundos\n\n// Limpiar el temporizador anterior si existe\nif (timeoutId) {\n clearTimeout(timeoutId);\n}\n\n// Establecer un nuevo temporizador para enviar el mensaje vacío si no hay actividad\ntimeoutId = setTimeout(() => {\n node.warn(\"No se han recibido datos en los últimos 5 segundos.\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': '',\n 'mensaje': 'No se han recibido datos en los últimos 5 segundos'\n };\n node.send(msg);\n}, timeoutDuration);\n\n// Guardar el nuevo temporizador en el contexto de flujo para futuras referencias\nflow.set(\"timeoutIdBascula1\", timeoutId);\n\nfor (var i = 0; i < payload.length; i++) {\n var frame = payload[i];\n tramaString += payload[i].toString();\n\n if (frame.length === LongitudTrama) {\n // Extraer el valor del peso y convertirlo a string\n var pesoBuffer = frame.slice(InicioPeso, InicioPeso + LongitudPeso);\n var pesoString = pesoBuffer.toString('utf8'); // Asumiendo que el peso es un string UTF-8\n\n // Agregar la trama procesada al array de salida\n processedFrames.push({\n trama: frame,\n peso: pesoString,\n pesoBuffer: pesoBuffer\n });\n\n // Enviar el mensaje con datos válidos\n msg.payload = {\n 'peso': parseInt(pesoString),\n 'pesoString': pesoString,\n 'trama': tramaString,\n 'mensaje': ''\n };\n flow.set(\"peso1\",pesoString)\n node.send(msg);\n\n // Reiniciar el temporizador al recibir datos válidos\n clearTimeout(timeoutId);\n timeoutId = setTimeout(() => {\n //node.warn(\"No se han recibido datos en los últimos 5 segundos.\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': '',\n 'mensaje': 'No se han recibido datos en los últimos 5 segundos'\n };\n // node.send(msg);\n let msg_close = {action:\"close\", host:flow.get(\"hostTCPBascula1\"), port:flow.get(\"puertoTCPBascula1\")}\n node.send([msg, msg_close]);\n }, timeoutDuration);\n flow.set(\"timeoutIdBascula1\", timeoutId);\n } else {\n // Manejamos la trama inválida\n // node.warn(\"Trama inválida\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': tramaString,\n // 'mensaje': 'TRAMA INVALIDA'\n };\n // node.send(msg);\n }\n}\n", + "outputs": 2, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1260, + "y": 300, + "wires": [ + [ + "2d26d00c4530ee1f" + ], + [ + "9dbc4cf2cb9ad616" + ] + ] + }, + { + "id": "47f84415577e771f", + "type": "switch", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "TCPBascula1", + "property": "tcp_flag_Bascula1", + "propertyType": "flow", + "rules": [ + { + "t": "true" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 800, + "y": 240, + "wires": [ + [ + "f0bede4dc0ffef68" + ] + ] + }, + { + "id": "4c97abf1728d0d77", + "type": "switch", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "RS-232 Bascula1", + "property": "tcp_flag_Bascula1", + "propertyType": "flow", + "rules": [ + { + "t": "false" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 770, + "y": 340, + "wires": [ + [ + "f0bede4dc0ffef68" + ] + ] + }, + { + "id": "760d3fa03416427f", + "type": "ui_text", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "group": "cff29c99ac572574", + "order": 1, + "width": "7", + "height": "2", + "name": "Bascula1", + "label": "", + "format": "{{msg.payload.bascula}}", + "layout": "row-left", + "className": "", + "x": 1920, + "y": 340, + "wires": [] + }, + { + "id": "5191ba50d8f57c1f", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "Enviar Datos", + "props": [ + { + "p": "host", + "v": "hostTCPBascula1", + "vt": "flow" + }, + { + "p": "port", + "v": "puertoTCPBascula1", + "vt": "flow" + }, + { + "p": "action", + "v": "connect", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "3", + "topic": "", + "x": 170, + "y": 220, + "wires": [ + [ + "7a6fc5c1767b38b0" + ] + ] + }, + { + "id": "aacff5572bb6eb47", + "type": "serial control", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "", + "serial": "f928d3e1f28c1f5b", + "x": 370, + "y": 400, + "wires": [ + [] + ] + }, + { + "id": "69b2b79597b10613", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "Enviar Datos", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "3", + "topic": "", + "payload": "{ \"serialport\": puertoSerie1, \"serialbaud\": velocidadBaudiosSerie1, \"databits\": databitsSerie1, \"parity\": paridadSerie1, \"stopbits\": stopbitsSerie1, \"enabled\": puertoHabilitadoSerie1 }", + "payloadType": "str", + "x": 190, + "y": 400, + "wires": [ + [ + "aacff5572bb6eb47" + ] + ] + }, + { + "id": "81d1e1eceb078857", + "type": "serial in", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "", + "serial": "f928d3e1f28c1f5b", + "x": 370, + "y": 340, + "wires": [ + [ + "bdad189ae1181c82" + ] + ] + }, + { + "id": "b3bf54cc42094984", + "type": "link in", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "tcp1close", + "links": [ + "9dbc4cf2cb9ad616" + ], + "x": 105, + "y": 320, + "wires": [ + [ + "5704a222c2e6219e" + ] + ] + }, + { + "id": "9dbc4cf2cb9ad616", + "type": "link out", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "tcp1close_out", + "mode": "link", + "links": [ + "b3bf54cc42094984" + ], + "x": 1375, + "y": 340, + "wires": [] + }, + { + "id": "5704a222c2e6219e", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "function 1", + "func": "/*msg.action = close\nmsg.host = flow.get(\"hostTCPBascula1\")\nmsg.port = flow.get(\"puertoTCPBascula1\")\n*/\nnode.send(msg)\nsetTimeout(() => {\n let msg_close = {action:\"connect\", host:flow.get(\"hostTCPBascula1\"), port:flow.get(\"puertoTCPBascula1\")}\n node.send(msg_close);\n }, 2000);\nreturn;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 180, + "y": 260, + "wires": [ + [ + "7a6fc5c1767b38b0" + ] + ] + }, + { + "id": "4d122b4d7ef323dd", + "type": "link out", + "z": "3fbc028716abf7a9", + "g": "3c69eb8420f499d7", + "name": "bascula1", + "mode": "link", + "links": [ + "9667a055f9c967a7" + ], + "x": 525, + "y": 180, + "wires": [] + }, + { + "id": "c03cc7b1afedcdad", + "type": "ui_template", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "group": "cff29c99ac572574", + "name": "PesoBascula2", + "order": 5, + "width": "7", + "height": "2", + "format": "\n\n\n\n\t
\n\t \n \t\t

\n\t\t\t{{msg.payload.pesoString}}

{{msg.payload.mensaje}}
\n\t\t

\n\t
\n", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 1940, + "y": 600, + "wires": [ + [] + ] + }, + { + "id": "5aa551b29b8cdca3", + "type": "ui_text", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "group": "cff29c99ac572574", + "order": 2, + "width": "7", + "height": "2", + "name": "Bascula2", + "label": "", + "format": "{{msg.payload.bascula}}", + "layout": "row-left", + "className": "", + "x": 1920, + "y": 640, + "wires": [] + }, + { + "id": "a979e217b6375370", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "envio WEBSOCKET, TRAMA,PESO Bascula2", + "func": "// ======= Parámetros =======\nconst muestras = flow.get(\"muestras\") || 5;\nconst escalon = flow.get(\"escalon\") || 20;\nconst minPesoVal = flow.get(\"minPesoVal\") || 0;\nconst TIMEOUT_MS = flow.get(\"pesoTimeoutMs\") || 5000;\n\n// ======= Entradas =======\nlet bascula = flow.get(\"nombreBascula2\") || \"Bascula2\";\nlet payloadIn = msg.payload || {};\nlet comunicacion = msg.topic;\nlet trama = payloadIn.trama;\nlet mensaje = payloadIn.mensaje;\n\n// ======= Timeout por báscula =======\nconst keyTimer = `b2_${bascula}_timer`;\nlet t = context.get(keyTimer);\nif (t) clearTimeout(t);\nt = setTimeout(() => {\n const aviso = {\n payload: {\n bascula,\n peso: null,\n pesoString: \"\",\n peso_estable: null,\n estable: false,\n comunicacion,\n trama: \"\",\n mensaje: \"No se han recibido datos en los últimos 5 segundos\",\n statuscolor: \"yellow\"\n }\n };\n flow.set(\"peso2\",\"---\")\n node.send([aviso, null]);\n}, TIMEOUT_MS);\ncontext.set(keyTimer, t);\n\n// ======= Normaliza el peso =======\nconst tienePeso = Object.prototype.hasOwnProperty.call(payloadIn, \"peso\");\nconst rawPeso = payloadIn.peso;\nconst vacio = (rawPeso === \"\" || rawPeso === null || typeof rawPeso === \"undefined\");\nconst pesoNum = Number(rawPeso);\nconst esNumero = Number.isFinite(pesoNum);\n\n// ======= Caso SIN lectura válida -> mensaje, NO 0 =======\nif (!tienePeso || vacio || !esNumero || pesoNum < minPesoVal) {\n const aviso = {\n payload: {\n bascula,\n peso: null,\n pesoString: \"\",\n peso_estable: null,\n estable: false,\n comunicacion,\n trama: trama || \"\",\n mensaje: (typeof mensaje === \"string\" ? mensaje : \"NO LLEGA PESO\"),\n statuscolor: \"yellow\"\n }\n };\n flow.set(\"peso2\",\"---\")\n return [aviso, null];\n}\n\n// ======= Lectura válida =======\nlet peso = pesoNum;\nlet pesoString = String(peso);\nlet colorStatus = peso <= 500 ? \"lime\" : \"yellow\";\n\n// ======= Estado por báscula (claves únicas) =======\nconst keyBase = `b2_${bascula}`;\nconst keyUltimo = `${keyBase}_ultimoPeso`;\nconst keyUltimoEst = `${keyBase}_ultimoPesoEstable`;\nconst keyBuffer = `${keyBase}_buffer`;\n\n// ======= Buffer circular de lecturas (solo válidas) =======\nlet buffer = flow.get(keyBuffer) || [];\nbuffer.unshift(peso);\nif (buffer.length > muestras) buffer.pop();\nflow.set(keyBuffer, buffer);\n\n// ======= Evaluación de estabilidad =======\nlet esEstable = buffer.length === muestras &&\n buffer.slice(1).every(v => Math.abs(buffer[0] - v) <= escalon);\n\nlet pesoEstable = esEstable ? buffer[0] : null;\n\n// ======= Control de repeticiones =======\nlet ultimoEst = flow.get(keyUltimoEst);\nif (esEstable && ultimoEst !== pesoEstable) {\n flow.set(keyUltimoEst, pesoEstable);\n}\n\n// ======= Salida =======\nflow.set(keyUltimo, peso);\n\nreturn [{\n payload: {\n bascula,\n peso,\n pesoString,\n peso_estable: pesoEstable,\n estable: esEstable,\n comunicacion,\n trama,\n mensaje: (typeof mensaje === \"string\" ? mensaje : \"\"),\n statuscolor: colorStatus\n }\n}, null];\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1520, + "y": 620, + "wires": [ + [ + "5aa551b29b8cdca3", + "c03cc7b1afedcdad", + "969714a9a2b1ef72" + ] + ] + }, + { + "id": "745217e56d674e38", + "type": "tcp-client", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "action": "action", + "actionType": "msg", + "host": "host", + "hostType": "msg", + "port": "port", + "portType": "msg", + "datamode": "stream", + "datatype": "buffer", + "newline": "", + "write": "", + "writeType": "str", + "maxRetries": "5", + "retryDelay": "2000", + "indefiniteRetries": true, + "topic": "basculaTCP2", + "name": "BASCULATCP2", + "debug": "error", + "x": 380, + "y": 560, + "wires": [ + [ + "af2b7b939331dbb9", + "e59e292827957f03" + ] + ] + }, + { + "id": "c90a867aaba4b730", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "Enviar Datos", + "props": [ + { + "p": "host", + "v": "hostTCPBascula2", + "vt": "flow" + }, + { + "p": "port", + "v": "puertoTCPBascula2", + "vt": "flow" + }, + { + "p": "action", + "v": "connect", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "3", + "topic": "", + "x": 170, + "y": 580, + "wires": [ + [ + "745217e56d674e38" + ] + ] + }, + { + "id": "af2b7b939331dbb9", + "type": "change", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "TCPBascula2", + "rules": [ + { + "t": "set", + "p": "topic", + "pt": "msg", + "to": "TCP", + "tot": "str" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 600, + "y": 580, + "wires": [ + [ + "69c5f45de39463f0" + ] + ] + }, + { + "id": "c9a6ce7c276cd0cf", + "type": "change", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "RS-232Bascula2", + "rules": [ + { + "t": "set", + "p": "topic", + "pt": "msg", + "to": "RS-232", + "tot": "str" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 590, + "y": 680, + "wires": [ + [ + "91bdf33233cd6dc8" + ] + ] + }, + { + "id": "e45740d360367781", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "TRAMA Bascula2", + "func": "var payload = msg.payload; // Obtener los datos del mensaje\nvar buffer = Buffer.from(payload); // Convertir a Buffer para manipulación de bytes\nconst stx = flow.get(\"InicioTramaBascula2\"); // Suponiendo que STX es 0x02 (ajusta según tu protocolo)\nconst etx = flow.get(\"FinalTramaBascula2\"); // Suponiendo que ETX es 0x03 (ajusta según tu protocolo)\nvar frameStart = 0;\nvar frames = [];\nvar remainingData = flow.get(\"remainingData2\") || Buffer.alloc(0); // Inicializar o usar el buffer restante de la llamada anterior\n\n// Concatenar los datos restantes de la llamada anterior con los nuevos datos\nbuffer = Buffer.concat([remainingData, buffer]);\n\nfor (var i = 0; i < buffer.length; i++) {\n if (buffer[i] === stx) {\n frameStart = i;\n } else if (buffer[i] === etx) {\n frames.push(buffer.slice(frameStart, i + 1));\n buffer = buffer.slice(i + 1); // Eliminar la trama procesada\n i = -1; // Reiniciar el índice para el siguiente bucle\n }\n}\n\n// Si queda un fragmento de trama al final, almacenarlo para la próxima llamada\nflow.set(\"remainingData2\", buffer);\nif(frames.length != 0){\n msg.payload = frames;\n node.send(msg)\n}\n\nreturn ;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1010, + "y": 620, + "wires": [ + [ + "92f2a1b83feac9b1" + ] + ] + }, + { + "id": "92f2a1b83feac9b1", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "PESOBascula2", + "func": "var payload = msg.payload;\nconst LongitudTrama = flow.get(\"LongitudTramaBascula2\");\nconst InicioPeso = flow.get(\"InicioPesoBascula2\");\nconst LongitudPeso = flow.get(\"LongitudPesoBascula2\");\nlet tramaString = '';\nvar processedFrames = [];\n\n// Obtener el temporizador previamente almacenado\nlet timeoutId = flow.get(\"timeoutIdBascula2\") || null;\nconst timeoutDuration = 5000; // Tiempo de espera en milisegundos\n\n// Limpiar el temporizador anterior si existe\nif (timeoutId) {\n clearTimeout(timeoutId);\n}\n\n// Establecer un nuevo temporizador para enviar el mensaje vacío si no hay actividad\ntimeoutId = setTimeout(() => {\n node.warn(\"No se han recibido datos en los últimos 5 segundos.\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': '',\n 'mensaje': 'No se han recibido datos en los últimos 5 segundos RS232'\n };\n node.send(msg);\n}, timeoutDuration);\n\n// Guardar el nuevo temporizador en el contexto de flujo para futuras referencias\nflow.set(\"timeoutIdBascula2\", timeoutId);\n\nfor (var i = 0; i < payload.length; i++) {\n var frame = payload[i];\n tramaString += payload[i].toString();\n\n if (frame.length === LongitudTrama) {\n // Extraer el valor del peso y convertirlo a string\n var pesoBuffer = frame.slice(InicioPeso, InicioPeso + LongitudPeso);\n var pesoString = pesoBuffer.toString('utf8'); // Asumiendo que el peso es un string UTF-8\n\n // Agregar la trama procesada al array de salida\n processedFrames.push({\n trama: frame,\n peso: pesoString,\n pesoBuffer: pesoBuffer\n });\n\n // Enviar el mensaje con datos válidos\n msg.payload = {\n 'peso': parseInt(pesoString),\n 'pesoString': pesoString,\n 'trama': tramaString,\n 'mensaje': ''\n };\n flow.set(\"peso2\",pesoString)\n node.send(msg);\n\n // Reiniciar el temporizador al recibir datos válidos\n clearTimeout(timeoutId);\n timeoutId = setTimeout(() => {\n //node.warn(\"No se han recibido datos en los últimos 5 segundos.\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': '',\n 'mensaje': 'No se han recibido datos en los últimos 5 segundos RED'\n };\n // node.send(msg);\n let msg_close = {action:\"close\", host:flow.get(\"hostTCPBascula2\"), port:flow.get(\"puertoTCPBascula2\")}\n node.send([msg, msg_close]);\n }, timeoutDuration);\n flow.set(\"timeoutIdBascula2\", timeoutId);\n } else {\n // Manejamos la trama inválida\n // node.warn(\"Trama inválida\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': tramaString,\n // 'mensaje': 'TRAMA INVALIDA'\n };\n // node.send(msg);\n }\n}\n", + "outputs": 2, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1220, + "y": 620, + "wires": [ + [ + "a979e217b6375370" + ], + [ + "fa67b799f5270de3" + ] + ] + }, + { + "id": "69c5f45de39463f0", + "type": "switch", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "TCPBascula2", + "property": "tcp_flag_Bascula2", + "propertyType": "flow", + "rules": [ + { + "t": "true" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 800, + "y": 580, + "wires": [ + [ + "e45740d360367781" + ] + ] + }, + { + "id": "91bdf33233cd6dc8", + "type": "switch", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "RS-232 Bascula2", + "property": "tcp_flag_Bascula2", + "propertyType": "flow", + "rules": [ + { + "t": "false" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 810, + "y": 680, + "wires": [ + [ + "e45740d360367781" + ] + ] + }, + { + "id": "751f20d5500f0560", + "type": "serial control", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "", + "serial": "f928d3e1f28c1f5b", + "x": 390, + "y": 740, + "wires": [ + [] + ] + }, + { + "id": "662de6fd08e45fb2", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "Enviar Datos", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "3", + "topic": "", + "payload": "{ \"serialport\": puertoSerie3, \"serialbaud\": velocidadBaudiosSerie2, \"databits\": databitsSerie2, \"parity\": paridadSerie2, \"stopbits\": stopbitsSerie2, \"enabled\": puertoHabilitadoSerie2 }", + "payloadType": "str", + "x": 170, + "y": 740, + "wires": [ + [ + "751f20d5500f0560" + ] + ] + }, + { + "id": "c7ac3675f9f951fe", + "type": "serial in", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "", + "serial": "f928d3e1f28c1f5b", + "x": 390, + "y": 680, + "wires": [ + [ + "c9a6ce7c276cd0cf" + ] + ] + }, + { + "id": "fa67b799f5270de3", + "type": "link out", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "tcp2close_out", + "mode": "link", + "links": [ + "4c71a5b6fc6bd505" + ], + "x": 1325, + "y": 660, + "wires": [] + }, + { + "id": "e59e292827957f03", + "type": "link out", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "bascula1", + "mode": "link", + "links": [ + "9667a055f9c967a7" + ], + "x": 545, + "y": 500, + "wires": [] + }, + { + "id": "a567f5be5995d73c", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "function 2", + "func": "/*msg.action = close\nmsg.host = flow.get(\"hostTCPBascula1\")\nmsg.port = flow.get(\"puertoTCPBascula1\")\n*/\nnode.send(msg)\nsetTimeout(() => {\n let msg_close = {action:\"connect\", host:flow.get(\"hostTCPBascula2\"), port:flow.get(\"puertoTCPBascula2\")}\n node.send(msg_close);\n }, 2000);\nreturn;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 380, + "y": 620, + "wires": [ + [ + "745217e56d674e38" + ] + ] + }, + { + "id": "4c71a5b6fc6bd505", + "type": "link in", + "z": "3fbc028716abf7a9", + "g": "414eb1569dd15f60", + "name": "tcp2close", + "links": [ + "fa67b799f5270de3" + ], + "x": 245, + "y": 680, + "wires": [ + [ + "a567f5be5995d73c" + ] + ] + }, + { + "id": "83659a5fa5422105", + "type": "ui_template", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "group": "cff29c99ac572574", + "name": "PesoBascula4", + "order": 9, + "width": "7", + "height": "2", + "format": "\n\n\n\n\t
\n\t \n \t\t

\n\t\t\t{{msg.payload.pesoString}}

{{msg.payload.mensaje}}
\n\t\t

\n\t
", + "storeOutMessages": true, + "fwdInMessages": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 1940, + "y": 1300, + "wires": [ + [] + ] + }, + { + "id": "d03daa5335e2514c", + "type": "ui_text", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "group": "cff29c99ac572574", + "order": 7, + "width": "7", + "height": "2", + "name": "Bascula4", + "label": "", + "format": "{{msg.payload.bascula}}", + "layout": "row-left", + "className": "", + "x": 1920, + "y": 1340, + "wires": [] + }, + { + "id": "5cf1088cefafd3a7", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "envio WEBSOCKET, TRAMA,PESO Bascula4", + "func": "// ======= Parámetros =======\nconst muestras = flow.get(\"muestras\") || 5;\nconst escalon = flow.get(\"escalon\") || 20;\nconst minPesoVal = flow.get(\"minPesoVal\") || 0;\nconst TIMEOUT_MS = flow.get(\"pesoTimeoutMs\") || 5000;\n\n// ======= Entradas =======\nlet bascula = flow.get(\"nombreBascula4\") || \"Bascula4\";\nlet payloadIn = msg.payload || {};\nlet comunicacion = msg.topic;\nlet trama = payloadIn.trama;\nlet mensaje = payloadIn.mensaje;\n\n// ======= Timeout por báscula =======\nconst keyTimer = `b4_${bascula}_timer`;\nlet t = context.get(keyTimer);\nif (t) clearTimeout(t);\nt = setTimeout(() => {\n const aviso = {\n payload: {\n bascula,\n peso: null,\n pesoString: \"\",\n peso_estable: null,\n estable: false,\n comunicacion,\n trama: \"\",\n mensaje: \"No se han recibido datos en los últimos 5 segundos\",\n statuscolor: \"yellow\"\n }\n };\n flow.set(\"peso4\",\"---\")\n node.send([aviso, null]);\n}, TIMEOUT_MS);\ncontext.set(keyTimer, t);\n\n// ======= Normaliza el peso =======\nconst tienePeso = Object.prototype.hasOwnProperty.call(payloadIn, \"peso\");\nconst rawPeso = payloadIn.peso;\nconst vacio = (rawPeso === \"\" || rawPeso === null || typeof rawPeso === \"undefined\");\nconst pesoNum = Number(rawPeso);\nconst esNumero = Number.isFinite(pesoNum);\n\n// ======= Caso SIN lectura válida -> mensaje, NO 0 =======\nif (!tienePeso || vacio || !esNumero || pesoNum < minPesoVal) {\n const aviso = {\n payload: {\n bascula,\n peso: null,\n pesoString: \"\",\n peso_estable: null,\n estable: false,\n comunicacion,\n trama: trama || \"\",\n mensaje: (typeof mensaje === \"string\" ? mensaje : \"NO LLEGA PESO\"),\n statuscolor: \"yellow\"\n }\n };\n flow.set(\"peso4\",\"---\")\n return [aviso, null];\n}\n\n// ======= Lectura válida =======\nlet peso = pesoNum;\nlet pesoString = String(peso);\nlet colorStatus = peso <= 500 ? \"lime\" : \"yellow\";\n\n// ======= Estado por báscula (claves únicas) =======\nconst keyBase = `b4_${bascula}`;\nconst keyUltimo = `${keyBase}_ultimoPeso`;\nconst keyUltimoEst = `${keyBase}_ultimoPesoEstable`;\nconst keyBuffer = `${keyBase}_buffer`;\n\n// ======= Buffer circular de lecturas (solo válidas) =======\nlet buffer = flow.get(keyBuffer) || [];\nbuffer.unshift(peso);\nif (buffer.length > muestras) buffer.pop();\nflow.set(keyBuffer, buffer);\n\n// ======= Evaluación de estabilidad =======\nlet esEstable = buffer.length === muestras &&\n buffer.slice(1).every(v => Math.abs(buffer[0] - v) <= escalon);\n\nlet pesoEstable = esEstable ? buffer[0] : null;\n\n// ======= Control de repeticiones =======\nlet ultimoEst = flow.get(keyUltimoEst);\nif (esEstable && ultimoEst !== pesoEstable) {\n flow.set(keyUltimoEst, pesoEstable);\n}\n\n// ======= Salida =======\nflow.set(keyUltimo, peso);\n\nreturn [{\n payload: {\n bascula,\n peso,\n pesoString,\n peso_estable: pesoEstable,\n estable: esEstable,\n comunicacion,\n trama,\n mensaje: (typeof mensaje === \"string\" ? mensaje : \"\"),\n statuscolor: colorStatus\n }\n}, null];\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1600, + "y": 1320, + "wires": [ + [ + "969714a9a2b1ef72", + "d03daa5335e2514c", + "83659a5fa5422105" + ] + ] + }, + { + "id": "b8228cdac41b95c1", + "type": "tcp-client", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "action": "action", + "actionType": "msg", + "host": "host", + "hostType": "msg", + "port": "port", + "portType": "msg", + "datamode": "stream", + "datatype": "buffer", + "newline": "", + "write": "", + "writeType": "str", + "maxRetries": "", + "retryDelay": "", + "indefiniteRetries": true, + "topic": "basculaTCP4", + "name": "BASCULATCP4", + "debug": "error", + "x": 380, + "y": 1220, + "wires": [ + [ + "d216a46466968eae", + "d6e380c9415bfb72" + ] + ] + }, + { + "id": "5d3d7b9e427ebd33", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "Enviar Datos", + "props": [ + { + "p": "host", + "v": "hostTCPBascula4", + "vt": "flow" + }, + { + "p": "port", + "v": "puertoTCPBascula4", + "vt": "flow" + }, + { + "p": "action", + "v": "connect", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "2", + "topic": "", + "x": 170, + "y": 1220, + "wires": [ + [ + "b8228cdac41b95c1" + ] + ] + }, + { + "id": "d216a46466968eae", + "type": "change", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "TCPBascula4", + "rules": [ + { + "t": "set", + "p": "topic", + "pt": "msg", + "to": "TCP", + "tot": "str" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 600, + "y": 1220, + "wires": [ + [ + "061c9a2b5eb5e8b5" + ] + ] + }, + { + "id": "b0ed51791aa894e6", + "type": "change", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "RS-232Bascula4", + "rules": [ + { + "t": "set", + "p": "topic", + "pt": "msg", + "to": "RS-232", + "tot": "str" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 590, + "y": 1320, + "wires": [ + [ + "6d6370556efac085" + ] + ] + }, + { + "id": "631799398328acdf", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "TRAMABascula4", + "func": "\nvar payload = msg.payload; // Obtener los datos del mensaje\nvar buffer4 = Buffer.from(payload); // Convertir a Buffer para manipulación de bytes\nconst stx = flow.get(\"InicioTramaBascula4\"); // Suponiendo que STX es 0x02 (ajusta según tu protocolo)\nconst etx = flow.get(\"FinalTramaBascula4\"); // Suponiendo que ETX es 0x03 (ajusta según tu protocolo)\nvar frameStart = 0;\nvar frames = [];\nvar remainingData4 = flow.get(\"remainingData4\") || Buffer.alloc(0); // Inicializar o usar el buffer restante de la llamada anterior\n\n// Concatenar los datos restantes de la llamada anterior con los nuevos datos\nbuffer4 = Buffer.concat([remainingData4, buffer4]);\n\nfor (var i = 0; i < buffer4.length; i++) {\n if (buffer4[i] === stx) {\n frameStart = i;\n } else if (buffer4[i] === etx) {\n frames.push(buffer4.slice(frameStart, i + 1));\n buffer4 = buffer4.slice(i + 1); // Eliminar la trama procesada\n i = -1; // Reiniciar el índice para el siguiente bucle\n }\n}\n\n// Si queda un fragmento de trama al final, almacenarlo para la próxima llamada\nflow.set(\"remainingData4\", buffer4);\nif(frames.length != 0){\n msg.payload = frames;\n node.send(msg)\n\n}\nreturn ;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1070, + "y": 1320, + "wires": [ + [ + "c5d5fe49f9134079" + ] + ] + }, + { + "id": "c5d5fe49f9134079", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "PESOBascula4", + "func": "var payload = msg.payload;\nconst LongitudTrama = flow.get(\"LongitudTramaBascula4\");\nconst InicioPeso = flow.get(\"InicioPesoBascula4\");\nconst LongitudPeso = flow.get(\"LongitudPesoBascula4\");\nlet tramaString = '';\nvar processedFrames = [];\n\n// Obtener el temporizador previamente almacenado\nlet timeoutId = flow.get(\"timeoutIdBascula4\") || null;\nconst timeoutDuration = 5000; // Tiempo de espera en milisegundos\n\n// Limpiar el temporizador anterior si existe\nif (timeoutId) {\n clearTimeout(timeoutId);\n}\n\n// Establecer un nuevo temporizador para enviar el mensaje vacío si no hay actividad\ntimeoutId = setTimeout(() => {\n node.warn(\"No se han recibido datos en los últimos 5 segundos.\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': '',\n 'mensaje': 'No se han recibido datos en los últimos 5 segundos'\n };\n node.send(msg);\n}, timeoutDuration);\n\n// Guardar el nuevo temporizador en el contexto de flujo para futuras referencias\nflow.set(\"timeoutIdBascula4\", timeoutId);\n\nfor (var i = 0; i < payload.length; i++) {\n var frame = payload[i];\n tramaString += payload[i].toString();\n\n if (frame.length === LongitudTrama) {\n // Extraer el valor del peso y convertirlo a string\n var pesoBuffer = frame.slice(InicioPeso, InicioPeso + LongitudPeso);\n var pesoString = pesoBuffer.toString('utf8'); // Asumiendo que el peso es un string UTF-8\n\n // Agregar la trama procesada al array de salida\n processedFrames.push({\n trama: frame,\n peso: pesoString,\n pesoBuffer: pesoBuffer\n });\n\n // Enviar el mensaje con datos válidos\n msg.payload = {\n 'peso': parseInt(pesoString),\n 'pesoString': pesoString,\n 'trama': tramaString,\n 'mensaje': ''\n };\n flow.set(\"peso4\",pesoString)\n node.send(msg);\n\n // Reiniciar el temporizador al recibir datos válidos\n clearTimeout(timeoutId);\n timeoutId = setTimeout(() => {\n node.warn(\"No se han recibido datos en los últimos 5 segundos.\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': '',\n 'mensaje': 'No se han recibido datos en los últimos 5 segundos'\n };\n //node.send(msg);\n let msg_close = {action:\"close\", host:flow.get(\"hostTCPBascula4\"), port:flow.get(\"puertoTCPBascula4\")}\n node.send([msg, msg_close]);\n }, timeoutDuration);\n flow.set(\"timeoutIdBascula4\", timeoutId);\n } else {\n // Manejamos la trama inválida\n // node.warn(\"Trama inválida\");\n msg.payload = {\n 'peso': '',\n 'pesoString': '',\n 'trama': tramaString,\n 'mensaje': 'TRAMA INVALIDA'\n };\n node.send(msg);\n }\n}\n", + "outputs": 2, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1280, + "y": 1320, + "wires": [ + [ + "5cf1088cefafd3a7" + ], + [ + "55d810bcd402f1c0" + ] + ] + }, + { + "id": "061c9a2b5eb5e8b5", + "type": "switch", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "TCPBascula4", + "property": "tcp_flag_Bascula4", + "propertyType": "flow", + "rules": [ + { + "t": "true" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 800, + "y": 1220, + "wires": [ + [ + "631799398328acdf" + ] + ] + }, + { + "id": "6d6370556efac085", + "type": "switch", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "RS-232 Bascula4", + "property": "tcp_flag_Bascula4", + "propertyType": "flow", + "rules": [ + { + "t": "false" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 810, + "y": 1320, + "wires": [ + [ + "631799398328acdf" + ] + ] + }, + { + "id": "6680d1b3b7ad8918", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "Enviar Datos", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "3", + "topic": "", + "payload": "{ \"serialport\": puertoSerie4, \"serialbaud\": velocidadBaudiosSerie4, \"databits\": databitsSerie4, \"parity\": paridadSerie4, \"stopbits\": stopbitsSerie4, \"enabled\": puertoHabilitadoSerie4 }", + "payloadType": "str", + "x": 190, + "y": 1380, + "wires": [ + [ + "63c34626c74e9d80" + ] + ] + }, + { + "id": "63c34626c74e9d80", + "type": "serial control", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "", + "serial": "f928d3e1f28c1f5b", + "x": 370, + "y": 1380, + "wires": [ + [] + ] + }, + { + "id": "12fbd1e2e0da99f5", + "type": "serial in", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "", + "serial": "f928d3e1f28c1f5b", + "x": 370, + "y": 1320, + "wires": [ + [ + "b0ed51791aa894e6" + ] + ] + }, + { + "id": "55d810bcd402f1c0", + "type": "link out", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "tcp4close_out", + "mode": "link", + "links": [ + "a1792e1b8fb6e6be" + ], + "x": 1425, + "y": 1380, + "wires": [] + }, + { + "id": "d6e380c9415bfb72", + "type": "link out", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "bascula1", + "mode": "link", + "links": [ + "9667a055f9c967a7" + ], + "x": 565, + "y": 1180, + "wires": [] + }, + { + "id": "98c9a64ec995f9d2", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "function 4", + "func": "/*msg.action = close\nmsg.host = flow.get(\"hostTCPBascula1\")\nmsg.port = flow.get(\"puertoTCPBascula1\")\n*/\nnode.send(msg)\nsetTimeout(() => {\n let msg_close = {action:\"connect\", host:flow.get(\"hostTCPBascula4\"), port:flow.get(\"puertoTCPBascula4\")}\n node.send(msg_close);\n }, 2000);\nreturn;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 360, + "y": 1280, + "wires": [ + [ + "b8228cdac41b95c1" + ] + ] + }, + { + "id": "a1792e1b8fb6e6be", + "type": "link in", + "z": "3fbc028716abf7a9", + "g": "21cb55110b1bae57", + "name": "tcp4close", + "links": [ + "55d810bcd402f1c0" + ], + "x": 235, + "y": 1320, + "wires": [ + [ + "98c9a64ec995f9d2" + ] + ] + }, + { + "id": "2ec53aa1784087c1", + "type": "uibuilder", + "z": "3fbc028716abf7a9", + "g": "e3bc37824e09b774", + "name": "", + "topic": "", + "url": "totem", + "okToGo": true, + "fwdInMessages": false, + "allowScripts": false, + "allowStyles": false, + "copyIndex": true, + "templateFolder": "blank", + "extTemplate": "", + "showfolder": false, + "reload": true, + "sourceFolder": "src", + "deployedVersion": "7.4.3", + "showMsgUib": false, + "title": "", + "descr": "", + "editurl": "vscode://file/home/trypton/.node-red/projects/raspipeso20_10_25/uibuilder/totem/?windowId=_blank", + "x": 2540, + "y": 960, + "wires": [ + [ + "be843dd4381d45d5" + ], + [] + ] + }, + { + "id": "b8fdc502b1bf35b7", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "e3bc37824e09b774", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{\"payload\":{\"pantalla\":\"menu-principal\"}}", + "payloadType": "json", + "x": 2350, + "y": 900, + "wires": [ + [ + "2ec53aa1784087c1" + ] + ] + }, + { + "id": "be843dd4381d45d5", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "e3bc37824e09b774", + "name": "buttons \\n decoder", + "func": "// === BOTONES / UIBUILDER ===\nif (msg.payload && msg.payload.hasOwnProperty(\"seccion\")) {\n\n switch (msg.payload.seccion) {\n\n // ──────────────────────────────────────────────────────────────\n // MENÚ PRINCIPAL (selección de báscula)\n // ──────────────────────────────────────────────────────────────\n case \"menu-principal1\":\n flow.set(\"bascula\", \"Bascula1\");\n node.warn(\"Bascula 1 \" + flow.get(\"bascula\"));\n return ([{ payload: { pantalla: \"menu-principal\" }, topic: \"bascula1\" }, null, null]);\n\n case \"menu-principal2\":\n flow.set(\"bascula\", \"Bascula2\");\n node.warn(\"Bascula 2 \" + flow.get(\"bascula\"));\n return ([{ payload: { pantalla: \"menu-principal\" }, topic: \"bascula2\" }, null, null]);\n\n case \"menu-principal3\":\n flow.set(\"bascula\", \"Bascula3\");\n node.warn(\"Bascula 3 \" + flow.get(\"bascula\"));\n return ([{ payload: { pantalla: \"menu-principal\" }, topic: \"bascula3\" }, null, null]);\n\n case \"menu-principal4\":\n flow.set(\"bascula\", \"Bascula4\");\n node.warn(\"Bascula 4 \" + flow.get(\"bascula\"));\n return ([{ payload: { pantalla: \"menu-principal\" }, topic: \"bascula4\" }, null, null]);\n\n // ──────────────────────────────────────────────────────────────\n // NAVEGACIÓN BÁSICA\n // ──────────────────────────────────────────────────────────────\n case \"editar\":\n return { payload: { pantalla: \"editar\" } };\n\n case \"otras\":\n return { payload: { pantalla: \"otras\" } };\n\n case \"regresar\":\n return { payload: { pantalla: \"menu-principal\" } };\n\n case \"salvar\":\n return ([{\n payload: { pantalla: \"editar\" },\n modalText: \"Desea MODIFICAR el archivo de CONFIGURACION ?\",\n modalCallback: \"modificar\"\n }, null, null, null]);\n\n case \"cancelar\":\n return ([{ payload: { pantalla: \"menu-principal\" } }, null]);\n\n // ──────────────────────────────────────────────────────────────\n // TABLAS: GENERAL (sin filtro)\n // ──────────────────────────────────────────────────────────────\n case \"general\":\n return ([\n { payload: { pantalla: \"tabla-general\" }, topic: \"general\", tabla: flow.get(\"config_general\") },\n null, null, null, null, null\n ]);\n\n // ──────────────────────────────────────────────────────────────\n // TABLAS: PARÁMETROS (mostrar SOLO báscula activa, sin globales)\n // ──────────────────────────────────────────────────────────────\n case \"parametros\": {\n const bascula = flow.get(\"bascula\") || \"Bascula1\"; // p.ej. \"Bascula3\"\n const sufijo = bascula.replace(\"Bascula\",\"\"); // \"3\"\n const isEsta = new RegExp(`Bascula${sufijo}$`, \"i\");\n\n const todos = flow.get(\"config_parametros\") || [];\n\n // Oculta globales y muestra solo claves de la báscula activa\n const ocultar = new Set([\"numerobasculas\",\"numerotrama\"]);\n const tabla = todos.filter(p => {\n if (!p || typeof p.parametro !== \"string\") return false;\n const key = p.parametro.toLowerCase().trim();\n if (ocultar.has(key)) return false;\n return isEsta.test(p.parametro);\n });\n\n // Orden legible (opcional)\n const orden = [\n `nombreBascula${sufijo}`, `tcp_flag_Bascula${sufijo}`,\n `InicioTramaBascula${sufijo}`, `FinalTramaBascula${sufijo}`, `LongitudTramaBascula${sufijo}`,\n `InicioPesoBascula${sufijo}`, `LongitudPesoBascula${sufijo}`,\n `hostTCPBascula${sufijo}`, `puertoTCPBascula${sufijo}`\n ];\n tabla.sort((a,b)=> orden.indexOf(a.parametro) - orden.indexOf(b.parametro));\n\n // Defaults visibles si faltan valores\n tabla.forEach(p => {\n if (p.parametro === `tcp_flag_Bascula${sufijo}` && (p.valor === undefined || p.valor === \"\")) {\n p.valor = false;\n }\n });\n\n return ([\n { payload: { pantalla: \"tabla-general\" }, topic: \"parametros\", tabla },\n null, null, null, null\n ]);\n }\n\n // ──────────────────────────────────────────────────────────────\n // TABLAS: RS232 (mostrar SOLO SerieN de la báscula activa)\n // ──────────────────────────────────────────────────────────────\n case \"rs232\": {\n const bascula = flow.get(\"bascula\") || \"Bascula1\";\n const sufijo = bascula.replace(\"Bascula\",\"\");\n const isEsta = new RegExp(`Serie${sufijo}$`, \"i\");\n\n const todos = flow.get(\"config_rs232\") || [];\n\n const tabla = todos.filter(p => {\n if (!p || typeof p.parametro !== \"string\") return false;\n return isEsta.test(p.parametro);\n });\n\n const orden = [\n `puertoSerie${sufijo}`,\n `velocidadBaudiosSerie${sufijo}`,\n `databitsSerie${sufijo}`,\n `paridadSerie${sufijo}`,\n `stopbitsSerie${sufijo}`,\n `puertoHabilitadoSerie${sufijo}`,\n ];\n tabla.sort((a,b)=> orden.indexOf(a.parametro) - orden.indexOf(b.parametro));\n\n tabla.forEach(p => {\n if (p.parametro === `paridadSerie${sufijo}` && (p.valor === undefined || p.valor === \"\")) p.valor = \"none\";\n if (p.parametro === `puertoHabilitadoSerie${sufijo}` && (p.valor === undefined || p.valor === \"\")) p.valor = 0;\n });\n\n return ([\n { payload: { pantalla: \"tabla-general\" }, topic: \"rs232\", tabla },\n null, null\n ]);\n }\n\n // ──────────────────────────────────────────────────────────────\n // OTRAS ACCIONES\n // ──────────────────────────────────────────────────────────────\n case \"aplicar\":\n return ([{\n payload: { pantalla: \"menu-principal\" },\n topic: \"menu-principal\",\n modalText: \"¿Desea Recargar la CONFIGURACION ?\",\n modalCallback: \"recargar\"\n }, null, null]);\n\n case \"reboot\":\n return ([{\n payload: { pantalla: \"otras\" },\n topic: \"otras\",\n modalText: \"Desea Reiniciar el TOTEM ?\",\n modalCallback: \"reiniciar\"\n }, null, null]);\n\n case \"apagar\":\n return ([{\n payload: { pantalla: \"otras\" },\n topic: \"otras\",\n modalText: \"Desea APAGAR el TOTEM ?\",\n modalCallback: \"apagar\"\n }, null, null]);\n\n case \"cancel-table\":\n return ({ payload: { pantalla: \"editar\" } });\n\n case \"salir\":\n return ({ payload: { pantalla: \"principal\" } });\n\n // ──────────────────────────────────────────────────────────────\n // DEFAULT: GUARDADOS PARCIALES DESDE EL FRONT\n // ──────────────────────────────────────────────────────────────\n default:\n // reenviar a última salida para depuración (opcional)\n node.send([null, null, null, null, null, null, msg]);\n\n // 🧩 ACTUALIZAR CONFIGURACIÓN EN MEMORIA (desde uibuilder)\n if (msg.origen === \"actualizar\") {\n const myconfig = msg.payload.table || [];\n const topic = msg.topic || \"\"; // \"general\" | \"parametros\" | \"rs232\"\n\n switch (topic) {\n // ----- GENERAL -----\n case \"general\":\n flow.set(\"config_general\", myconfig);\n break;\n\n // ----- RS232 (merge por SerieN de la báscula activa) -----\n case \"rs232\": {\n const basculaNombre = flow.get(\"bascula\") || \"Bascula1\";\n const sufijo = basculaNombre.replace(\"Bascula\", \"\"); // \"4\"\n const regexEsta = new RegExp(`Serie${sufijo}$`, \"i\");\n\n const numericKeys = [\n /^velocidadBaudiosSerie\\d+$/i,\n /^databitsSerie\\d+$/i,\n /^stopbitsSerie\\d+$/i,\n /^puertoHabilitadoSerie\\d+$/i,\n ];\n const stringKeys = [\n /^puertoSerie\\d+$/i,\n /^paridadSerie\\d+$/i,\n ];\n\n const normalizado = (myconfig || []).map(it => {\n if (!it || typeof it.parametro !== \"string\") return it;\n\n let p = String(it.parametro).normalize('NFKC').trim();\n if (/Serie\\d+$/i.test(p)) p = p.replace(/Serie\\d+$/i, `Serie${sufijo}`);\n else if (/Serie$/i.test(p)) p = p.replace(/Serie$/i, `Serie${sufijo}`);\n\n let v = (typeof it.valor === 'string') ? it.valor.trim() : it.valor;\n if (numericKeys.some(rx => rx.test(p))) {\n const n = Number(v); if (Number.isFinite(n)) v = n;\n } else if (stringKeys.some(rx => rx.test(p))) {\n v = String(v);\n if (/^paridadSerie\\d+$/i.test(p)) v = v.toLowerCase();\n }\n\n return { parametro: p, valor: v };\n });\n\n const todos = flow.get(\"config_rs232\") || [];\n const resto = todos.filter(p => !(p && typeof p.parametro === \"string\" && regexEsta.test(p.parametro)));\n const fusion = [...resto, ...normalizado];\n\n flow.set(\"config_rs232\", fusion);\n break;\n }\n\n // ----- PARÁMETROS (merge por BasculaN de la báscula activa) -----\n case \"parametros\": {\n const bascula = flow.get(\"bascula\") || \"Bascula1\";\n const sufijo = bascula.replace(\"Bascula\",\"\"); // \"1\"\n const rxEsta = new RegExp(`Bascula${sufijo}$`, \"i\");\n\n const NUMERIC_KEYS = [\n /^InicioTramaBascula\\d+$/i,\n /^FinalTramaBascula\\d+$/i,\n /^LongitudTramaBascula\\d+$/i,\n /^InicioPesoBascula\\d+$/i,\n /^LongitudPesoBascula\\d+$/i,\n /^puertoTCPBascula\\d+$/i,\n /^numeroBasculas$/i,\n /^numeroTrama$/i,\n ];\n const BOOL_KEYS = [\n new RegExp(`^tcp_flag_Bascula${sufijo}$`, 'i'),\n ];\n const STRING_KEYS = [\n new RegExp(`^nombreBascula${sufijo}$`, 'i'),\n new RegExp(`^hostTCPBascula${sufijo}$`, 'i'),\n ];\n\n const normalizado = (myconfig || []).map(it => {\n if (!it || typeof it.parametro !== \"string\") return it;\n\n let p = String(it.parametro).normalize('NFKC').trim();\n let v = (typeof it.valor === 'string') ? it.valor.trim() : it.valor;\n\n if (/Bascula\\d+$/i.test(p)) p = p.replace(/Bascula\\d+$/i, `Bascula${sufijo}`);\n else if (/Bascula$/i.test(p)) p = p.replace(/Bascula$/i, `Bascula${sufijo}`);\n\n if (NUMERIC_KEYS.some(rx => rx.test(p))) {\n const n = Number(v); if (Number.isFinite(n)) v = n;\n } else if (BOOL_KEYS.some(rx => rx.test(p))) {\n if (v === \"true\") v = true;\n else if (v === \"false\") v = false;\n else v = Boolean(v);\n } else if (STRING_KEYS.some(rx => rx.test(p))) {\n v = String(v);\n }\n\n return { parametro: p, valor: v };\n });\n\n const todos = flow.get(\"config_parametros\") || [];\n const resto = todos.filter(p => !(p && typeof p.parametro === \"string\" && rxEsta.test(p.parametro)));\n const fusion = [...resto, ...normalizado];\n flow.set(\"config_parametros\", fusion);\n\n // vuelve directo a la pantalla de edición\n return { payload: { pantalla: \"editar\" } };\n }\n }\n\n // si no era \"parametros\", mantenemos navegación por defecto:\n return { payload: { pantalla: \"editar\" } };\n }\n\n return null;\n }\n}\n\n// === MODALES / ACCIONES CONFIRMADAS ===\nelse if (msg.modalCallback === \"modificar\") {\n // Normalización final antes de guardar a archivo (opcional, pero recomendado)\n function normParam(arr = []) {\n const NUMERIC_PARAM_KEYS = [\n /^InicioTramaBascula\\d+$/i, /^FinalTramaBascula\\d+$/i, /^LongitudTramaBascula\\d+$/i,\n /^InicioPesoBascula\\d+$/i, /^LongitudPesoBascula\\d+$/i, /^puertoTCPBascula\\d+$/i,\n /^numeroBasculas$/i, /^numeroTrama$/i\n ];\n return (arr || []).map(it => {\n if (!it || typeof it.parametro !== 'string') return it;\n const k = String(it.parametro).normalize('NFKC').trim();\n let v = it.valor;\n if (typeof v === 'string') v = v.trim();\n if (NUMERIC_PARAM_KEYS.some(rx => rx.test(k))) {\n const n = Number(v); if (Number.isFinite(n)) v = n;\n }\n return { parametro: k, valor: v };\n });\n }\n function normRS232(arr = []) {\n const NUMERIC_RS232_KEYS = [/^velocidadBaudiosSerie\\d+$/i, /^databitsSerie\\d+$/i, /^stopbitsSerie\\d+$/i, /^puertoHabilitadoSerie\\d+$/i];\n const STRING_RS232_KEYS = [/^puertoSerie\\d+$/i, /^paridadSerie\\d+$/i];\n return (arr || []).map(it => {\n if (!it || typeof it.parametro !== 'string') return it;\n const k = String(it.parametro).normalize('NFKC').trim();\n let v = it.valor;\n if (typeof v === 'string') v = v.trim();\n if (NUMERIC_RS232_KEYS.some(rx => rx.test(k))) {\n const n = Number(v); if (Number.isFinite(n)) v = n;\n } else if (STRING_RS232_KEYS.some(rx => rx.test(k))) {\n v = String(v).toLowerCase();\n }\n return { parametro: k, valor: v };\n });\n }\n\n const config_totem = {\n general: flow.get(\"config_general\") || [],\n parametros: normParam(flow.get(\"config_parametros\") || []),\n rs232: normRS232(flow.get(\"config_rs232\") || [])\n };\n\n flow.set(\"config_totem\", config_totem);\n\n msg.payload = JSON.stringify(config_totem, null, 2) + \"\\n\\r\";\n return ([{ payload: { pantalla: \"menu-principal\" } }, msg, null]);\n}\n\nelse if (msg.modalCallback === \"recargar\") {\n return ([null, null, null, { payload: 1 }]);\n}\n\nelse if (msg.modalCallback === \"reiniciar\") {\n return ([null, null, null, null, { payload: \"sudo shutdown -r now\" }]);\n}\n\nelse if (msg.modalCallback === \"apagar\") {\n return ([null, null, null, null, { payload: \"sudo poweroff\" }]);\n}\n", + "outputs": 5, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2720, + "y": 940, + "wires": [ + [ + "88b9b4d1c5aa7359" + ], + [ + "6d39924d1d2081dd" + ], + [], + [ + "1ba8bfe271072a91" + ], + [ + "ba5e6b2b40fba02b" + ] + ] + }, + { + "id": "88b9b4d1c5aa7359", + "type": "link out", + "z": "3fbc028716abf7a9", + "g": "e3bc37824e09b774", + "name": "feedback", + "mode": "link", + "links": [ + "7058b9e632aa584b" + ], + "x": 2865, + "y": 840, + "wires": [] + }, + { + "id": "7a4c757e6f6ddfcd", + "type": "comment", + "z": "3fbc028716abf7a9", + "g": "e3bc37824e09b774", + "name": "CONFIGURACION DE PARAMETROS Y PRUEBAS ", + "info": "Este grupo de nodos permiten servir una pagina web para que el usuario desde un movil pueda realizar la gestion de configuracion\nde los principales parametros del Totem asi como realizar algunas pruebas en la camara y en el semaforo.\nEl punto central de este grupo es el paquete UIBUILDER para nodered el cual convive sin inconvenientes con el DASHBOARD de nodered.\nEl uibuilder posee un nodo para cada path de acceso que deseemos habilitar. Cada nodo es independiente del otro en el sentido que \ncada uno dispone de un index.html, un index.js y un index.css ue permiten configurar la pagina que sirve el nodo. \nRecordemos que estos archivos se ejecutan en el cliente, por lo tanto para que el index.js tenga acceso a los datos de nodered y nodered \na los datos generados por el cliente, el nodo uibuilder establece una comunicacion websocket transparente para el usuario de manera\na poder intercambiar informacion entre nodered y el browser. Los elementos fundamentales en index.js para llevar a cabo esta comunicacion\nson las funciones \"uibuilder.onChange()\" para recibir los mensajes de nodered y \"uibuilder.send()\" para enviar a nodered los datos generados \npor el cliente.\nEl nodo \"buttons decoder\" fundamentalmente se dedica a interpretar los comandos del cliente (botones que ha pulsado el usuario) de manera a recopilar\nla informacion adecuada para enviar al uibuilder nuevamente de manera que cambie de pagina o muestre ciertos datos, o tambien a ejecutar\nlos comandos solicitados como reboot, apagar dispositivo, encender / apagar semaforo, ...\nEl nodo de entrada \"datos de pesada y snapshot\" recibe por un lado los snapshots enviados por la camara y extrae la informacion necesaria \npara enviar al cliente, y por otro lado siempre que haya alguna recepcion de datos del Totem se preparan los datos correspondientes para \ncolocarlos en la pantalla principal del uibuilder o pantalla de monitoreo.", + "x": 2420, + "y": 1080, + "wires": [] + }, + { + "id": "6d39924d1d2081dd", + "type": "file", + "z": "3fbc028716abf7a9", + "g": "e3bc37824e09b774", + "name": "guardar config test", + "filename": "/home/trypton/.node-red/projects/config_raspeso_test.txt", + "filenameType": "str", + "appendNewline": false, + "createDir": true, + "overwriteFile": "true", + "encoding": "none", + "x": 2950, + "y": 880, + "wires": [ + [] + ] + }, + { + "id": "882ee24d1489b06c", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "e3bc37824e09b774", + "name": "datos de\\n pesada", + "func": "// =============================================================\n// 📦 DATOS DE PESADA Y SNAPSHOT (versión depurada)\n// =============================================================\n\nlet topic = (msg.topic || \"\").toLowerCase();\nlet parametros = flow.get(\"config_parametros\") || [];\nlet rs232 = flow.get(\"config_rs232\") || [];\n\n// =============================================================\n// 🧭 Detección de báscula activa\n// =============================================================\n\nlet bascula = flow.get(\"bascula\")||\"Bascula1\"; // valor por defecto\n\nif (topic.includes(\"bascula1\")) bascula = \"Bascula1\";\nelse if (topic.includes(\"bascula2\")) bascula = \"Bascula2\";\nelse if (topic.includes(\"bascula3\")) bascula = \"Bascula3\";\nelse if (topic.includes(\"bascula4\")) bascula = \"Bascula4\";\n\n// Guardamos siempre la báscula actual\n//flow.set(\"bascula\", bascula);\nnode.status({ fill: \"blue\", shape: \"dot\", text: \"📟 \" + bascula });\n\n// =============================================================\n// ⚙️ Filtrar configuración de la báscula actual\n// =============================================================\nlet sufijo = bascula.replace(\"Bascula\", \"\");\n\nlet bascula_parametros = parametros.filter(p =>\n p.parametro.toLowerCase().includes(\"bascula\" + sufijo)\n);\nflow.set(\"config_bascula_parametros\", bascula_parametros);\n\nlet bascula_rs232 = rs232.filter(p =>\n p.parametro.toLowerCase().includes(sufijo)\n);\nflow.set(\"config_bascula_rs232\", bascula_rs232);\n\n// =============================================================\n// 🧩 Mezclar datos del flujo actuales\n// =============================================================\nlet msgmix = Object.assign({}, msg, {\n bascula: bascula,\n peso1: flow.get(\"peso1\") || 0,\n peso2: flow.get(\"peso2\") || 0,\n peso3: flow.get(\"peso3\") || 0,\n peso4: flow.get(\"peso4\") || 0,\n \n});\n\n\n// =============================================================\n// 🧭 DEBUG (opcional)\n// =============================================================\n//node.warn(\"📟 Actualizada \" + bascula + \" → snapshot: \" + JSON.stringify(snapshot));\n\n// =============================================================\n// 🚀 Enviar datos al frontend\n// =============================================================\nreturn msgmix;\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2340, + "y": 960, + "wires": [ + [ + "2ec53aa1784087c1" + ] + ] + }, + { + "id": "9667a055f9c967a7", + "type": "link in", + "z": "3fbc028716abf7a9", + "g": "e3bc37824e09b774", + "name": "link in 2", + "links": [ + "4d122b4d7ef323dd", + "e59e292827957f03", + "1457adeb1e1238af", + "d6e380c9415bfb72" + ], + "x": 2215, + "y": 960, + "wires": [ + [ + "882ee24d1489b06c" + ] + ] + }, + { + "id": "7058b9e632aa584b", + "type": "link in", + "z": "3fbc028716abf7a9", + "g": "e3bc37824e09b774", + "name": "entrada uibuilder-1", + "links": [ + "88b9b4d1c5aa7359" + ], + "x": 2215, + "y": 900, + "wires": [ + [ + "882ee24d1489b06c" + ] + ] + }, + { + "id": "ba5e6b2b40fba02b", + "type": "exec", + "z": "3fbc028716abf7a9", + "g": "e3bc37824e09b774", + "command": "", + "addpay": "payload", + "append": "", + "useSpawn": "false", + "timer": "", + "winHide": false, + "oldrc": false, + "name": "", + "x": 2920, + "y": 960, + "wires": [ + [], + [], + [] + ] + }, + { + "id": "1ba8bfe271072a91", + "type": "link out", + "z": "3fbc028716abf7a9", + "g": "e3bc37824e09b774", + "name": "feedback", + "mode": "link", + "links": [ + "aa13d9f112f46a30" + ], + "x": 2875, + "y": 920, + "wires": [] + }, + { + "id": "960b4b6685af7406", + "type": "http in", + "z": "3fbc028716abf7a9", + "g": "05b4bc2ccdd08ee9", + "name": "API", + "url": "/api", + "method": "post", + "upload": false, + "swaggerDoc": "", + "x": 2790, + "y": 1240, + "wires": [ + [ + "f806cc253d96d34a", + "363afa81160f118e" + ] + ] + }, + { + "id": "f806cc253d96d34a", + "type": "debug", + "z": "3fbc028716abf7a9", + "g": "05b4bc2ccdd08ee9", + "name": "debug 8", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2960, + "y": 1240, + "wires": [] + }, + { + "id": "363afa81160f118e", + "type": "http response", + "z": "3fbc028716abf7a9", + "g": "05b4bc2ccdd08ee9", + "name": "", + "statusCode": "", + "headers": {}, + "x": 2950, + "y": 1300, + "wires": [] + }, + { + "id": "87154c99372a5f9c", + "type": "function", + "z": "3fbc028716abf7a9", + "g": "9a733b496ecde31c", + "name": "function 1", + "func": "let uuid = msg.payload.uuid\nmsg.payload = {movimiento_ok: true, uuid: uuid}\n\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2440, + "y": 1320, + "wires": [ + [ + "93bfa84a0f29e988", + "bfe9a33e26aa7cf4" + ] + ] + }, + { + "id": "bfe9a33e26aa7cf4", + "type": "mqtt out", + "z": "3fbc028716abf7a9", + "g": "9a733b496ecde31c", + "name": "tx ack", + "topic": "totemautonomo/movimientos/movimiento_ok", + "qos": "1", + "retain": "", + "respTopic": "", + "contentType": "", + "userProps": "", + "correl": "", + "expiry": "", + "broker": "4eb9ee9b8ea93b8a", + "x": 2610, + "y": 1320, + "wires": [] + }, + { + "id": "56b300da80743113", + "type": "mqtt in", + "z": "3fbc028716abf7a9", + "g": "9a733b496ecde31c", + "name": "rx movimiento", + "topic": "totemautonomo/movimientos/movimiento", + "qos": "1", + "datatype": "auto-detect", + "broker": "4eb9ee9b8ea93b8a", + "nl": false, + "rap": true, + "rh": 0, + "inputs": 0, + "x": 2270, + "y": 1320, + "wires": [ + [ + "87154c99372a5f9c", + "d4174078d4cb9375" + ] + ] + }, + { + "id": "d4174078d4cb9375", + "type": "debug", + "z": "3fbc028716abf7a9", + "g": "9a733b496ecde31c", + "name": "rx mov", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2430, + "y": 1240, + "wires": [] + }, + { + "id": "93bfa84a0f29e988", + "type": "debug", + "z": "3fbc028716abf7a9", + "g": "9a733b496ecde31c", + "name": "tx ack", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2590, + "y": 1380, + "wires": [] + }, + { + "id": "780c6c61.a0cb04", + "type": "exec", + "z": "3fbc028716abf7a9", + "g": "f4b1c07e498c11be", + "command": " ", + "addpay": "payload", + "append": "", + "useSpawn": "false", + "timer": "", + "oldrc": false, + "name": "Screen ON/OFF", + "x": 2500, + "y": 240, + "wires": [ + [], + [], + [] + ] + }, + { + "id": "49500a91.3971e4", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "f4b1c07e498c11be", + "name": "screen off", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "echo 1 | sudo tee /sys/class/backlight/0-0045/bl_power", + "payloadType": "str", + "x": 2280, + "y": 220, + "wires": [ + [ + "780c6c61.a0cb04" + ] + ] + }, + { + "id": "97ec61bc.7e2aa", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "f4b1c07e498c11be", + "name": "screen on", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "echo 0 | sudo tee /sys/class/backlight/0-0045/bl_power", + "payloadType": "str", + "x": 2280, + "y": 180, + "wires": [ + [ + "780c6c61.a0cb04" + ] + ] + }, + { + "id": "afb26e9306481251", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "f4b1c07e498c11be", + "name": "reboot", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "sudo shutdown -r now", + "payloadType": "str", + "x": 2270, + "y": 260, + "wires": [ + [ + "780c6c61.a0cb04" + ] + ] + }, + { + "id": "2c73d02c99e64b37", + "type": "inject", + "z": "3fbc028716abf7a9", + "g": "f4b1c07e498c11be", + "name": "poweroff", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "sudo poweroff", + "payloadType": "str", + "x": 2280, + "y": 300, + "wires": [ + [ + "780c6c61.a0cb04" + ] + ] + }, + { + "id": "1b496d68c6bda310", + "type": "link in", + "z": "3fbc028716abf7a9", + "name": "wsout", + "links": [ + "e5d5709dcfce538e" + ], + "x": 2975, + "y": 600, + "wires": [ + [ + "33449eadf7bfd3da" + ] + ] + }, + { + "id": "33449eadf7bfd3da", + "type": "websocket out", + "z": "3fbc028716abf7a9", + "name": "", + "server": "e18837acff3382fd", + "client": "", + "x": 3090, + "y": 520, + "wires": [] + }, + { + "id": "c5acaeca734b57ad", + "type": "websocket in", + "z": "3fbc028716abf7a9", + "name": "input", + "server": "e18837acff3382fd", + "client": "", + "x": 3090, + "y": 640, + "wires": [ + [ + "7f35b38b82e9d3d1", + "63dfdbc95bd896d0" + ] + ] + }, + { + "id": "7f35b38b82e9d3d1", + "type": "debug", + "z": "3fbc028716abf7a9", + "name": "debug 41", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3320, + "y": 580, + "wires": [] + }, + { + "id": "63dfdbc95bd896d0", + "type": "link out", + "z": "3fbc028716abf7a9", + "name": "wsin", + "mode": "link", + "links": [ + "c61c445897350722" + ], + "x": 3255, + "y": 640, + "wires": [] + }, + { + "id": "3e6f6384f2ec9846", + "type": "link out", + "z": "3fbc028716abf7a9", + "g": "bfec5f4fae03e5b4", + "name": "salida RaspiPeso", + "mode": "link", + "links": [ + "29b8ed91354ac1e2" + ], + "x": 2725, + "y": 640, + "wires": [] + }, + { + "id": "57ecdac37231f743", + "type": "uibuilder", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "", + "topic": "", + "url": "supervisor", + "okToGo": true, + "fwdInMessages": false, + "allowScripts": false, + "allowStyles": false, + "copyIndex": true, + "templateFolder": "blank", + "extTemplate": "", + "showfolder": false, + "reload": true, + "sourceFolder": "src", + "deployedVersion": "7.5.0", + "showMsgUib": false, + "title": "", + "descr": "", + "editurl": "vscode://file/home/trypton/.node-red/projects/raspipeso20_10_25/uibuilder/supervisor/?windowId=_blank", + "x": 4110, + "y": 680, + "wires": [ + [ + "4657b0f0cefa89d7", + "7698b09169dc1c99" + ], + [] + ] + }, + { + "id": "b75f8eb3486aed8d", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{\"payload\":{\"pantalla\":\"menu-principal\"}}", + "payloadType": "json", + "x": 3910, + "y": 560, + "wires": [ + [ + "57ecdac37231f743" + ] + ] + }, + { + "id": "4657b0f0cefa89d7", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "buttons \\n decoder", + "func": "\nif (msg.topic === \"tipoconfig\"){\n let tiposDatosGlobal = flow.get(\"tiposDatosGlobal\") || {\"1\": [], \"2\": [], \"3\": [], \"4\": [], \"5\": [], \"6\": []};\n\nconst tipo = msg.payload.tipo; // ej: 2\nconst datos = msg.payload.datos; // array con los parámetros\n\ntiposDatosGlobal[tipo] = datos;\n\nflow.set(\"tiposDatosGlobal\", tiposDatosGlobal);\n\nreturn ;\n}\nelse if (msg.payload.hasOwnProperty(\"seccion\")) {\n\n\n /* Los diferentes \"case\" corresponden a la pulsacion de botones de los diferentes menus de configuracion\n Cada menu solicitado mediante el click de un boton devuelve el nombre del \"div\" en el archivo html que debemos habilitar\n para que se muestre en pantalla.\n Si la entrada no consigue ningun case que se ajuste entonces caemos en el \"default\" y ahi buscamos primero si hay\n algun mensaje en \"origen\", lo que corresponde a la actualizacion de alguna de las tablas dinamicas que se generan\n para modificar cada tipo de parametro. Luego se revisan los \"modalCallback\" los cuales no indican que la respuesta\n de la notificacion de alguna accion importante ha sido positiva (\"SI\").\n \n */\n\n switch (msg.payload.seccion) {\n\n case \"menu-principal\": // boton de configuracion en pantalla principal para acceder al menu principal \n flow.set(\"snapshot_cam\", false)\n flow.set(\"escuchar_cam\", false)\n return { payload: { pantalla: \"menu-principal\" } }\n\n case \"editar\": // boton para acceder al menu de edicion de la configuracion\n return { payload: { pantalla: \"editar\" } }\n\n case \"pruebas\": // boton para acceder al menu de pruebas del sistema\n return { payload: { pantalla: \"pruebas\" } }\n\n case \"semaforo\": // boton para acceder al menu prueba del semaforo\n return { payload: { pantalla: \"semaforo\" } }\n\n case \"otras\": // boton para acceder al menu de otras acciones\n return { payload: { pantalla: \"otras\" } }\n\n case \"regresar\": // boton para regresar al menu principal desde otras acciones y desde menu de pruebas del sistema\n return { payload: { pantalla: \"menu-principal\" } }\n\n case \"salvar\": // boton para almacenar los cambios en el archivo de configuracion en el menu de edicion de la configuracion\n return ([{ payload: { pantalla: \"editar\" }, modalText: \"Desea MODIFICAR el archivo de CONFIGURACION ?\", modalCallback: \"modificar\" }, null, null, null])\n\n case \"cancelar\": // boton para cancelar y salir del menu de editar configuracion\n return ([{ payload: { pantalla: \"menu-principal\" } }, null])\n\n case \"semaforo_on\": // boton para poner el semaforo en rojo\n flow.set(\"semaforo\", \"red\")\n return ([{ payload: { pantalla: \"semaforo\" }, semaforo: \"firebrick\" }, null, { payload: 0 }])\n\n case \"semaforo_off\": // boton para poner el semaforo en verde\n flow.set(\"semaforo\", \"green\")\n return ([{ payload: { pantalla: \"semaforo\" }, semaforo: \"green\" }, null, { payload: 1 }])\n\n case \"salir-semaforo\": // boton de salida de la pantalla de prueba del semaforo\n return ({ payload: { pantalla: \"pruebas\" } })\n\n case \"barrera\": // boton de activar barrera\n return ({ payload: { pantalla: \"barrera\" }, barrera: false })\n\n case \"general\": // boton para ver / modificar tabla con los parametros generales\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"general\", tabla: flow.get(\"config_general\") }, null, null])\n\n case \"camara\": // boton para ver / modificar tabla con los parametros de la camara\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"camara\", tabla: flow.get(\"config_camara\") }, null, null])\n\n case \"conexion\": // boton para ver / modificar tabla con los parametros de conexion\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"conexion\", tabla: flow.get(\"config_conex\") }, null, null])\n\n case \"sensores\": // boton para ver / modificar tabla con los parametros de sensores\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"sensores\", tabla: flow.get(\"config_sensor\") }, null, null])\n\n case \"mensajes\": // boton para ver / modificar tabla con los parametros de los mensajes\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"mensajes\", tabla: flow.get(\"config_mensajes\") }, null, null])\n\n case \"tiempos\": // boton para ver / modificar tabla con los parametros del tiempo\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"tiempos\", tabla: flow.get(\"config_tiempos\") }, null, null])\n\n case \"aplicar\": // boton para aplicar la configuracion al RPi, en el menu-principal\n return ([{ payload: { pantalla: \"menu-principal\" }, topic: \"menu-principal\", modalText: \"Desea Recargar la CONFIGURACION ?\", modalCallback: \"recargar\" }, null, null])\n\n case \"reboot\": // boton para resetear el RPi en el menu de otras acciones\n return ([{ payload: { pantalla: \"otras\" }, topic: \"otras\", modalText: \"Desea Reiniciar el TOTEM ?\", modalCallback: \"reiniciar\" }, null, null])\n\n case \"apagar\": // boton para apagar el RPi en el menu de otras acciones\n return ([{ payload: { pantalla: \"otras\" }, topic: \"otras\", modalText: \"Desea APAGAR el TOTEM ?\", modalCallback: \"apagar\" }, null, null])\n\n case \"snapshot\": // boton para pedir snapshot\n flow.set(\"snapshot_cam\", true)\n flow.set(\"escuchar_cam\", false)\n let msgrqt = {}\n msgrqt.payload = {}\n msgrqt.url = \"http://\" + flow.get(\"camara_IP\") + \"/cgi-bin/trafficSnap.cgi?action=manSnap&channel=1\"\n msgrqt.topic = \"snapshot_rqtd\"\n return ([{ payload: { pantalla: \"snapshot\" }, topic: \"snapshot\" }, null, null, null, null, msgrqt])\n\n case \"escuchar\": // boton para escuchar camara ( recibir todas la imagenes que envie)\n flow.set(\"snapshot_cam\", false)\n flow.set(\"escuchar_cam\", true)\n return ([{ payload: { pantalla: \"escuchar\" }, topic: \"escuchar\" }, null, null])\n\n case \"left\": // Boton para ir a la imagen anterior en escuchar camara\n flow.set(\"getfromarray\", \"left\")\n return ([{ payload: { pantalla: \"escuchar\" }, topic: \"escuchar\" }, null, null])\n\n case \"right\": // Boton para ir a la siguiente imagen en escuchar camara\n flow.set(\"getfromarray\", \"right\")\n return ([{ payload: { pantalla: \"escuchar\" }, topic: \"escuchar\" }, null, null])\n\n case \"salir-snapshot\": // boton de salida de la pantalla de ver snapshot\n flow.set(\"snapshot_cam\", false)\n flow.set(\"escuchar_cam\", false)\n return ({ payload: { pantalla: \"pruebas\" } })\n\n case \"salir-barrera\": // boton de salida de la barrera\n\n return ({ payload: { pantalla: \"pruebas\" }, barrera: false })\n\n case \"salir-escuchar\": // boton de salida de la pantalla de escuchar la camara\n flow.set(\"snapshot_cam\", false)\n flow.set(\"escuchar_cam\", false)\n flow.set(\"snapshot_array\", [])\n flow.set(\"snapshot_readindex\", 0)\n flow.set(\"snapshot_saveindex\", 0)\n flow.set(\"snapshot_indexmax\", 0)\n flow.set(\"getfromarray\", \"\")\n return ({ payload: { pantalla: \"pruebas\" } })\n\n case \"activar-barrera\": // boton de activar barrera\n node.send([{ payload: { pantalla: \"barrera\" }, barrera: true }, null, null, null, null, null, null, { payload: 0 }])\n setTimeout(function () {\n node.send([null, null, null, null, null, null, null, { payload: 1 }])\n }, flow.get(\"relay2_pulse\"));\n return\n\n case \"cancel-table\": // boton de CANCEL de las tablas de configuracion de parametros\n return ({ payload: { pantalla: \"editar\" } })\n\n case \"salir\": // boton de salida del menu principal\n return ({ payload: { pantalla: \"principal\" } })\n\n case \"tipos\": // boton de salida al menu editar\n msg.payload = {pantalla: \"tipos\", tiposDatos: flow.get(\"tiposDatosGlobal\") || {\"1\": [], \"2\": [], \"3\": [], \"4\": [], \"5\": [], \"6\": []}};\n return (msg)\n\n case \"volver_tipos\": // boton de salida al menu editar\n return ({ payload: { pantalla: \"editar\" } })\n\n case \"ayuda\": // boton de salida al menu editar\n return ({ payload: { pantalla: \"ayuda\" } })\n\n case \"salir-ayuda\": // boton de salida al menu editar\n return ({ payload: { pantalla: \"editar\" } })\n\n default: // chequeamos si tenemos acciones a realizar para lo cual revisamos el \"origen\" (actualizar en memoria sin preguntar usuario) y/o el \"modalCallback\" con esto sabemos de que tabla es el mensaje enviado por el MODAL\n node.send([null, null, null, null, null, null, msg])\n if (msg.origen == \"actualizar\") {// salvar en memoria la configuracion que ha sido modificada (el Topic nos dice cual configuracion)\n let myconfig = msg.payload.table // table contiene el resultado de leer la tabla con todos los valores normalizados en uibuilder\n \n switch (msg.topic) { // guardamos cambios en el tipo de configuracion correspondiente\n case \"general\":\n flow.set(\"config_general\", myconfig)\n break;\n case \"conexion\":\n // ==================================== VALIDAMOS todas las IP de la configuracion ==================================================\n //node.warn(\"valor 1 = \" + myconfig[1].valor)\n \n if (validarIP(myconfig[0].valor)){\n flow.set(\"config_conex\", myconfig)\n }\n else {\n return ([{ payload: { pantalla: \"editar\" }, topic: \"error\", modalText: \"La IP del DISPOSITIVO NO es correcta ?\", modalType:\"notificacion\" }, null, null])\n }\n \n if (validarIP(myconfig[1].valor)) {\n flow.set(\"config_conex\", myconfig)\n }\n \n else {\n return ([{ payload: { pantalla: \"editar\" }, topic: \"error\", modalText: \"La IP del ROUTER NO es correcta ?\", modalType: \"notificacion\" }, null, null])\n }\n /*\n if (validarIP(myconfig[5].valor)) {\n flow.set(\"config_conex\", myconfig)\n }\n else {\n return ([{ payload: { pantalla: \"editar\" }, topic: \"error\", modalText: \"La IP del WEBSOCKET NO es correcta ?\", modalType: \"notificacion\" }, null, null])\n }\n \n if (validarIP(myconfig[15].valor)) {\n flow.set(\"config_conex\", myconfig)\n }\n else {\n return ([{ payload: { pantalla: \"editar\" }, topic: \"error\", modalText: \"La IP del TOTEM pareja NO es correcta ?\", modalType: \"notificacion\" }, null, null])\n }\n */\n break;\n case \"camara\":\n flow.set(\"config_camara\", myconfig)\n break;\n case \"sensores\":\n flow.set(\"config_sensor\", myconfig)\n break;\n case \"tiempos\":\n flow.set(\"config_tiempos\", myconfig)\n break;\n case \"mensajes\":\n flow.set(\"config_mensajes\", myconfig)\n break;\n }\n return { payload: { pantalla: \"editar\" } }\n }\n\n \n msg.hello = \"hello 3\"\n node.send([null, null, null, null, null, null, msg])\n return null\n\n }\n} // final del SWITCH\nelse if (msg.modalCallback == \"modificar\") {\n /*\n let config_totem = []\n config_totem = flow.get(\"config_general\").concat(flow.get(\"config_camara\"), flow.get(\"config_conex\"), flow.get(\"config_sensor\"), flow.get(\"config_mensajes\"), flow.get(\"config_tiempos\"), flow.get(\"tiposDatosGlobal\"))\n flow.set(\"config_totem\", config_totem)\n let jsontest = toJson(flow.get(\"config_totem\"));\n */\n let config_totem = {\n general: flow.get(\"config_general\") || [],\n camara: flow.get(\"config_camara\") || [],\n conexion: flow.get(\"config_conex\") || [],\n sensores: flow.get(\"config_sensor\") || [],\n mensajes: flow.get(\"config_mensajes\") || [],\n tiempos: flow.get(\"config_tiempos\") || [],\n tipos: flow.get(\"tiposDatosGlobal\") || {}\n };\n\n flow.set(\"config_totem\", config_totem);\n\n msg.payload = JSON.stringify(config_totem, null, 2) + \"\\n\\r\";\n //msg.payload = JSON.stringify(jsontest, null, 2) + \"\\n\\r\"; // el factor 2 agrega 2 espacio al inicio de cada elemento\n //y agrega tambien una linea lo cual es interesante para la legibilidad del formato y para que los clientes SFTP puedan\n //leer bien el archivo, como es el caso de Mobaxterm.\n\n return ([{ payload: { pantalla: \"menu-principal\" } }, msg, null])\n}\nelse if (msg.modalCallback == \"recargar\") {\n return ([null, null, null, { payload: 1 }])\n}\n\nelse if (msg.modalCallback == \"reiniciar\") {\n return ([null, null, null, null, { payload: \"sudo shutdown -r now\" }])\n}\n\nelse if (msg.modalCallback == \"apagar\") {\n return ([null, null, null, null, { payload: \"sudo poweroff\" }])\n}\n\n//============================================== FUNCIONES ==============================================================\n//esta funcion convierte un array con elementos [parametro, valor] en un objeto Json respetando los tipos de parametro (numerico, booleano, texto)\nfunction toJson(data) {\n let newJson = {}\n /*\n data.forEach(item => {\n let param = item.parametro;\n let value = item.valor;\n */\n for (let i = 0; i < data.length; i++) {\n let param = data[i].parametro;\n let value = data[i].valor;\n\n // Parseo de tipos igual que antes\n if (typeof value === 'string') {\n if (value.toLowerCase() === \"true\") {\n newJson[param] = true;\n } else if (value.toLowerCase() === \"false\") {\n newJson[param] = false;\n } else if (!isNaN(value) && value.trim() !== \"\") {\n newJson[param] = Number(value);\n } else {\n newJson[param] = value;\n }\n } else {\n // Por si el value ya viene como booleano o número real\n newJson[param] = value;\n }\n }\n return (newJson)\n}\n\n//=======================================================================================================================================\n\nfunction validarIP(ip) {\n const regex = /^(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)$/;\n return regex.test(ip);\n}", + "outputs": 8, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 4320, + "y": 660, + "wires": [ + [ + "58bb0cd40f89ba0d", + "fcce285ce4cc49d0" + ], + [ + "4ae8af66b4ef6232", + "508d57d90e013805" + ], + [ + "7004d0202704343a" + ], + [ + "17547c6cf2cc8bdf" + ], + [ + "e4f5cd90a27363a0" + ], + [ + "9a0817ab9b58c626" + ], + [ + "eaf1c8163a641d84" + ], + [ + "38d2c6565b5ca0bf", + "c8010702a8431461" + ] + ] + }, + { + "id": "7698b09169dc1c99", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "debug 13", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 4300, + "y": 560, + "wires": [] + }, + { + "id": "7004d0202704343a", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "set semaforo", + "mode": "link", + "links": [], + "x": 4535, + "y": 680, + "wires": [] + }, + { + "id": "58bb0cd40f89ba0d", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "feedback", + "mode": "link", + "links": [ + "dd15174f0db1d58c" + ], + "x": 4485, + "y": 600, + "wires": [] + }, + { + "id": "fcce285ce4cc49d0", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "debug 14", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 4520, + "y": 520, + "wires": [] + }, + { + "id": "17547c6cf2cc8bdf", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "aplicar_config", + "mode": "link", + "links": [ + "c5a3e80cc446b610" + ], + "x": 4535, + "y": 720, + "wires": [] + }, + { + "id": "e4f5cd90a27363a0", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "reiniciar", + "mode": "link", + "links": [ + "1ca5e534fa4962de" + ], + "x": 4535, + "y": 760, + "wires": [] + }, + { + "id": "0f3cd31839136f10", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "datos de\\n pesada y \\n snapshot", + "func": "if(msg.payload.hasOwnProperty(\"tiposDatos\")){\n return(msg)\n\n}\nelse {\n let msgmix = {}\n if (flow.get(\"snapshot_cam\") && msg.payload.hasOwnProperty(\"Picture\")) { // si recibimos snapshot extraemos informacion necesaria para mostrar en la pagina de snapshot\n msgmix.plate = msg.payload.Picture.Plate.PlateNumber // guardamos el numero de matricula\n msgmix.picture = \"data:image/png;base64,\" + msg.payload.Picture.NormalPic.Content\n msgmix.plate_time = msg.payload.Picture.SnapInfo.SnapTime\n msgmix.camera_id = msg.payload.Picture.SnapInfo.DeviceID\n }\n\n\n else if (flow.get(\"escuchar_cam\")) { // Si estamos esperando snapshots de la camara o acciones del usuario en botones left o right\n let snapshot_array = flow.get('snapshot_array') || [];\n let snapshot_readindex = flow.get('snapshot_readindex') || 0;\n let snapshot_saveindex = flow.get('snapshot_saveindex') || 0;\n let snapshot_indexmax = flow.get('snapshot_indexmax') || 0;\n\n\n if (flow.get(\"getfromarray\") == \"left\") {\n msgmix.buttons = true // indica al index.js (del uibuilder) que no tiene que inicializar la pantalla de escuchar camara\n if (snapshot_readindex > 0) {\n snapshot_readindex = snapshot_readindex - 1\n flow.set(\"snapshot_readindex\", snapshot_readindex)\n msgmix = snapshot_array[snapshot_readindex]\n msgmix.snapshot_readindex = snapshot_readindex\n msgmix.snapshot_indexmax = snapshot_indexmax\n }\n }\n else if (flow.get(\"getfromarray\") == \"right\") {\n msgmix.buttons = true // indica al index.js (del uibuilder) que no tiene que inicializar la pantalla de escuchar camara\n if (snapshot_readindex < flow.get(\"snapshot_indexmax\") - 1) {\n snapshot_readindex = snapshot_readindex + 1\n flow.set(\"snapshot_readindex\", snapshot_readindex)\n msgmix = snapshot_array[snapshot_readindex]\n msgmix.snapshot_readindex = snapshot_readindex\n msgmix.snapshot_indexmax = snapshot_indexmax\n\n }\n }\n else {\n if (msg.payload.hasOwnProperty(\"Picture\")) { // si recibimos snapshot extraemos informacion necesaria para mostrar en la pagina de escuchar camara\n msgmix.plate = msg.payload.Picture.Plate.PlateNumber // guardamos el numero de matricula\n msgmix.picture = \"data:image/png;base64,\" + msg.payload.Picture.NormalPic.Content\n msgmix.plate_time = msg.payload.Picture.SnapInfo.SnapTime\n msgmix.camera_id = msg.payload.Picture.SnapInfo.DeviceID\n msgmix.buttons = true\n\n\n snapshot_array[snapshot_saveindex] = RED.util.cloneMessage(msgmix); // Clonamos para evitar referencias\n flow.set(\"snapshot_array\", snapshot_array)\n flow.set(\"snapshot_saveindex\", snapshot_saveindex + 1)\n flow.set(\"snapshot_indexmax\", snapshot_indexmax + 1)\n\n if (snapshot_readindex == snapshot_saveindex - 1) {\n flow.set(\"snapshot_readindex\", snapshot_readindex + 1) // en caso que el index de lectura este al final, se incrementa al llegar una nueva imagen\n }\n msgmix.snapshot_indexmax = flow.get(\"snapshot_indexmax\")\n msgmix.snapshot_readindex = flow.get(\"snapshot_readindex\")\n }\n }\n }\n\n else {\n\n let msgmix = {}\n\n }\n flow.set(\"getfromarray\", \"\")\n\n //*****************************************Agrega datos de la pesada para PAGINA PRINCIPAL************************************************************ */\n if (!msg.payload.hasOwnProperty(\"Picture\")) {\n msgmix = Object.assign({}, msg, msgmix); // msg sobrescribe valores si hay conflictos\n }\n msgmix.peso = flow.get(\"peso\")\n msgmix.semaforo_color = flow.get(\"semaforo\")\n msgmix.matricula = flow.get(\"matricula\")\n msgmix.peso_estable_flag = flow.get(\"peso_estable_flag\")\n msgmix.vial = flow.get(\"numero_vial\")\n msgmix.totem_id = flow.get(\"totem_id\")\n msgmix.iplan = flow.get(\"myip\")\n msgmix.ipsupervisor = flow.get(\"websocket_url\")\n msgmix.snapshot_cam = flow.get(\"snapshot_cam\")\n msgmix.escuchar_cam = flow.get(\"escuchar_cam\")\n return msgmix;\n}", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3880, + "y": 680, + "wires": [ + [ + "57ecdac37231f743", + "9559a621baa2e0e5" + ] + ] + }, + { + "id": "9559a621baa2e0e5", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "debug 17", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 4100, + "y": 580, + "wires": [] + }, + { + "id": "9a0817ab9b58c626", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "snapshot_rqst", + "mode": "link", + "links": [], + "x": 4535, + "y": 800, + "wires": [] + }, + { + "id": "eaf1c8163a641d84", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "debug 18", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 4580, + "y": 840, + "wires": [] + }, + { + "id": "22a6d78769eca8a4", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "CONFIGURACION DE PARAMETROS Y PRUEBAS ", + "info": "Este grupo de nodos permiten servir una pagina web para que el usuario desde un movil pueda realizar la gestion de configuracion\nde los principales parametros del Totem asi como realizar algunas pruebas en la camara y en el semaforo.\nEl punto central de este grupo es el paquete UIBUILDER para nodered el cual convive sin inconvenientes con el DASHBOARD de nodered.\nEl uibuilder posee un nodo para cada path de acceso que deseemos habilitar. Cada nodo es independiente del otro en el sentido que \ncada uno dispone de un index.html, un index.js y un index.css ue permiten configurar la pagina que sirve el nodo. \nRecordemos que estos archivos se ejecutan en el cliente, por lo tanto para que el index.js tenga acceso a los datos de nodered y nodered \na los datos generados por el cliente, el nodo uibuilder establece una comunicacion websocket transparente para el usuario de manera\na poder intercambiar informacion entre nodered y el browser. Los elementos fundamentales en index.js para llevar a cabo esta comunicacion\nson las funciones \"uibuilder.onChange()\" para recibir los mensajes de nodered y \"uibuilder.send()\" para enviar a nodered los datos generados \npor el cliente.\nEl nodo \"buttons decoder\" fundamentalmente se dedica a interpretar los comandos del cliente (botones que ha pulsado el usuario) de manera a recopilar\nla informacion adecuada para enviar al uibuilder nuevamente de manera que cambie de pagina o muestre ciertos datos, o tambien a ejecutar\nlos comandos solicitados como reboot, apagar dispositivo, encender / apagar semaforo, ...\nEl nodo de entrada \"datos de pesada y snapshot\" recibe por un lado los snapshots enviados por la camara y extrae la informacion necesaria \npara enviar al cliente, y por otro lado siempre que haya alguna recepcion de datos del Totem se preparan los datos correspondientes para \ncolocarlos en la pantalla principal del uibuilder o pantalla de monitoreo.", + "x": 4010, + "y": 820, + "wires": [] + }, + { + "id": "38d2c6565b5ca0bf", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "activa-barrera", + "mode": "link", + "links": [], + "x": 4535, + "y": 880, + "wires": [] + }, + { + "id": "c8010702a8431461", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "debug 27", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 4580, + "y": 920, + "wires": [] + }, + { + "id": "4ae8af66b4ef6232", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "debug 26", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 4640, + "y": 720, + "wires": [] + }, + { + "id": "508d57d90e013805", + "type": "file", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "guardar config", + "filename": "/home/trypton/.node-red/projects/config_supervisor.txt", + "filenameType": "str", + "appendNewline": true, + "createDir": true, + "overwriteFile": "true", + "encoding": "none", + "x": 4620, + "y": 640, + "wires": [ + [] + ] + }, + { + "id": "cb6c28fe05c25229", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "6c0376d0b4669415", + "name": "ws_out", + "links": [ + "4526132d69e6c210", + "7f282cc6ba963043", + "ce3029f2688a0aba", + "ed04082db0d29b5b" + ], + "x": 4155, + "y": 1720, + "wires": [ + [ + "d7d43c051dffd484", + "e5d5709dcfce538e" + ] + ] + }, + { + "id": "d7d43c051dffd484", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "6c0376d0b4669415", + "name": "salida websocket", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 4350, + "y": 1660, + "wires": [] + }, + { + "id": "e5d5709dcfce538e", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "6c0376d0b4669415", + "name": "link out 4", + "mode": "link", + "links": [ + "1b496d68c6bda310" + ], + "x": 4305, + "y": 1780, + "wires": [] + }, + { + "id": "7ced20377dba5074", + "type": "debug", + "z": "744bdf3a0e08d754", + "name": "debug 20", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3020, + "y": 2440, + "wires": [] + }, + { + "id": "73ab7179bc4cf185", + "type": "inject", + "z": "744bdf3a0e08d754", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "SELECT * FROM movimientos WHERE matricula = '6421FPG' AND tipo = 'inicial'", + "payload": "1", + "payloadType": "num", + "x": 650, + "y": 2060, + "wires": [ + [ + "cc229c10e3f4e423" + ] + ] + }, + { + "id": "558309d02524b5f9", + "type": "inject", + "z": "744bdf3a0e08d754", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "0420MMR", + "payloadType": "str", + "x": 3320, + "y": 2420, + "wires": [ + [ + "9dd64e7e2d2a77c9" + ] + ] + }, + { + "id": "1f6ee5fb9fb53d0d", + "type": "debug", + "z": "744bdf3a0e08d754", + "name": "debug 29", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3940, + "y": 2340, + "wires": [] + }, + { + "id": "6c42dc5e8e906084", + "type": "debug", + "z": "744bdf3a0e08d754", + "name": "debug 30", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 4200, + "y": 2400, + "wires": [] + }, + { + "id": "3d15b8a25709d397", + "type": "inject", + "z": "744bdf3a0e08d754", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "1334DEF", + "payloadType": "str", + "x": 3320, + "y": 2500, + "wires": [ + [ + "9dd64e7e2d2a77c9" + ] + ] + }, + { + "id": "44c64f7eb7bdf346", + "type": "debug", + "z": "744bdf3a0e08d754", + "name": "debug 35", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1800, + "y": 1820, + "wires": [] + }, + { + "id": "85f07bfdecf73577", + "type": "inject", + "z": "744bdf3a0e08d754", + "name": "Matriculas", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Matriculas", + "payloadType": "str", + "x": 1880, + "y": 2680, + "wires": [ + [ + "c95d9408ac6b1595" + ] + ] + }, + { + "id": "4aa21914e723be52", + "type": "inject", + "z": "744bdf3a0e08d754", + "name": "Identificadores", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Identificadores", + "payloadType": "str", + "x": 1860, + "y": 2760, + "wires": [ + [ + "c95d9408ac6b1595" + ] + ] + }, + { + "id": "cecb419ca825ee49", + "type": "inject", + "z": "744bdf3a0e08d754", + "name": "Matriculas_Identificadores", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Matriculas_Identificadores", + "payloadType": "str", + "x": 1830, + "y": 2720, + "wires": [ + [ + "c95d9408ac6b1595" + ] + ] + }, + { + "id": "381ba7abb9ab0be7", + "type": "mysql", + "z": "744bdf3a0e08d754", + "mydb": "1c8613764b7f685e", + "name": "check matricula", + "x": 3760, + "y": 2420, + "wires": [ + [ + "8586621b0d788c6c", + "1f6ee5fb9fb53d0d" + ] + ] + }, + { + "id": "9dd64e7e2d2a77c9", + "type": "function", + "z": "744bdf3a0e08d754", + "name": "check matricula", + "func": "let matricula = msg.payload\nflow.set(\"matricula\", matricula)\n//Solicitamos un solo record donde aparezca registrada la matricula (tabla de Vehiculos) y revisamos si se encuentra en transito (tabla movimientos)\n// en caso que no este registrada la matricula, la BD nos entrega un record con los parametros en cero o en null\nmsg.topic = `SELECT\n CASE WHEN ma.matricula IS NOT NULL THEN 1 ELSE 0 END AS registrada,\n CAST(EXISTS(SELECT 1 FROM movimientos m WHERE m.matricula='${matricula}') AS INTEGER) AS transito,\n ma.tara, ma.pma\n FROM (SELECT '${matricula}' AS MatriculaInput) AS input\n LEFT JOIN Matriculas ma ON ma.matricula = input.MatriculaInput\n LIMIT 1;`;\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3520, + "y": 2420, + "wires": [ + [ + "381ba7abb9ab0be7" + ] + ] + }, + { + "id": "8586621b0d788c6c", + "type": "function", + "z": "744bdf3a0e08d754", + "name": "check results", + "func": "let result = msg.payload\nflow.set(\"matricula_revisada\", true)\nmsg.payload = {}\nmsg.topic = \"\"\nmsg.payload.totem_id = flow.get(\"totem_id\")\nmsg.payload.numero_vial = flow.get(\"numero_vial\")\n\nif (result.length > 0){ // Tenemos al menos un record de respuesta de la BD\n if (result[0].registrada == 1){\n msg.payload.registrada = !!result[0].registrada // la doble negacion convierte a booleano ( true / false)\n msg.payload.pma = result[0].pma\n msg.payload.tara = result[0].tara\n msg.payload.transito = !!result[0].transito\n if(!msg.payload.transito){ // si esta en transito no busca lista de menu\n setTimeout(function () {\n node.send([null, { payload: flow.get(\"matricula\") }]) //genera lista de opciones para presentar el menu en pantalla\n }, 1000)\n }\n }\n else{\n msg.payload.registrada = result[0].registrada\n }\n}\nelse{\n msg.payload.respuestaBD = \"ningun record\"\n}\nreturn ([msg, null]);\n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3970, + "y": 2420, + "wires": [ + [ + "6c42dc5e8e906084" + ], + [] + ] + }, + { + "id": "9bd381438eec96eb", + "type": "debug", + "z": "744bdf3a0e08d754", + "name": "debug 40", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3020, + "y": 2340, + "wires": [] + }, + { + "id": "914c1af9a76e0d6b", + "type": "inject", + "z": "744bdf3a0e08d754", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "Select * from Matriculas_Identificadores where matricula = \"6421FPG\";", + "payload": "1", + "payloadType": "num", + "x": 2150, + "y": 2480, + "wires": [ + [ + "8558335b01c7e441" + ] + ] + }, + { + "id": "1bcbf5de4c44ac05", + "type": "inject", + "z": "744bdf3a0e08d754", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "INSERT INTO Matriculas_Identificadores (matricula, id) VALUES (\"6423FPG\", \"546DA2A4-B07B-482D-B98F-B4EB0FFABCAE\") ;", + "payload": "1", + "payloadType": "num", + "x": 2150, + "y": 2520, + "wires": [ + [ + "8558335b01c7e441" + ] + ] + }, + { + "id": "4df94cef4bd6c3bf", + "type": "inject", + "z": "744bdf3a0e08d754", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "INSERT INTO Identificadores (id, id_padre,nivel, descripcion) VALUES (\"546DA2A4-B07B-482D-B98F-B4EB0FFABCAE\", null , 0, \"AYTO ALLER, ESCOMBROS\") ;", + "payload": "1", + "payloadType": "num", + "x": 2310, + "y": 2520, + "wires": [ + [ + "8558335b01c7e441" + ] + ] + }, + { + "id": "bf207ef538347e56", + "type": "comment", + "z": "744bdf3a0e08d754", + "name": "Version 1.0", + "info": "Version 1.10. Se modifica la forma de actualizar las tablas. Actualmente se hace un comando SQL multiple que consiste en comenzar una Transaccion,\nBorrar el contenido de la tabla con DELETE, hacer los INSERT necesarios del nuevo contenido tomando la precaucion de que el array que ha llegado\ntiene registros y finalmente se realiza un COMMIT. De esta forma hacemos un espejo de la tavla del Servidor.\nSe agrego una nueva tabla \"UbicacionesInternas\" que contiene basicamente dos campos, un id que representa el tipo de residuos y una descripcion\nque contiene el texto a mostrar en pantalla y que le dice al conductor hacia donde debe dirigirse.\n\nSe agrego el envio de movimiento inicial al servidor de Urgesbas. Esto lo hacemos en el nodo \"comandos via websockets\". Para ello hemos agregado\ntambien una nueva salida y un nuevo nodo https. En esta ocasion no esperamos por ningun ACK simplemente enviamos.\n\nSe agrego almacenamiento de imagen de firma en el nodo \"comandos via websockets\" para ello utilizamos un nuevo nodo de escritura de archivos\ny se agrego una nueva salida al nodo.\n\nSe agrego en el nodo \"send data\", el \"nombre_imagen_f\" para tambien considerar las imagenes del movimiento final a la hora de borrar \nlos archivos de imagenes vencidas\n\nSe agregaron los campos \"qr_f\" y \"rfid_f\" en el nodo \"movimiento cerrado\" correspondientes a la identificacion en el movimiento final\n\n", + "x": 1120, + "y": 880, + "wires": [] + }, + { + "id": "422ffdb7a80b308a", + "type": "https-node", + "z": "744bdf3a0e08d754", + "g": "1e7a7eecbf88e674", + "name": "https request", + "method": "POST", + "ret": "txt", + "paytoqs": "ignore", + "url": "", + "tls": "", + "persist": false, + "proxy": "", + "authType": "", + "senderr": false, + "x": 2130, + "y": 380, + "wires": [ + [ + "ecd2ede4ce450c22" + ] + ] + }, + { + "id": "5c03e30bd6a7dd59", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "1e7a7eecbf88e674", + "name": "function 5", + "func": "msg.url = \"https://pre-urbgesbas.urbaser.com/WebService/api/Totem/InsertarMovimientos?token=FDEB4C00-5F58-4C2D-AD80-9E7064D8FC4E\"\nmsg.payload = flow.get(\"record_test\")\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1940, + "y": 380, + "wires": [ + [ + "6455539b9fac2063" + ] + ] + }, + { + "id": "150a34e02c2def17", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "1e7a7eecbf88e674", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "1", + "payloadType": "num", + "x": 1790, + "y": 380, + "wires": [ + [ + "5c03e30bd6a7dd59" + ] + ] + }, + { + "id": "bebcf89f4daa2d2c", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "1e7a7eecbf88e674", + "name": "debug 42", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2500, + "y": 360, + "wires": [] + }, + { + "id": "ecd2ede4ce450c22", + "type": "json", + "z": "744bdf3a0e08d754", + "g": "1e7a7eecbf88e674", + "name": "", + "property": "payload", + "action": "", + "pretty": false, + "x": 2330, + "y": 380, + "wires": [ + [ + "bebcf89f4daa2d2c" + ] + ] + }, + { + "id": "54bb5d92c06ce75c", + "type": "debug", + "z": "744bdf3a0e08d754", + "name": "debug 44", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3580, + "y": 660, + "wires": [] + }, + { + "id": "34bf8ec19b0e78a7", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "updateBD", + "links": [ + "a58ecf0f246aff6a" + ], + "x": 2055, + "y": 1160, + "wires": [ + [ + "950c8115e364a493" + ] + ] + }, + { + "id": "950c8115e364a493", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "SELECT pendientes", + "func": "setTimeout(function () {\n msg.topic = \"SELECT * FROM movimientos_cerrados WHERE sincro = 0 ORDER BY ts_cierre ASC\";\n flow.set(\"indiceActual\", 0); //inicializamos el indice que recorre el array de records pendientes en los siguientes nodos\n msg.inicio = true // indica que estamos iniciando el array. Con esto en el proximo nodo se hace diferencia en el procesamiento\n node.send(msg)\n}, 2000);\n\nreturn", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2200, + "y": 1160, + "wires": [ + [ + "d144432c1ab94daf" + ] + ] + }, + { + "id": "f41c4b5c61a58ea2", + "type": "http request", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "Enviar a API", + "method": "POST", + "ret": "txt", + "paytoqs": "ignore", + "url": "", + "headers": [ + { + "keyType": "Content-Type", + "keyValue": "application/json", + "valueType": "other", + "valueValue": "application/json" + } + ], + "x": 3270, + "y": 1160, + "wires": [ + [ + "b1c81347b429091a" + ] + ] + }, + { + "id": "b1c81347b429091a", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "Procesar respuesta", + "func": "let pendientes = flow.get(\"pendientes\") || [];\nlet i = flow.get(\"indiceActual\") || 0;\nlet actual = pendientes[i];\nif (flow.get(\"movimientosxapi\")) {\n if (msg.statusCode === 200) {\n msg.topic = `UPDATE movimientos_cerrados SET sincro = 1 WHERE uuid = '${actual.uuid}'`;\n msg.payload = undefined;\n return [msg, null];\n } else {\n node.warn(`Error al enviar UUID ${actual.uuid}, código HTTP: ${msg.statusCode}`);\n return [null, { payload: 1 }];\n }\n}\nif (flow.get(\"movimientosxmqtt\")){\n if(flow.get(\"ultimo_movimiento_ok\")){\n msg.topic = `UPDATE movimientos_cerrados SET sincro = 1 WHERE uuid = '${actual.uuid}'`;\n msg.payload = undefined;\n return [msg, null];\n }\n else{\n node.warn(`Error al enviar UUID ${actual.uuid} via MQTT`);\n return [null, { payload: 1 }];\n }\n}", + "outputs": 2, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3490, + "y": 1160, + "wires": [ + [ + "76d19764677ebc8a", + "867c5654842a1210" + ], + [ + "911f1b379de9ba3a" + ] + ] + }, + { + "id": "911f1b379de9ba3a", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "Siguiente registro", + "func": "let pendientes = flow.get(\"pendientes\") || [];\nlet i = flow.get(\"indiceActual\") || 0;\n\ni += 1;\nflow.set(\"indiceActual\", i);\nmsg.inicio = false; // con esto indicamos al nodo \"extrae files\" que continue el recorrido del array\n\nif (i < pendientes.length) {\n msg.payload = pendientes[i];\n msg.url = flow.get(\"url_API\")\n msg.requestTimeout = 5000\n return msg;\n} else {\n flow.set(\"pendientes\", []);\n flow.set(\"indiceActual\", 0);\n node.log(\"Sincronización completa.\");\n return null;\n}", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3930, + "y": 1160, + "wires": [ + [ + "2b9158681d5eb09b", + "f4c6152e8ed7f5a6" + ] + ] + }, + { + "id": "76d19764677ebc8a", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "debug 1", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3680, + "y": 1100, + "wires": [] + }, + { + "id": "3c3867c2a2185f63", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "debug 7", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2600, + "y": 1100, + "wires": [] + }, + { + "id": "2b9158681d5eb09b", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "debug 9", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 4060, + "y": 1100, + "wires": [] + }, + { + "id": "d4a053997921cfc0", + "type": "file in", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "lee imagen_i", + "filename": "filename", + "filenameType": "msg", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 2870, + "y": 1100, + "wires": [ + [ + "89f64c532d3d78e7" + ] + ] + }, + { + "id": "89f64c532d3d78e7", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "agrega imagenes", + "func": "\nif (msg.tipo == \"imagen_i\"){\n flow.set(\"imagen_i\", msg.payload)\n flow.set(\"imagen_i_rx_flag\", true)\n}\nelse if (msg.tipo == \"imagen_f\"){\n flow.set(\"imagen_f\", msg.payload)\n flow.set(\"imagen_f_rx_flag\", true)\n}\nelse if (msg.tipo == \"firma\"){\n flow.set(\"firma\", msg.payload)\n flow.set(\"firma_rx_flag\", true)\n}\n//**********************************************Hay que revisar esto porque solamente tenemos que enviar cuando ************************************************** */\n//********************************************** tengamos todas las imagenes a enviar. por lo tanto es necesario *************************** */\n//********************************************** revisar si existen imagenes sobre todo de la firma que no siempre existe *******************************************************/\n\n\nif (msg.files == 1 && flow.get(\"imagen_i_rx_flag\")){\n let reg2send = flow.get(\"pendientes\")[flow.get(\"indiceActual\")]\n reg2send.imagen_entrada = flow.get(\"imagen_i\")\n if (flow.get(\"movimientosxapi\")){\n node.send([{ payload: reg2send, url: flow.get(\"url_API\"), requestTimeout : 5000}, null, null])\n }\n if (flow.get(\"movimientosxmqtt\")) { // ponemos if en vez de else if para que se pueda hacer por las dos vias si estan habilitadas\n flow.set(\"ultimo_movimiento_ok\", false) // inicializamos el flag de acknowledge que hemos implementado\n flow.set(\"ultimo_movimiento_id\", reg2send.uuid) // guardamos uuid como marca para el ack que debemos recibir\n flow.set(\"ack_old_movimientos\", true)\n node.send([null, { payload: reg2send}, {payload:reg2send.uuid}])\n }\n}\nelse if (msg.files == 2 && flow.get(\"imagen_f_rx_flag\") && flow.get(\"firma_rx_flag\")){\n let reg2send = flow.get(\"pendientes\")[flow.get(\"indiceActual\")]\n reg2send.imagen_entrada = flow.get(\"imagen_i\")\n reg2send.imagen_salida = flow.get(\"imagen_f\")\n reg2send.firma = flow.get(\"firma\")\n if (flow.get(\"movimientosxapi\")) {\n node.send([{ payload: reg2send, url: flow.get(\"url_API\"), requestTimeout: 5000 }, null, null])\n }\n if (flow.get(\"movimientosxmqtt\")) {\n flow.set(\"ultimo_movimiento_ok\", false)\n flow.set(\"ultimo_movimiento_id\", reg2send.uuid)\n flow.set(\"ack_old_movimientos\", true)// indicamos que el ack que esperamos es de la sincronizacion de viejos movimientos\n node.send([null, { payload: reg2send }, { payload: reg2send.uuid }])\n }\n}\nreturn", + "outputs": 3, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3070, + "y": 1160, + "wires": [ + [ + "a42d3e0da2d9dbda", + "f41c4b5c61a58ea2" + ], + [ + "4a2f600ba9ca7a15", + "d5ced8b11f124c67" + ], + [ + "1d3c5c1f2e5be4b0" + ] + ] + }, + { + "id": "e944fa11afc97160", + "type": "file in", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "lee firma", + "filename": "filename", + "filenameType": "msg", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 2880, + "y": 1240, + "wires": [ + [ + "89f64c532d3d78e7" + ] + ] + }, + { + "id": "f4c6152e8ed7f5a6", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "extrae nombres de archivos", + "func": "if (msg.inicio){\n flow.set(\"pendientes\", msg.payload);\n}\n\n//flow.set(\"indiceActual\", 0);\nlet indiceActual = flow.get(\"indiceActual\")\nlet pendientes = flow.get(\"pendientes\")\nmsg = {} //atencion a esta inicializacion, verificar que este msg no influye en ninguna respuesta a ningun totem\nlet msg2 = {}\nlet msg_s = {}\nif (pendientes.length > 0) {\n let imagename_i = pendientes[indiceActual].nombre_imagen_i\n let imagename_f = pendientes[indiceActual].nombre_imagen_f\n\n // Procesamos imagen de entrada\n if(imagename_i != \"Sin Matricula\"){\n msg.filename = flow.get(\"path_images\") + imagename_i // extrae el nombre de la imagen de entrada del record pendiente de sincronizar\n }\n else{\n msg.filename = flow.get(\"path_images\") + imagename_i + \".jpg\"\n }\n msg.tipo = \"imagen\"\n // Procesamos imagen de salida\n if(imagename_f != \"Sin Matricula\"){\n msg_s.filename = flow.get(\"path_images\") + imagename_f // extrae el nombre de la imagen de salida del record pendiente de sincronizar\n }\n else{\n msg.filename = flow.get(\"path_images\") + imagename_f + \".jpg\"\n }\n msg_s.tipo = \"imagen_s\"\n \n if (pendientes[indiceActual].nombre_firma != \"No Habilitada\" && pendientes[indiceActual].nombre_firma != \"firma-No Picture\"){\n msg2.filename = flow.get(\"path_firmas\") + pendientes[indiceActual].nombre_firma //extrae el nombre de la firma del record pendiente de sincronizar en caso de existir\n msg2.tipo = \"firma\"\n msg.files = 2 // indica que hay dos archivos a procesar\n node.send([msg, msg_s, msg2])\n }\n else{\n msg.files = 1 // indica que solo esta el archivo de la imagen de la matricula a procesar\n node.send([msg, msg_s, null])\n }\n return null\n}\nreturn null; ", + "outputs": 3, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2660, + "y": 1160, + "wires": [ + [ + "d4a053997921cfc0" + ], + [ + "9eda24ef14d3892c" + ], + [ + "e944fa11afc97160" + ] + ] + }, + { + "id": "a42d3e0da2d9dbda", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "debug 15", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3280, + "y": 1120, + "wires": [] + }, + { + "id": "7b7da2e1e640ca0a", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "1", + "payloadType": "num", + "x": 2090, + "y": 1240, + "wires": [ + [ + "950c8115e364a493" + ] + ] + }, + { + "id": "1387a80aca617b59", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "Sincronizacion de registros anteriores", + "info": "Este grupo de nodos realiza las siguiente acciones:\n1. Extrae de la BD un array con todos los records que aun no han sido sincronizados con el servidor central (sincro = 0)\n2. Extrae del record actual (el que este en el apuntador del indice) los nombres de los archivos de la matricula y de la firma\n3. Lee el contenido de las imagenes de matricula y de firma.\n4. Agrega estas imagenes al contenido del json que se va a enviar al servidor. El cual consta de los campos del record mas las imagenes\n5. Envia el contenido del json al servidor:\n\n - A traves del nodo http / https con un timeout de 5 segundos (la url esta en el archivo de configuracion)\n Si la respuesta del servidor es satisfactoria (Codigo = 200) se procede a marcar en la BD el bit de sincronizacion a 1\n Lo cual indica que ese record ya ha sido sincronizado. Posteriormente se procede a procesar el proximo record del array.\n En caso de timeout (Codigo diferente a 200) se procede a recuperar el proximo record del array para realizarle el\n procedimiento a partir del paso 2. Para diferenciar si es el primer record o es la continuacion del scan del array se utiliza el parametro\n msg.inicio = true (primer record) en caso de que sea false el record es el que se indique en el indice del array actual.\n\n\n - A traves de MQTT con un timeout de 5 segundos para esperar un ACK donde el UUID que envíe debe coincidir con el UUID enviado\n Si el nodo de ACK recibe la confirmacion de la recepcion del UUID correspondiente, entonces se procede a marcar en la BD el bit de sincronizacion a 1\n Lo cual indica que ese record ya ha sido sincronizado. Posteriormente se procede a procesar el proximo record del array.\n En caso que el nodo trigger produzca el time out se procede a borrar la referencia del ultimo uuid (nodo init uuid) la cual sirve para comparar\n con el uuid recibido cuando llegue el ack (esto es necesario hacerlo por si llega un ack retrasado) y se continua con el proceso del proximo record si existe", + "x": 2750, + "y": 1380, + "wires": [] + }, + { + "id": "ca758e5b6e2d880e", + "type": "mqtt in", + "z": "744bdf3a0e08d754", + "g": "fdddf9cbc2fa6622", + "name": "ack", + "topic": "totemautonomo/movimientos/movimiento_ok", + "qos": "1", + "datatype": "auto-detect", + "broker": "3966bb218232f6b3", + "nl": false, + "rap": true, + "rh": 0, + "inputs": 0, + "x": 3130, + "y": 1440, + "wires": [ + [ + "a62dedb280979227", + "6d9ec47623128a2d" + ] + ] + }, + { + "id": "4a2f600ba9ca7a15", + "type": "mqtt out", + "z": "744bdf3a0e08d754", + "g": "fdddf9cbc2fa6622", + "name": "send data", + "topic": "totemautonomo/movimientos/movimiento", + "qos": "2", + "retain": "", + "respTopic": "", + "contentType": "", + "userProps": "", + "correl": "", + "expiry": "", + "broker": "3966bb218232f6b3", + "x": 3280, + "y": 1260, + "wires": [] + }, + { + "id": "a62dedb280979227", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "fdddf9cbc2fa6622", + "name": "rx ACK", + "func": "/*\nAqui recibimos el ack enviado por el programa del servidor que recibe los movimientos. Este programa debe enviar\nel topic:totemautonomo/movimientos/movimiento_ok ; payload: true / false ; uuid: el uuid del movimiento\n*/\n\nif (msg.payload.uuid === flow.get(\"ultimo_movimiento_id\") && msg.payload.movimiento_ok && flow.get(\"ack_old_movimientos\")){\n flow.set(\"ack_old_movimientos\", false) // inicializamos el ack\n flow.set(\"ultimo_movimiento_ok\", true) // indicamos que el ultimo envio ha sido recibido\n flow.set(\"ultimo_movimiento_id\", \"\")\n msg.reset = true // esto es para parar el timeout counter\n return msg\n}\n \nreturn ;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3280, + "y": 1420, + "wires": [ + [ + "b1c81347b429091a", + "1d3c5c1f2e5be4b0" + ] + ] + }, + { + "id": "d5ced8b11f124c67", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "fdddf9cbc2fa6622", + "name": "tx mqtt", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3270, + "y": 1320, + "wires": [] + }, + { + "id": "6d9ec47623128a2d", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "fdddf9cbc2fa6622", + "name": "rx ack", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3270, + "y": 1480, + "wires": [] + }, + { + "id": "1d3c5c1f2e5be4b0", + "type": "trigger", + "z": "744bdf3a0e08d754", + "g": "fdddf9cbc2fa6622", + "name": "timeout", + "op1": "", + "op2": "1", + "op1type": "nul", + "op2type": "num", + "duration": "5", + "extend": false, + "overrideDelay": false, + "units": "s", + "reset": "", + "bytopic": "all", + "topic": "topic", + "outputs": 1, + "x": 3280, + "y": 1360, + "wires": [ + [ + "0a4223f208ebf170", + "b1c81347b429091a" + ] + ] + }, + { + "id": "0a4223f208ebf170", + "type": "change", + "z": "744bdf3a0e08d754", + "g": "fdddf9cbc2fa6622", + "name": "init uuid", + "rules": [ + { + "t": "set", + "p": "ultimo_movimiento_id", + "pt": "flow", + "to": "", + "tot": "str" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 3460, + "y": 1360, + "wires": [ + [] + ] + }, + { + "id": "fc3b9b3c65b9e540", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "fdddf9cbc2fa6622", + "name": "", + "info": "Este grupo se encarga de enviar via mqtt el json correspondiente al contenido del movimiento\ndel vehiculo en la bascula. \nAdicionalmente espera un ACK con un timeout de 5 segundos. Este timeout se consigue con el uso de un nodo\ntrigger configurado en 5 segundos. Este nodo al cabo de 5 segundos encia un mensaje {payload: 1} al nodo\nde procesamiento para que continue el proceso con el proximo record.\nEn caso de recibir el acknoledge por parete del servidor se envia un mensaje de reset al nodo trigger\ncon la finalidad de parar el contador.\nCuando se recibe el ack se borra la referencia con el ultimo uuid para evitar recepciones de ack falsas", + "x": 3080, + "y": 1380, + "wires": [] + }, + { + "id": "9eda24ef14d3892c", + "type": "file in", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "lee imagen_f", + "filename": "filename", + "filenameType": "msg", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 2870, + "y": 1160, + "wires": [ + [ + "89f64c532d3d78e7" + ] + ] + }, + { + "id": "867c5654842a1210", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "mydb": "1c8613764b7f685e", + "name": "Actualiza tabla", + "x": 3720, + "y": 1140, + "wires": [ + [ + "911f1b379de9ba3a" + ] + ] + }, + { + "id": "d144432c1ab94daf", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "mydb": "1c8613764b7f685e", + "name": "Actualiza tabla", + "x": 2420, + "y": 1160, + "wires": [ + [ + "f4c6152e8ed7f5a6", + "3c3867c2a2185f63" + ] + ] + }, + { + "id": "d683e9cccedc561f", + "type": "mqtt in", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "Contratos Update", + "topic": "totemautonomo/contratos/update", + "qos": "1", + "datatype": "utf8", + "broker": "3966bb218232f6b3", + "nl": false, + "rap": true, + "rh": 0, + "inputs": 0, + "x": 2120, + "y": 1700, + "wires": [ + [ + "36b9bf6834913595" + ] + ] + }, + { + "id": "0c144d19f803250f", + "type": "switch", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "Tabla destino", + "property": "payload.tabla", + "propertyType": "msg", + "rules": [ + { + "t": "eq", + "v": "Matriculas", + "vt": "str" + }, + { + "t": "eq", + "v": "Identificadores", + "vt": "str" + }, + { + "t": "eq", + "v": "Matriculas_Identificadores", + "vt": "str" + }, + { + "t": "eq", + "v": "UbicacionesInternas", + "vt": "str" + } + ], + "checkall": "true", + "repair": false, + "outputs": 4, + "x": 2450, + "y": 1700, + "wires": [ + [ + "ac9d1ea0c4c9dfbe" + ], + [ + "e0c3a3c33ed7aaca", + "b9b6a469329290a5" + ], + [ + "76a88549a38a751d" + ], + [ + "1dae889e8697bf15" + ] + ] + }, + { + "id": "105c677a40b6461b", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "✅ Éxito actualización", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": true, + "complete": "true", + "targetType": "full", + "statusVal": "✓ OK", + "statusType": "str", + "x": 3580, + "y": 1640, + "wires": [] + }, + { + "id": "f0f98e25ebd44ac3", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "delete records", + "func": "let tabla = msg.payload.tabla\nlet datos = msg.payload.datos\nlet queries = []\nswitch (msg.payload.tabla){\n case \"Vehiculos\":\n for (let row of datos) {\n const matricula = row.Matricula;\n queries.push(`DELETE FROM ('${tabla}') WHERE Matricula = ('${matricula}')`);\n }\n msg.topic = queries\n break;\n\n case \"Centros\":\n let IdCentro = msg.payload.IdCentro\n msg.topic = `DELETE FROM ('${tabla}') WHERE IdCentro = ('${IdCentro}')`;\n break;\n}\n\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2840, + "y": 1860, + "wires": [ + [] + ] + }, + { + "id": "67e7ede89ac08b87", + "type": "mqtt in", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "Delete a record", + "topic": "totemautonomo/contratos/delete", + "qos": "1", + "datatype": "json", + "broker": "3966bb218232f6b3", + "nl": false, + "rap": true, + "rh": 0, + "inputs": 0, + "x": 2180, + "y": 1860, + "wires": [ + [ + "f0f98e25ebd44ac3" + ] + ] + }, + { + "id": "36b9bf6834913595", + "type": "json", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "", + "property": "payload", + "action": "", + "pretty": false, + "x": 2290, + "y": 1700, + "wires": [ + [ + "0c144d19f803250f" + ] + ] + }, + { + "id": "c7a7b3e8c093ae1e", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "data_tables", + "links": [ + "24855820a3fadbb8" + ], + "x": 2315, + "y": 1760, + "wires": [ + [ + "0c144d19f803250f" + ] + ] + }, + { + "id": "afd34cb6f61a6475", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "debug 37", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3080, + "y": 1620, + "wires": [] + }, + { + "id": "ac9d1ea0c4c9dfbe", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "Matriculas", + "func": "// Verifica que payload sea un array\nif (!Array.isArray(msg.payload.datos) || msg.payload.datos.length === 0) {\n node.warn(\"msg.payload.datos no es un array o está vacío.\");\n return null;\n}\n\n// Función para escapar comillas simples\nconst esc = s => (s ?? \"\").toString().replace(/'/g, \"''\");\n\n// Construimos las tuplas\nflow.set(\"matriculas_size\", msg.payload.datos.length)\nconst valuesArray = msg.payload.datos.map(obj => {\n const matricula = esc(obj.matricula || \"\").trim().toUpperCase();\n const tara = Number(obj.tara) || 0;\n const pma = Number(obj.pma) || 0;\n return `('${matricula}', ${tara}, ${pma})`;\n});\nflow.set(\"matriculas_counter\", valuesArray.length )\n// Query final\n//Borra la tabla actual y si la tabla recibida es diferente de 0 records guarda la nueva tabla, si no pone \"\"\n// finalmente hace un commit\nmsg.topic = `\nSTART TRANSACTION;\nDELETE FROM Matriculas;\n${valuesArray.length ? `\nINSERT INTO Matriculas (matricula, tara, pma)\nVALUES ${valuesArray.join(\",\")};\n` : \"\"}\nCOMMIT;\n`;\nmsg.payload = \"\"\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2810, + "y": 1660, + "wires": [ + [ + "afd34cb6f61a6475", + "7aee3c3ea10fd9a5" + ] + ] + }, + { + "id": "b9b6a469329290a5", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "Identificadores", + "func": "// Verifica que payload sea un array\nif (!Array.isArray(msg.payload.datos) || msg.payload.datos.length === 0) {\n node.warn(\"msg.payload.datos no es un array o está vacío.\");\n return null;\n}\n\n// Función para escapar comillas simples\nconst esc = s => (s ?? \"\").toString().replace(/'/g, \"''\");\n\n// Construimos las tuplas\nflow.set(\"matriculas_size\", msg.payload.datos.length);\n\nconst valuesArray = msg.payload.datos.map(obj => {\n // Si el campo es null, undefined o la cadena \"null\", lo tratamos como NULL\n const toSQL = val => {\n if (val === null || val === undefined || val === \"null\" || val === \"NULL\") {\n return \"NULL\";\n }\n const txt = esc(val.toString().trim());\n return `'${txt}'`;\n };\n\n const id = toSQL(obj.id);\n const id_padre = toSQL(obj.id_padre);\n const nivel = Number(obj.nivel) || 0;\n const descripcion = toSQL(obj.descripcion);\n\n return `(${id}, ${id_padre}, ${nivel}, ${descripcion})`;\n});\n\nflow.set(\"matriculas_counter\", valuesArray.length);\n\n// Query final\n//En esta tabla puede haber varios registros con la misma matricula por lo tanto hacemos solo INSERT INTO ( SIN IGNORE o REPLACE)\n//Ademas estos campos no pueden ser Primary Key ya que sino no puede haber registros con el mismo valor\n// Query final\n//Borra la tabla actual y si la tabla recibida es diferente de 0 records guarda la nueva tabla, si no pone \"\"\n// finalmente hace un commit\nmsg.topic = `\nSTART TRANSACTION;\nDELETE FROM Identificadores;\n${valuesArray.length ? `\nINSERT INTO Identificadores (id, id_padre, nivel, descripcion)\nVALUES ${valuesArray.join(\",\")};\n` : \"\"}\nCOMMIT;\n`;\n\nmsg.payload = \"\";\nreturn msg;\n\n\n\n\n\n/*\n// Verifica que payload sea un array\nif (!Array.isArray(msg.payload.datos) || msg.payload.datos.length === 0) {\n node.warn(\"msg.payload.datos no es un array o está vacío.\");\n return null;\n}\n\n// Función para escapar comillas simples\nconst esc = s => (s ?? \"\").toString().replace(/'/g, \"''\");\n\n// Construimos las tuplas\nflow.set(\"matriculas_size\", msg.payload.datos.length)\n\nconst valuesArray = msg.payload.datos.map(obj => {\n const id = esc(obj.id || \"\").trim().toUpperCase();\n const id_padre = esc(obj.id_padre || \"\").trim().toUpperCase();\n const nivel = Number(obj.nivel) || 0;\n const descripcion = `'${esc(obj.descripcion)}'`; // ¡entre comillas y escapado!\n return `('${id}', '${id_padre}', ${nivel}, ${descripcion})`;\n});\nflow.set(\"matriculas_counter\", valuesArray.length )\n// Query final\n\nmsg.topic = `\nINSERT INTO Identificadores (id, id_padre, nivel, descripcion)\nVALUES ${valuesArray.join(\",\")};\n`;\nmsg.payload = \"\"\nreturn msg;\n*/\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2820, + "y": 1700, + "wires": [ + [ + "afd34cb6f61a6475", + "7aee3c3ea10fd9a5" + ] + ] + }, + { + "id": "76a88549a38a751d", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "Matriculas_Identificadores", + "func": "// Verifica que payload sea un array\nif (!Array.isArray(msg.payload.datos) || msg.payload.datos.length === 0) {\n node.warn(\"msg.payload.datos no es un array o está vacío.\");\n return null;\n}\n\n// Función para escapar comillas simples\nconst esc = s => (s ?? \"\").toString().replace(/'/g, \"''\");\n\n// Construimos las tuplas\nflow.set(\"matriculas_size\", msg.payload.datos.length)\nconst valuesArray = msg.payload.datos.map(obj => {\n const id = esc(obj.id || \"\").trim().toUpperCase();\n const matricula = esc(obj.matricula || \"\").trim().toUpperCase();\n return `('${matricula}','${id}')`;\n});\nflow.set(\"matriculas_counter\", valuesArray.length )\n// Query final\n//En esta tabla puede haber varios registros con la misma matricula por lo tanto hacemos solo INSERT INTO ( SIN IGNORE o REPLACE)\n//Ademas estos campos no pueden ser Primary Key ya que sino no puede haber registros con el mismo valor\n// Query final\n//Borra la tabla actual y si la tabla recibida es diferente de 0 records guarda la nueva tabla, si no pone \"\"\n// finalmente hace un commit\nmsg.topic = `\nSTART TRANSACTION;\nDELETE FROM Matriculas_Identificadores;\n${valuesArray.length ? `\nINSERT INTO Matriculas_Identificadores (matricula, id)\nVALUES ${valuesArray.join(\",\")};\n` : \"\"}\nCOMMIT;\n`;\nmsg.payload = \"\"\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2850, + "y": 1740, + "wires": [ + [ + "afd34cb6f61a6475", + "7aee3c3ea10fd9a5" + ] + ] + }, + { + "id": "e0c3a3c33ed7aaca", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "debug 36", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2620, + "y": 1620, + "wires": [] + }, + { + "id": "7aee3c3ea10fd9a5", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "mydb": "1c8613764b7f685e", + "name": "Actualiza tablas", + "x": 3260, + "y": 1680, + "wires": [ + [ + "105c677a40b6461b" + ] + ] + }, + { + "id": "aa870f8d73e4f504", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "delete_items_in", + "links": [ + "6a2b1526103aa3b0" + ], + "x": 3135, + "y": 1820, + "wires": [ + [ + "7aee3c3ea10fd9a5" + ] + ] + }, + { + "id": "1d9e5518c4689f75", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "8013dc387b26381b", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "0236MWK", + "payloadType": "str", + "x": 2120, + "y": 2040, + "wires": [ + [ + "b76ed973b1299bb9" + ] + ] + }, + { + "id": "a1ac4bf3b8c14b21", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "8013dc387b26381b", + "name": "entrada matricula", + "links": [ + "6703fb916aef8f4b" + ], + "x": 2345, + "y": 2080, + "wires": [ + [ + "9a65971441f7579f", + "ceba75f09293b8f4" + ] + ] + }, + { + "id": "7f282cc6ba963043", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "8013dc387b26381b", + "name": "salida resultados", + "mode": "link", + "links": [ + "cb6c28fe05c25229" + ], + "x": 2915, + "y": 2020, + "wires": [] + }, + { + "id": "c470404997683059", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "8013dc387b26381b", + "name": "", + "info": "Este conjunto de nodos revisa la existencia de una matricula en base de datos. Para ello recibe la matricula en msg.paylad.\nHace entonces un select en la BD para buscar la matricula recibida. En caso que la BD devuelva un array vacio no se activa\nel flag correspondiente a matricula OK por lo que aparecera un mensaje en la pantalla indicando al usuario que esa matricula\nno esta registrada. \nEn caso de existrir la matricula en la BD entonces el proceso continua y lo que hacemos es pasar la matricula al grupo de nodos\nque busca los datos correspondientes a los posibles contratos para dicha matricula y se elabora un menu para que el usuario\nseleccione en la pantalla los valores adecuados de manera a poder finalizar la pesada.\n", + "x": 2180, + "y": 2100, + "wires": [] + }, + { + "id": "008f41b960a6d13f", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "8013dc387b26381b", + "name": "debug 10", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2720, + "y": 2000, + "wires": [] + }, + { + "id": "9a65971441f7579f", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "8013dc387b26381b", + "name": "debug 11", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2500, + "y": 2100, + "wires": [] + }, + { + "id": "9c65121589e6726e", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "8013dc387b26381b", + "name": "debug 19", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2860, + "y": 2100, + "wires": [] + }, + { + "id": "b76ed973b1299bb9", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "8013dc387b26381b", + "name": "check matricula", + "func": "let matricula = msg.payload\nmsg.matricula = matricula\n//flow.set(\"matricula\", matricula)\n//Solicitamos un solo record donde aparezca registrada la matricula (tabla de Vehiculos) y revisamos si se encuentra en transito (tabla movimientos)\n// en caso que no este registrada la matricula, la BD nos entrega un record con los parametros en cero o en null\nmsg.topic = `\nWITH input AS (SELECT TRIM('${matricula}') AS MatriculaInput)\nSELECT\n CASE WHEN ma.matricula IS NOT NULL THEN 1 ELSE 0 END AS registrada,\n CAST(EXISTS(SELECT 1 FROM movimientos m JOIN input i ON UPPER(TRIM(m.matricula)) = UPPER(i.MatriculaInput) LIMIT 1) AS INTEGER) AS transito,\n ma.tara, ma.pma, (SELECT CASE WHEN COUNT(*)=1 THEN MAX(idf.descripcion) ELSE NULL END FROM Matriculas_Identificadores mi JOIN input i ON UPPER(TRIM(mi.matricula)) = UPPER(i.MatriculaInput) LEFT JOIN Identificadores idf ON idf.id = mi.id) AS descripcion,\n (SELECT COUNT(*) FROM Matriculas_Identificadores mi JOIN input i ON UPPER(TRIM(mi.matricula)) = UPPER(i.MatriculaInput)) AS repeticion_matricula\nFROM input i\nLEFT JOIN Matriculas ma ON UPPER(TRIM(ma.matricula)) = UPPER(i.MatriculaInput)\nLIMIT 1;\n`;\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2300, + "y": 2040, + "wires": [ + [ + "ceba75f09293b8f4" + ] + ] + }, + { + "id": "a16351341e1a09da", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "8013dc387b26381b", + "name": "check results", + "func": "let result = msg.payload\nflow.set(\"matricula_revisada\", true)\nmsg.payload = {}\nmsg.topic = \"\"\n//msg.payload.totem_id = flow.get(\"totem_id\")\n//msg.payload.numero_vial = flow.get(\"numero_vial\")\n\nif (result.length > 0){ // Tenemos al menos un record de respuesta de la BD\n if (result[0].registrada == 1){\n msg.payload.registrada = !!result[0].registrada // la doble negacion convierte a booleano ( true / false)\n msg.payload.pma = result[0].pma\n msg.payload.tara = result[0].tara\n msg.payload.transito = !!result[0].transito\n msg.payload.matricula = msg.matricula\n let msgOut = RED.util.cloneMessage(msg);\n msg.payload.repeticion_matricula = result[0].repeticion_matricula\n msg.payload.aviso_identificacion = false // por defecto comenzamos con false\n if (result[0].repeticion_matricula == 1){\n let descripcion = result[0].descripcion || \"\" // en caso de null o undefined ponemos cadena vacia\n msg.payload.aviso_identificacion = descripcion.includes(\",\"); // esto es true si la descripcion tiene una \",\" lo cual indica que es para un QR o RFID\n }\n if (!msg.payload.transito && !msg.payload.aviso_identificacion){ // si esta en transito o si NO hay que sacar aviso de identificacion, buscamos menu en BD\n setTimeout(function () {\n node.send([null, msgOut]) //genera lista de opciones para presentar el menu en pantalla\n }, 1000)\n }\n }\n else{\n msg.payload.registrada = result[0].registrada\n }\n}\nelse{\n msg.payload.respuestaBD = \"ningun record\"\n}\nmsg.payload.totem_id = msg.totem_id\nmsg.payload.numero_vial = msg.numero_vial\nreturn ([msg, null]);\n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2750, + "y": 2040, + "wires": [ + [ + "22f142c6e6d19334", + "7f282cc6ba963043" + ], + [ + "9c65121589e6726e", + "dcd65882403b8019" + ] + ] + }, + { + "id": "22f142c6e6d19334", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "8013dc387b26381b", + "name": "debug 32", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2880, + "y": 1980, + "wires": [] + }, + { + "id": "ceba75f09293b8f4", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "8013dc387b26381b", + "mydb": "1c8613764b7f685e", + "name": "check matricula", + "x": 2540, + "y": 2040, + "wires": [ + [ + "a16351341e1a09da", + "008f41b960a6d13f" + ] + ] + }, + { + "id": "dcd65882403b8019", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "name": "genera lista", + "func": "// Asegura que recibimos la matrícula\nconst matricula = msg.matricula;\nif (!matricula) {\n node.error(\"No se recibió matrícula en msg.payload o msg.matricula\");\n return null;\n}\n\nmsg.topic = `\nWITH\nseeds AS (\n SELECT mi.id AS id_root\n FROM Matriculas_Identificadores mi\n WHERE UPPER(TRIM(mi.matricula)) = UPPER(TRIM('${matricula}'))\n)\nSELECT\n 0 AS nivel,\n i.descripcion,\n i.id,\n NULL AS id_padre\nFROM Identificadores i\nJOIN seeds s ON i.id = s.id_root\n\nUNION ALL\n\nSELECT\n 1 AS nivel,\n c.descripcion,\n c.id,\n c.id_padre\nFROM Identificadores c\nJOIN seeds s ON c.id_padre = s.id_root\n\nORDER BY nivel, id;\n`;\n\n/* Esto es lo que estaba con Sqlite\nmsg.topic = `\nWITH\nseeds AS (\n SELECT mi.id AS id_root\n FROM Matriculas_Identificadores mi\n WHERE TRIM(mi.matricula) = TRIM('${matricula}') COLLATE NOCASE\n)\nSELECT\n 0 AS nivel,\n i.descripcion,\n i.id,\n NULL AS id_padre\nFROM Identificadores i\nJOIN seeds s ON i.id = s.id_root\n\nUNION ALL\n\nSELECT\n 1 AS nivel,\n c.descripcion,\n c.id,\n c.id_padre\nFROM Identificadores c\nJOIN seeds s ON c.id_padre = s.id_root\n\nORDER BY nivel, id;\n`;\n*/\nreturn msg;\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/* codigo anterior\n// Armamos el SELECT combinado\nmsg.topic = `\n-- Nivel 0: Centros asociados a la matrícula\nSELECT \n 0 AS nivel,\n c.Nombre AS descripcion,\n c.IdCentro AS id,\n NULL AS id_padre\nFROM Matriculas_Centros mc\nJOIN Centros c ON mc.IdCentro = c.IdCentro\nWHERE mc.Matricula = '${matricula}'\n\nUNION ALL\n\n-- Nivel 1: Servicios asociados a los centros de esa matrícula\nSELECT \n 1 AS nivel,\n s.Descripcion AS descripcion,\n cs.IdCentroServicio AS id,\n cs.IdCentro AS id_padre\nFROM Matriculas_Centros mc\nJOIN Centros_Servicios cs ON mc.IdCentro = cs.IdCentro\nJOIN Servicios s ON cs.IdServicio = s.IdServicio\nWHERE mc.Matricula = '${matricula}';\n`;\n\nreturn msg;\n*/", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3270, + "y": 2020, + "wires": [ + [ + "4f214eb96e82b18b", + "dfdc2a0fceee62b1" + ] + ] + }, + { + "id": "39ce9cfd3bee5c07", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "4265HGS", + "payloadType": "str", + "x": 3100, + "y": 2060, + "wires": [ + [ + "dcd65882403b8019" + ] + ] + }, + { + "id": "eb466c2601840e18", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "name": "genera json", + "func": "\nif (msg.payload.length > 0) { // tenemos un menu asociado a la matricula\n if (msg.payload.length > 1) {\n msg.metadatos = {\n //numero_vial: flow.get(\"numero_vial\"),\n //totem_id: flow.get(\"totem_id\"),\n encabezado_lista: flow.get(\"encabezado_lista\") ?? \"Seleccione opción\",\n button: \"button1\",\n button1: \"COMPLETAR INFO\",\n menu_inicio: true,\n lista: msg.payload\n };\n msg.payload = msg.metadatos\n msg.metadatos = \"\"\n msg.topic = \"\"\n msg.payload.totem_id = msg.totem_id\n msg.payload.numero_vial = msg.numero_vial\n node.send([msg, null])\n }\n}\nelse{\n //node.warn(\"array size <= 0\")\n msg.payload = {\n numero_vial: msg.numero_vial,\n totem_id: msg.totem_id,\n registrada: false,\n \n };\n \n node.send([null, msg])\n}\nreturn;", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3710, + "y": 2020, + "wires": [ + [ + "3605e72173f34f32", + "4526132d69e6c210" + ], + [ + "ce3029f2688a0aba", + "6ef121d9eb28bd91" + ] + ] + }, + { + "id": "3605e72173f34f32", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "name": "debug 21", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3900, + "y": 1980, + "wires": [] + }, + { + "id": "4526132d69e6c210", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "name": "salida lista", + "mode": "link", + "links": [ + "cb6c28fe05c25229" + ], + "x": 3895, + "y": 2020, + "wires": [] + }, + { + "id": "b49f7a7bb3272707", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "name": "Generacion de Lista", + "links": [], + "x": 3145, + "y": 2100, + "wires": [ + [ + "dcd65882403b8019" + ] + ] + }, + { + "id": "96d638d71a74e652", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "name": "", + "info": "Este conjunto de nodos recibe primeramente la matricula a buscar y elabora un select con los join y union adecuados \npara buscar en las diferentes tablas los posibles valores asociados a la matricula para entonces elaborar un json\ncon la lista de opciones que se van a presentar al usuario. Este json se injecta en el nodo de entrada del sistema.\n", + "x": 3320, + "y": 2100, + "wires": [] + }, + { + "id": "ce3029f2688a0aba", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "name": "link out 11", + "mode": "link", + "links": [ + "cb6c28fe05c25229" + ], + "x": 3895, + "y": 2060, + "wires": [] + }, + { + "id": "6ef121d9eb28bd91", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "name": "debug 16", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3900, + "y": 2100, + "wires": [] + }, + { + "id": "74375df4238da5a5", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "name": "debug 8", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3680, + "y": 1980, + "wires": [] + }, + { + "id": "4f214eb96e82b18b", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "mydb": "1c8613764b7f685e", + "name": "get menu", + "x": 3460, + "y": 2020, + "wires": [ + [ + "eb466c2601840e18", + "74375df4238da5a5" + ] + ] + }, + { + "id": "dfdc2a0fceee62b1", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "cb22a118da74ca79", + "name": "debug 39", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3420, + "y": 1980, + "wires": [] + }, + { + "id": "9b47806ebf017d2c", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "f968efec7b96ec13", + "name": "check BD", + "func": "if (msg.tipo == \"rfid\") {\n let tarjeta = msg.payload\n msg.topic = `SELECT\n CASE WHEN mi.id IS NOT NULL THEN TRUE ELSE FALSE END AS autorizado,\n mi.matricula AS matricula,\n i.descripcion AS descripcion,\n i.nivel AS nivel\nFROM (SELECT '${tarjeta}' AS id) AS input\nLEFT JOIN Matriculas_Identificadores mi\n ON TRIM(mi.id) = TRIM(input.id)\nLEFT JOIN Identificadores i\n ON TRIM(i.id) = TRIM(input.id)\nLIMIT 1;`;\n}\nelse if (msg.tipo == \"qr\") {\n let qr = msg.payload\n msg.topic = `\nSELECT\n CASE WHEN mi.id IS NOT NULL THEN TRUE ELSE FALSE END AS autorizado,\n mi.matricula AS matricula,\n i.descripcion AS descripcion,\n i.nivel AS nivel\nFROM (SELECT '${qr}' AS id) AS input\nLEFT JOIN Matriculas_Identificadores mi\n ON TRIM(mi.id) = TRIM(input.id)\nLEFT JOIN Identificadores i\n ON TRIM(i.id) = TRIM(input.id)\nLIMIT 1;`;\n}\nreturn msg;\n\n\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2240, + "y": 2360, + "wires": [ + [ + "57fd039f5dadf4fb", + "8558335b01c7e441" + ] + ] + }, + { + "id": "7269e082a5989305", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "f968efec7b96ec13", + "name": "qr", + "props": [ + { + "p": "payload" + }, + { + "p": "tipo", + "v": "qr", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "546DA2A4-B07B-482D-B98F-B4EB0FFABCAE", + "payloadType": "str", + "x": 2090, + "y": 2360, + "wires": [ + [ + "9b47806ebf017d2c" + ] + ] + }, + { + "id": "8e56735bbb37a598", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "f968efec7b96ec13", + "name": "check qr rfid", + "links": [ + "4e1f0ff54eef4e7b" + ], + "x": 2095, + "y": 2320, + "wires": [ + [ + "9b47806ebf017d2c" + ] + ] + }, + { + "id": "ea115b9b38cdaea1", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "f968efec7b96ec13", + "name": "check results", + "func": " msg.topic = \"\"\n let descripcion = \"\"\n let partes = []\nif (msg.tipo == \"rfid\") {\n let result = msg.payload\n msg.payload = {\n numero_vial: msg.numero_vial,\n totem_id: msg.totem_id,\n rfid_revisado: true,\n rfid_verificado: false\n };\n if (result.length > 0) { // Tarjeta tiene contrato\n msg.payload.rfid_verificado = result[0].autorizado\n descripcion = result[0].descripcion || \"\"\n partes = descripcion.split(\",\");\n msg.payload.cliente = (partes[0] || \"\").trim();\n msg.payload.residuo = (partes[1] || \"\").trim();\n msg.payload.matricula = result[0].matricula || \"\"\n msg.payload.nivel = result[0].nivel || 0\n }\n \n}\nelse if (msg.tipo == \"qr\") {\n let result = msg.payload\n msg.payload = {\n numero_vial: msg.numero_vial,\n totem_id: msg.totem_id,\n qr_revisado: true,\n qr_verificado: false\n };\n if (result.length > 0) { // QR tiene contrato\n msg.payload.qr_verificado = result[0].autorizado\n descripcion = result[0].descripcion || \"\"\n partes = descripcion.split(\",\");\n msg.payload.cliente = (partes[0] || \"\").trim();\n msg.payload.residuo = (partes[1] || \"\").trim();\n //msg.payload.matricula = result[0].matricula || \"\" // no enviamos matricula cuando se trata de QR, ya que el QR puede estar asociado a varias matriculas\n msg.payload.nivel = result[0].nivel || 0\n \n }\n}\nreturn ([msg, null]);", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2750, + "y": 2360, + "wires": [ + [ + "9bd381438eec96eb", + "ed04082db0d29b5b" + ], + [] + ] + }, + { + "id": "ed04082db0d29b5b", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "f968efec7b96ec13", + "name": "salida resultados", + "mode": "link", + "links": [ + "cb6c28fe05c25229" + ], + "x": 2895, + "y": 2320, + "wires": [] + }, + { + "id": "52c76e5f8d275432", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "f968efec7b96ec13", + "name": "rfid", + "props": [ + { + "p": "payload" + }, + { + "p": "tipo", + "v": "rfid", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "1983696218", + "payloadType": "str", + "x": 2090, + "y": 2400, + "wires": [ + [ + "9b47806ebf017d2c" + ] + ] + }, + { + "id": "57fd039f5dadf4fb", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "f968efec7b96ec13", + "name": "debug 12", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2380, + "y": 2420, + "wires": [] + }, + { + "id": "8558335b01c7e441", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "f968efec7b96ec13", + "mydb": "1c8613764b7f685e", + "name": "check QR / Tarjeta", + "x": 2530, + "y": 2360, + "wires": [ + [ + "ea115b9b38cdaea1", + "7ced20377dba5074" + ] + ] + }, + { + "id": "a8e9253184936edf", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "0a78f1a176dd7ad5", + "name": "Matriculas", + "func": "\nmsg.url = `api/Totem/SeleccionarMatriculasTotem?token=${flow.get(\"tokenapi\")}&idCentroProveedor=${flow.get(\"IdCentroProveedor\") }`\nmsg.url = flow.get(\"url_API\") + msg.url \nmsg.tabla = \"Matriculas\"\nreturn msg;\n\n\n//https://pre-urbgesbas.urbaser.com/WebService/api/Totem/SeleccionarMatriculasTotem?token=FDEB4C00-5F58-4C2D-AD80-9E7064D8FC4E&idCentroProveedor=A1ADAC83-AEFA-4E1F-8944-56722AC43F4F", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2450, + "y": 2600, + "wires": [ + [ + "e87018791b56046f" + ] + ] + }, + { + "id": "24855820a3fadbb8", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "0a78f1a176dd7ad5", + "name": "link out 3", + "mode": "link", + "links": [ + "c7a7b3e8c093ae1e" + ], + "x": 2985, + "y": 2640, + "wires": [] + }, + { + "id": "20976aa7738e9650", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "0a78f1a176dd7ad5", + "name": "function 2", + "func": "let datos = msg.payload\nlet tabla = msg.tabla\nmsg = {payload:{}}\nmsg.payload.datos = datos\nmsg.payload.tabla = tabla\nif (typeof msg.payload.datos === \"string\") {\n try {\n // Convierte el texto a array\n msg.payload.datos = JSON.parse(msg.payload.datos);\n } catch (err) {\n node.error(\"Error al parsear msg.payload.datos: \" + err.message);\n return null;\n }\n}\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2840, + "y": 2640, + "wires": [ + [ + "ae259a894fbedeb4", + "24855820a3fadbb8" + ] + ] + }, + { + "id": "a091c51915be9f0a", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "0a78f1a176dd7ad5", + "name": "Identificadores", + "func": "\nmsg.url = `api/Totem/SeleccionarIdentificadoresTotem?token=${flow.get(\"tokenapi\")}&idCentroProveedor=${flow.get(\"IdCentroProveedor\")}`\nmsg.url = flow.get(\"url_API\") + msg.url\nmsg.tabla = \"Identificadores\"\nreturn msg;\n\n//https://pre-urbgesbas.urbaser.com/WebService/api/Totem/SeleccionarIdentificadoresTotem?token=FDEB4C00-5F58-4C2D-AD80-9E7064D8FC4E&idCentroProveedor=A1ADAC83-AEFA-4E1F-8944-56722AC43F4F", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2440, + "y": 2680, + "wires": [ + [ + "e87018791b56046f" + ] + ] + }, + { + "id": "9d2b6d3b968d3fcf", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "0a78f1a176dd7ad5", + "name": "Matriculas_Identificadores", + "func": "\nmsg.url = `api/Totem/SeleccionarMatriculasIdentificadoresTotem?token=${flow.get(\"tokenapi\")}&idCentroProveedor=${flow.get(\"IdCentroProveedor\")}`\nmsg.url = flow.get(\"url_API\") + msg.url\nmsg.tabla = \"Matriculas_Identificadores\"\nreturn msg;\n\n//https://pre-urbgesbas.urbaser.com/WebService/api/Totem/SeleccionarMatriculasIdentificadoresTotem?token=FDEB4C00-5F58-4C2D-AD80-9E7064D8FC4E&idCentroProveedor=A1ADAC83-AEFA-4E1F-8944-56722AC43F4F\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2410, + "y": 2640, + "wires": [ + [ + "e87018791b56046f" + ] + ] + }, + { + "id": "c95d9408ac6b1595", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "0a78f1a176dd7ad5", + "name": "Select", + "func": "switch(msg.payload){\n case \"Matriculas\":\n node.send([{payload:1}, null, null])\n break;\n case \"Matriculas_Identificadores\":\n node.send([null, { payload: 1 }, null])\n break;\n case \"Identificadores\":\n node.send([null, null, { payload: 1 }])\n break;\n case \"UbicacionesInternas\":\n node.send([null, null, null, { payload: 1 }])\n break;\n}\nreturn ;", + "outputs": 4, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2150, + "y": 2640, + "wires": [ + [ + "a8e9253184936edf" + ], + [ + "9d2b6d3b968d3fcf" + ], + [ + "a091c51915be9f0a" + ], + [ + "eca4e2e077d64f16" + ] + ] + }, + { + "id": "5cd2d2a7d0b81e81", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "0a78f1a176dd7ad5", + "name": "pedir_tablas", + "links": [], + "x": 2035, + "y": 2620, + "wires": [ + [ + "c95d9408ac6b1595" + ] + ] + }, + { + "id": "e87018791b56046f", + "type": "https-node", + "z": "744bdf3a0e08d754", + "g": "0a78f1a176dd7ad5", + "name": "https request", + "method": "GET", + "ret": "txt", + "paytoqs": "ignore", + "url": "", + "tls": "", + "persist": false, + "proxy": "", + "authType": "", + "senderr": false, + "x": 2670, + "y": 2640, + "wires": [ + [ + "20976aa7738e9650" + ] + ] + }, + { + "id": "ae259a894fbedeb4", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "0a78f1a176dd7ad5", + "name": "debug 38", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3000, + "y": 2600, + "wires": [] + }, + { + "id": "0b16f0b9cb1c5ab9", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "envia a API", + "func": "if(flow.get(\"movimientosxapi\")){\n //msg.payload contiene el movimiento a enviar\n msg.url = flow.get(\"url_API\")\n msg.url = msg.url + `api/Totem/InsertarMovimientos?token=${flow.get(\"tokenapi\")}`\n //msg.requestTimeout = \"20000\"\n node.send([msg, null, null])\n \n}\n\nelse if (flow.get(\"movimientosxmqtt\") ){\n //msg.payload contiene el movimiento a enviar\n node.warn(\"tipoenvio = \" + msg.payload.tipoenvio)\n flow.set(\"ultimo_movimiento_ok\", false)\n flow.set(\"ultimo_movimiento_id\", msg.payload.uuid) \n node.send([null, msg, { payload: msg.payload.uuid }])\n}\nreturn ;\n\n//\"https://pre-urbgesbas.urbaser.com/WebService/api/Totem/InsertarMovimientos?token=FDEB4C00-5F58-4C2D-AD80-9E7064D8FC4E\"", + "outputs": 3, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2150, + "y": 560, + "wires": [ + [ + "6455539b9fac2063", + "08bf0c9897d6e815" + ], + [ + "e36bb58515b89053", + "7ef59a8ac4d8a7f3" + ], + [ + "ac94ab2b132fab52" + ] + ] + }, + { + "id": "c858b37cb0eeba4f", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "API", + "links": [ + "b1b9ac8e8b543dbb" + ], + "x": 2045, + "y": 560, + "wires": [ + [ + "0b16f0b9cb1c5ab9" + ] + ] + }, + { + "id": "e192c564f2050e94", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "debug 3", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2680, + "y": 500, + "wires": [] + }, + { + "id": "1bdaeab49913082e", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "debug 4", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3340, + "y": 540, + "wires": [] + }, + { + "id": "b8029f9a251a232e", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "debug 5", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3360, + "y": 620, + "wires": [] + }, + { + "id": "fd9524c0921c0990", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "debug 6", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3120, + "y": 580, + "wires": [] + }, + { + "id": "8778197111d2ab0d", + "type": "exec", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "command": "", + "addpay": "payload", + "append": "", + "useSpawn": "false", + "timer": "", + "winHide": false, + "oldrc": false, + "name": "delete files", + "x": 3350, + "y": 680, + "wires": [ + [ + "54bb5d92c06ce75c" + ], + [], + [] + ] + }, + { + "id": "d47200e5f67c772a", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "UPDATE SINCRO", + "func": "\n\nlet sincro = true\nif (flow.get(\"movimientosxapi\")) {\n //let lastmsg = JSON.parse(msg) // enviado por el servidor como respuesta. Debe enviar el IdMovimiento que hemos sincronizado para procesarlo\n let lastmsg = msg\n \n node.warn(\"idmovimiento = \" + lastmsg.payload.IdMovimiento)\n \n msg = {}\n msg.tipo = \"sincro\";\n //node.send([null, lastmsg])\n if (lastmsg.statusCode === 200 && lastmsg.payload.Resultado) {\n //marcamos este record como enviado al API.\n msg.topic = `UPDATE movimientos_cerrados SET sincro = ${sincro} WHERE uuid = '${lastmsg.payload.IdMovimiento}'; `.trim(); // trim para eliminar espacios en blanco en los extremos\n msg.payload = \"\";\n return [{ payload: 1 }, msg];\n } \n else {\n node.warn(`Error al enviar UUID ${lastmsg.uuid}, código HTTP: ${lastmsg.statusCode}`);\n return [{ payload: 1 }, null];\n }\n}\nif (flow.get(\"movimientosxmqtt\")) {\n if (flow.get(\"ultimo_movimiento_ok\")) {\n msg.topic = `UPDATE movimientos_cerrados SET sincro = ${sincro} WHERE uuid = '${msg.payload.uuid}'`;\n msg.payload = \"\";\n return [{ payload: 1 }, msg];\n }\n else {\n node.warn(`Error al enviar UUID ${msg.payload.uuid} via MQTT`);\n return [{ payload: 1 }, null];\n }\n}\n\n\nreturn \n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2690, + "y": 540, + "wires": [ + [], + [ + "732068c66d8e0a25", + "10affb7590fa95f1" + ] + ] + }, + { + "id": "b4caaec341e453e0", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "send data", + "func": "// msg.payload viene del SELECT o del UPDATE previo (array de filas)\nif(msg.tipo == \"check\"){ //revisamos la BD para detectar los archivos que ya estan sincronizados\n const rows = msg.payload;\n const filesToDelete = [];\n const uuids = [];\n\n // 1) Recorremos todas las filas y recopilamos rutas de los archivos y uuids que han sido sincronizados\n rows.forEach(row => {\n if (row.sincro == 1) {\n // si tiene foto válida, la añadimos\n if (row.nombre_imagen_i && row.nombre_imagen_i !== \"Sin Matricula\") {\n filesToDelete.push(flow.get(\"path_images\") + row.nombre_imagen_i);\n }\n if (row.nombre_imagen_f && row.nombre_imagen_f !== \"Sin Matricula\") {\n filesToDelete.push(flow.get(\"path_images\") + row.nombre_imagen_f);\n }\n // si tiene firma válida, la añadimos\n if (row.nombre_firma && row.nombre_firma !== \"No Habilitada\") {\n filesToDelete.push(flow.get(\"path_firmas\") + row.nombre_firma);\n }\n // guardamos el uuid para luego borrar el registro en BD\n uuids.push(row.uuid);\n }\n });\n\n // 2) Construimos el DELETE SQL (solo si hay uuids)\n let deleteSQL = \"\";\n if (uuids.length > 0) {\n // Formamos una lista: 'uuid1','uuid2',...\n const inList = uuids.map(u => `'${u}'`).join(\",\"); // el map nos transforma el uuid agregandole dos ' simple y luego el join hace una cadena con los items separados por \",\"\n deleteSQL = `DELETE FROM movimientos_cerrados WHERE uuid IN (${inList});`; //este es el sql para borrar los records que contengan esos uuid \n\n\n // 3) Preparamos el mensaje para la SALIDA 1 (SQLite)\n const msgDelete = {}\n msgDelete.payload = \"\"\n msgDelete.topic = deleteSQL\n\n // 4) Preparamos el mensaje para la SALIDA 2 (Exec) para borra los archivos de imagenes y de firmas\n\n let msgExec = {};\n if (filesToDelete.length > 0) {\n\n // Genera la cadena de comando\n msgExec.payload = `rm -f ${filesToDelete.map(f => `\"${f}\"`).join(\" \")}`;\n return [msgDelete, msgExec, null, null];\n }\n else {\n return [msgDelete, null, null, null];\n }\n\n\n }\n}\nelse if (msg.tipo == \"api\") { // caso de envio de movimientos a cliente de nuestra API local\n return [null, null, msg, null];\n}\nelse if (msg.tipo == \"sincro\") { // caso de envio del movimiento actual, Ya fue enviado y sincronizado. No hay mas que hacer\n return [null, null, null];\n}\nreturn\n", + "outputs": 3, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3120, + "y": 640, + "wires": [ + [ + "1bdaeab49913082e", + "6a2b1526103aa3b0" + ], + [ + "8778197111d2ab0d", + "b8029f9a251a232e" + ], + [ + "49bed5a59768fbef" + ] + ] + }, + { + "id": "d19507c7bbe4bb32", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "UPDATE BD y Files", + "func": "const dias = Number(flow.get(\"dias_en_BD\")) || 2\nmsg.topic = `SELECT uuid, nombre_imagen_i, nombre_imagen_f, nombre_firma, sincro FROM movimientos_cerrados WHERE sincro = 1 AND ts_cierre <= UNIX_TIMESTAMP(NOW() - INTERVAL ${dias} DAY);`\nmsg.tipo = \"check\";\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2710, + "y": 920, + "wires": [ + [ + "10affb7590fa95f1", + "65b220fc46d55c96" + ] + ] + }, + { + "id": "e2fca1c2499902fa", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "1", + "payloadType": "num", + "x": 2510, + "y": 920, + "wires": [ + [ + "d19507c7bbe4bb32" + ] + ] + }, + { + "id": "6a2b1526103aa3b0", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "delete items", + "mode": "link", + "links": [ + "aa870f8d73e4f504" + ], + "x": 3345, + "y": 580, + "wires": [] + }, + { + "id": "26f04e07495817eb", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "read records", + "func": "/*\nCon esta funcion seleccionamos los records que van a ser enviados via el nodo http response\n*/\n\nlet tipo = msg.req.query.tipo || \"todos\";\n\n// Construimos SQL dinámico\nlet sql = \"\";\nswitch (tipo) {\n case \"no_sincro\":\n sql = \"SELECT * FROM movimientos_cerrados WHERE sincro = 0\";\n break;\n case \"sincro\":\n sql = \"SELECT * FROM movimientos_cerrados WHERE sincro = 1\";\n break;\n case \"todos\":\n default:\n sql = \"SELECT * FROM movimientos_cerrados\";\n break;\n}\nmsg.tipo = \"api\";\nmsg.topic = sql;\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2730, + "y": 980, + "wires": [ + [ + "10affb7590fa95f1" + ] + ] + }, + { + "id": "d5ce4cb6b3abd807", + "type": "http in", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "API movimientos", + "url": "/api/movimientos", + "method": "get", + "upload": false, + "swaggerDoc": "", + "x": 2480, + "y": 980, + "wires": [ + [ + "26f04e07495817eb" + ] + ] + }, + { + "id": "49bed5a59768fbef", + "type": "http response", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "respuesta API", + "statusCode": "", + "headers": {}, + "x": 3360, + "y": 740, + "wires": [] + }, + { + "id": "a58ecf0f246aff6a", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "link out 1", + "mode": "link", + "links": [ + "34bf8ec19b0e78a7" + ], + "x": 2855, + "y": 520, + "wires": [] + }, + { + "id": "4e6d5242d6531d79", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "Envio de movimiento y actualizacion BD", + "info": "Este grupo de nodos efectua fundamentalmente tres funciones:\n1. Envia el movimiento al servidor al salir el camion de la bascula cambiando el bit de sincronizacion\n de ese record en la BD en caso de exito en el envio. Eso lo hacen los nodos: \"envia API\", \"Enviar a API\", \"UPDATE SINCRO\" y \"SQLITE_TOTEM\",\n y los nodos MQTT involucrados con rx ACK.\n2. Diariamente a una hora determinada extrae un array de la BD con los records que tienen en la BD mas de una cierta cantidad\n de dias (configurable) y los elimina de la BD si estos ya han sido sincronizados. Adicionalmente borra tambien \n del sistema de archivos las imagenes correspondientes a las matriculas y firmas asociadas a dichos records.\n Esta funcion la hacen los nodos \"UPDATE BD y Files\", \"SQLITE_TOTEM\", \"send data\" y \"delete files\".\n3. Sirve un API para proporcionar a un cliente http un json con los records que ya han sido sincronizados pero que todavia\n permanecen en BD, un json con los records no sincronizados o bien un json con todos los records en la tabla de movimientos.\n http://direccion-deltotem:1880/api/movimientos?tipo=no_sincro\n http://direccion-deltotem:1880/api/movimientos?tipo=sincro\n http://direccion-deltotem:1880/api/movimientos?tipo=todos\n Los nodos que realizan esta funcion son \"API movimientos\", \"read records\", \"SQLITE_TOTEM\", \"send data\" y \"respuesta API\"\n ", + "x": 2190, + "y": 980, + "wires": [] + }, + { + "id": "732068c66d8e0a25", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "debug 23", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2920, + "y": 600, + "wires": [] + }, + { + "id": "32e555ac8f46a0f1", + "type": "mqtt in", + "z": "744bdf3a0e08d754", + "g": "e716da8ff5c458c9", + "name": "ack", + "topic": "totemautonomo/movimientos/movimiento_ok", + "qos": "1", + "datatype": "auto-detect", + "broker": "3966bb218232f6b3", + "nl": false, + "rap": true, + "rh": 0, + "inputs": 0, + "x": 2190, + "y": 800, + "wires": [ + [ + "6b1ae2b4cdb257fb", + "aba034491220fa28" + ] + ] + }, + { + "id": "e36bb58515b89053", + "type": "mqtt out", + "z": "744bdf3a0e08d754", + "g": "e716da8ff5c458c9", + "name": "send data", + "topic": "totemautonomo/movimientos/movimiento", + "qos": "2", + "retain": "", + "respTopic": "", + "contentType": "", + "userProps": "", + "correl": "", + "expiry": "", + "broker": "3966bb218232f6b3", + "x": 2340, + "y": 640, + "wires": [] + }, + { + "id": "6b1ae2b4cdb257fb", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "e716da8ff5c458c9", + "name": "rx ACK-1", + "func": "/*\nAqui recibimos el ack enviado por el programa del servidor que recibe y almacena los movimientos. Este programa (el del servidor) \ndebe enviar el topic:totemautonomo/movimientos/movimiento_ok ; payload: true / false ; uuid: el uuid del movimiento\n*/\n\nif (msg.payload.uuid === flow.get(\"ultimo_movimiento_id\") && msg.payload.movimiento_ok && !flow.get(\"ack_old_movimientos\")){\n // evaluamos el \"ack_old_movimientos\" para estar seguros que el ack recibido es para este nodo y no para un nodo\n // semejante (rx ACK) que tenemos en el grupo que se encarga de sincronizar los movimientos antiguos que no han podido ser \n // sincronizados en su momento.\n flow.set(\"ultimo_movimiento_ok\", true) // indicamos al nodo de UPDATE SINCRO que el ultimo envio ha sido recibido\n flow.set(\"ultimo_movimiento_id\", \"\")\n msg.reset = true // esto es para parar el timeout counter\n return msg\n}\n \nreturn ;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2340, + "y": 800, + "wires": [ + [ + "d47200e5f67c772a", + "ac94ab2b132fab52" + ] + ] + }, + { + "id": "7ef59a8ac4d8a7f3", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "e716da8ff5c458c9", + "name": "tx mqtt-1", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2360, + "y": 700, + "wires": [] + }, + { + "id": "aba034491220fa28", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "e716da8ff5c458c9", + "name": "rx ack-1", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2360, + "y": 840, + "wires": [] + }, + { + "id": "ac94ab2b132fab52", + "type": "trigger", + "z": "744bdf3a0e08d754", + "g": "e716da8ff5c458c9", + "name": "timeout", + "op1": "", + "op2": "1", + "op1type": "nul", + "op2type": "num", + "duration": "5", + "extend": false, + "overrideDelay": false, + "units": "s", + "reset": "", + "bytopic": "all", + "topic": "topic", + "outputs": 1, + "x": 2360, + "y": 740, + "wires": [ + [ + "774b2334aae0188c", + "d47200e5f67c772a", + "9a5e603ea67d2991" + ] + ] + }, + { + "id": "774b2334aae0188c", + "type": "change", + "z": "744bdf3a0e08d754", + "g": "e716da8ff5c458c9", + "name": "init uuid", + "rules": [ + { + "t": "set", + "p": "ultimo_movimiento_id", + "pt": "flow", + "to": "", + "tot": "str" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 2540, + "y": 740, + "wires": [ + [] + ] + }, + { + "id": "9a5e603ea67d2991", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "e716da8ff5c458c9", + "name": "timeout", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2520, + "y": 780, + "wires": [] + }, + { + "id": "9606dbbee4d79399", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "e716da8ff5c458c9", + "name": "", + "info": "Este grupo se encarga de enviar via mqtt el json correspondiente al contenido del movimiento\ndel vehiculo en la bascula. \nAdicionalmente espera un ACK con un timeout de 5 segundos. Este timeout se consigue con el uso de un nodo\ntrigger configurado en 5 segundos. Este nodo al cabo de 5 segundos envia un mensaje {payload: 1} al nodo\nde procesamiento para que continue el proceso con el proximo record.\nEn caso de recibir el acknowledge por parte del servidor, se envia un mensaje de reset al nodo trigger\ncon la finalidad de parar el contador.\nCuando se recibe el ack se borra tambien la referencia con el ultimo uuid para evitar recepciones de ack falsas\n\n\nNota sobre el funcionamiento del QoS.\nEl factor QoS de MQTT define la forma en que se gestiona el envio de una publicacion hacia el broker. (cliente - broker)\nMQTT define 3 niveles de QoS (Quality of Service) que determinan la garantía de entrega del mensaje entre el cliente y el broker:\n\n1. QoS 0 \"At most once\" (como máximo una vez)\nNo se garantiza la entrega. El mensaje se envía una única vez y no hay reintento ni confirmación.\nNo hay acuse de recibo (ACK).\nUsos comunes: datos de sensores donde la pérdida ocasional no importa (ej. temperatura cada 60 segundos).\nImplica solo un paquete TCP: PUBLISH \n\n2. QoS 1 \"At least once\" (al menos una vez)\nEl mensaje se entrega una o más veces. Se garantiza que llegará al broker, pero puede haber duplicados.\nSe requiere acuse de recibo: el cliente envía PUBLISH, el broker responde con PUBACK una vez recibido.\nEl cliente debe reintentar si no recibe el PUBACK. (Aqui es donde puede haber duplicados del mensaje)\nImplica al menos dos paquetes TCP: PUBLISH → PUBACK\n\n3. QoS 2 \"Exactly once\" (exactamente una vez)\nEl más seguro: garantiza que el mensaje se entrega una sola vez y sin duplicados.\nUsa un handshake de 4 pasos entre cliente y broker:\n 1. Cliente → Broker: PUBLISH (mensaje)\n 2. Broker → Cliente: PUBREC (recibido)\n 3. Cliente → Broker: PUBREL (libera) (el mensaje puede ser entregado al usuario)\n 4. Broker → Cliente: PUBCOMP (completado) (ciclo completado)\nImplica cuatro paquetes TCP para confirmar entrega precisa\n\n¿Qué pasa con TCP?\nMQTT siempre usa TCP como transporte (puerto 1883 por defecto o 8883 para MQTT sobre TLS), lo que significa que:\n - Hay verificación de integridad de paquetes.\n - Se garantiza el orden y la entrega de paquetes TCP.\n - Pero TCP por sí solo no garantiza que la aplicación MQTT haya recibido o procesado el mensaje.\n - Por eso, MQTT usa sus propios niveles de QoS por encima de TCP, para controlar la lógica de entrega a nivel de aplicación.", + "x": 2160, + "y": 720, + "wires": [] + }, + { + "id": "10affb7590fa95f1", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "mydb": "1c8613764b7f685e", + "name": "Actualiza tabla", + "x": 2920, + "y": 640, + "wires": [ + [ + "fd9524c0921c0990", + "b4caaec341e453e0" + ] + ] + }, + { + "id": "ee2f281460418d2a", + "type": "function", + "z": "744bdf3a0e08d754", + "d": true, + "g": "75dd57faa07f33b9", + "name": "UPDATE SINCRO", + "func": "\n\nlet sincro = 1\nif (flow.get(\"movimientosxapi\")) {\n let lastmsg = JSON.parse(msg.payload) // enviado por el servidor como respuesta. Debe enviar el IdMovimiento que hemos sincronizado para procesarlo\n msg = {}\n msg.tipo = \"sincro\";\n if (lastmsg.statusCode === 200) {\n //marcamos este record como enviado al API. Ponemos 1 ya que en sqlite no existe el tipo booleano true o false \n msg.topic = `UPDATE movimientos_cerrados SET sincro = ${sincro} WHERE uuid = '${lastmsg.IdMovimiento}'; `.trim(); // trim para eliminar espacios en blanco en los extremos\n msg.payload = \"\";\n return [{ payload: 1 }, msg];\n } \n else {\n node.warn(`Error al enviar UUID ${lastmsg.uuid}, código HTTP: ${msg.msg.statusCode}`);\n return [{ payload: 1 }, null];\n }\n}\nif (flow.get(\"movimientosxmqtt\")) {\n if (flow.get(\"ultimo_movimiento_ok\")) {\n msg.topic = `UPDATE movimientos_cerrados SET sincro = ${sincro} WHERE uuid = '${msg.payload.uuid}'`;\n msg.payload = \"\";\n return [{ payload: 1 }, msg];\n }\n else {\n node.warn(`Error al enviar UUID ${msg.payload.uuid} via MQTT`);\n return [{ payload: 1 }, null];\n }\n}\n\n\nreturn \n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2990, + "y": 500, + "wires": [ + [], + [] + ] + }, + { + "id": "6455539b9fac2063", + "type": "https-node", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "Enviar a API", + "method": "POST", + "ret": "txt", + "paytoqs": "ignore", + "url": "", + "tls": "", + "persist": false, + "proxy": "", + "authType": "", + "senderr": false, + "x": 2350, + "y": 540, + "wires": [ + [ + "7ff54b99b5b933b2" + ] + ] + }, + { + "id": "7ff54b99b5b933b2", + "type": "json", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "", + "property": "payload", + "action": "", + "pretty": false, + "x": 2510, + "y": 540, + "wires": [ + [ + "d47200e5f67c772a", + "e192c564f2050e94" + ] + ] + }, + { + "id": "08bf0c9897d6e815", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "debug 41", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2340, + "y": 500, + "wires": [] + }, + { + "id": "65b220fc46d55c96", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "name": "debug 43", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2940, + "y": 820, + "wires": [] + }, + { + "id": "1279bc86f850f792", + "type": "exec", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "command": " ./remove-add-host.sh ", + "addpay": "payload", + "append": "", + "useSpawn": "false", + "timer": "", + "winHide": false, + "oldrc": false, + "name": "change hosts", + "x": 1330, + "y": 1120, + "wires": [ + [ + "c18ea33c86d877db" + ], + [], + [] + ] + }, + { + "id": "c18ea33c86d877db", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1490, + "y": 1060, + "wires": [] + }, + { + "id": "9b375376ea8f2a66", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "1.5", + "topic": "", + "payload": "192.168.11.67 raspeso.com", + "payloadType": "str", + "x": 1090, + "y": 1040, + "wires": [ + [ + "1279bc86f850f792" + ] + ] + }, + { + "id": "844805b92ed8fc5c", + "type": "exec", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "command": "", + "addpay": "payload", + "append": "", + "useSpawn": "false", + "timer": "", + "winHide": false, + "oldrc": false, + "name": "change IP", + "x": 1320, + "y": 1180, + "wires": [ + [ + "b1eb9bccca87f0f9" + ], + [], + [] + ] + }, + { + "id": "b1eb9bccca87f0f9", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "name": "change IP", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1500, + "y": 1160, + "wires": [] + }, + { + "id": "29b2dd9b01235f99", + "type": "file in", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "name": "read-file", + "filename": "/home/trypton/.node-red/projects/config_comun.txt", + "filenameType": "str", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 860, + "y": 1180, + "wires": [ + [ + "8435634f3b8cf017" + ] + ] + }, + { + "id": "0471bc0a0beb44fd", + "type": "file in", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "name": "read-file", + "filename": "/home/trypton/.node-red/projects/config_supervisor.txt", + "filenameType": "str", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 860, + "y": 1120, + "wires": [ + [ + "f372e14a6921bc3a" + ] + ] + }, + { + "id": "c5a3e80cc446b610", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "name": "read_config_in", + "links": [ + "17547c6cf2cc8bdf" + ], + "x": 685, + "y": 1140, + "wires": [ + [ + "0471bc0a0beb44fd", + "29b2dd9b01235f99" + ] + ] + }, + { + "id": "323c2ee052338182", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "2", + "topic": "", + "payload": "1", + "payloadType": "num", + "x": 710, + "y": 1080, + "wires": [ + [ + "0471bc0a0beb44fd", + "29b2dd9b01235f99" + ] + ] + }, + { + "id": "a8218646952839ba", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "name": "config comun", + "func": "var config = JSON.parse(msg.payload);\nmsg ={}; // clear msg object\nvar read;\n\nflow.set(\"vehiculo_activo_flag\", false);\nflow.set(\"last_QR\", \"\");\n\n//read = config.peso;\n//flow.set(\"peso\", read); // crea variable para recibir peso del supervisor\nread = config.peso_estable_flag;\nflow.set(\"peso_estable_flag\", read);\nread = config.peso_estable; // valor del peso estabilizado\nflow.set(\"peso_estable\", read);\nread = config.fin_pesada_flag;\nflow.set(\"fin_pesada_flag\", read);\nread = config.salida_vehiculo_flag;\nflow.set(\"salida_vehiculo_flag\", read);\nread = config.vehiculo_presente_flag;\nflow.set(\"vehiculo_presente_flag\", read);\nread = config.remover_firma;\nflow.set(\"remover_firma\", read);\n\nread = config.matricula_ok_flag;\nflow.set(\"matricula_ok_flag\", read);\nread = config.QR_ok_flag;\nflow.set(\"QR_ok_flag\", read);\nread = config.RFID\nflow.set(\"RFID\", read);\nread = config.RFID_ok_flag;\nflow.set(\"RFID_ok_flag\", read);\nread = config.QR_flag;\nflow.set(\"QR_flag\", read);\n\n\nread = config.auth_pin;\nflow.set(\"auth_pin\", read)\nread = config.pin_timeout; // timeout de espera del pin\nflow.set(\"pin_timeout\", read)\nread = config.mante_timeout; // timeout de la pantalla de mantenimiento\nflow.set(\"mante_timeout\", read)\n\nread = config.sw_bkgnd_on // color del background ON de los botones de seleccion de parametro \nflow.set(\"sw_bkgnd_on\", read);\nread = config.sw_bkgnd_off // color del background OFF de los botones de seleccion de parametro \nflow.set(\"sw_bkgnd_off\", read);\nread = config.titulo_param_color // color del nombre de los parametros\nflow.set(\"titulo_param_color\", read);\nflow.set(\"value_param_color\", read);\nread = config.namefixedparams_color // color del nombre de los parametros fijos (ip, serial, puertos remotos)\nflow.set(\"namefixedparams_color\", read);\nread = config.semaforo_green_color // color del semaforo en verde\nflow.set(\"semaforo_green_color\", read);\nread = config.semaforo_red_color // color del semaforo en rojo\nflow.set(\"semaforo_red_color\", read);\nread = config.titulo_matricula_color // color del titulo de la matricula y del ID\nflow.set(\"titulo_matricula_color\", read)\nread = config.valor_matricula_color // color del titulo de la matricula y del ID\nflow.set(\"valor_matricula_color\", read)\n\nread = config.totemid_max // maximo numero de identificacion asignado al totem\nflow.set(\"totemid_max\", read);\nread = config.vial_max // maximo numero de viales admitidos para asignar al dispositico\nflow.set(\"vial_max\", read);\n\n\n\n\nreturn", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1110, + "y": 1180, + "wires": [ + [] + ] + }, + { + "id": "8435634f3b8cf017", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "name": "debug 11", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1040, + "y": 1080, + "wires": [] + }, + { + "id": "f372e14a6921bc3a", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "name": "config edit", + "func": "const config = JSON.parse(msg.payload);\nmsg ={}; // clear msg object\nlet read;\nlet config_general = []\nlet config_tiempos = []\nlet config_mensajes = []\nlet config_sensor = []\n//let config_camara = []\nlet config_conex = []\n//let config_tipos = []\n\nconfig.general = toObject(config.general);\n//config.camara = toObject(config.camara);\nconfig.conexion = toObject(config.conexion);\nconfig.sensores = toObject(config.sensores);\nconfig.mensajes = toObject(config.mensajes);\nconfig.tiempos = toObject(config.tiempos);\n\n\n\n/*\n// Aseguramos que cada tipo esté como array vacío si no tiene nada\nfor (let i = 1; i <= 6; i++) {\n if (!Array.isArray(tiposOriginales[i])) {\n tiposOriginales[i] = [];\n }\n}\nflow.set(\"tiposDatosGlobal\", tiposOriginales);\n\nconfig.tipos = tiposOriginales;\n*/\n\nread = config.general.formatoPais; // peso minimo para utilizado para indicar cuando hay vehiculo en la balanza o no\nconfig_general.push({ \"parametro\": \"formatoPais\", \"valor\": read })\nflow.set(\"formatoPais\", read);\n\n//---------------------------------------------------------------------------------------------\nread = config.tiempos.timer1;\nconfig_tiempos.push({ \"parametro\": \"timer1\", \"valor\": read })\nflow.set(\"timer1\", read);\nread = config.general.audio_level;\nconfig_general.push({ \"parametro\": \"audio_level\", \"valor\": read })\nflow.set(\"audio_level\", read);\n\n//-------------------------------------------------------\nread = config.general.mante_code_RFID; //codigo RFID para pasar a pantalla de mantenimiento\nconfig_general.push({ \"parametro\": \"mante_code_RFID\", \"valor\": read })\nflow.set(\"mante_code_RFID\", read)\nread = config.general.mante_code_QR; //codigo QR para pasar a pantalla de mantenimiento\nconfig_general.push({ \"parametro\": \"mante_code_QR\", \"valor\": read })\nflow.set(\"mante_code_QR\", read)\n\n//------------------------------- MENSAJES -------------------------\nread = config.tiempos.tiempo_mensaje;\nconfig_tiempos.push({ \"parametro\": \"tiempo_mensaje\", \"valor\": read })\nflow.set(\"tiempo_mensaje\", read);\nread = config.tiempos.relay2_pulse // duración del pulso generado en el relay 2 al terminar el pesaje\nconfig_tiempos.push({ \"parametro\": \"relay2_pulse\", \"valor\": read })\nflow.set(\"relay2_pulse\", read);\nread = config.mensajes.mensaje2\nconfig_mensajes.push({ \"parametro\": \"mensaje2\", \"valor\": read })\nflow.set(\"mensaje2\", read);\nread = config.mensajes.mensaje1\nconfig_mensajes.push({ \"parametro\": \"mensaje1\", \"valor\": read })\nflow.set(\"mensaje1\", read);\nread = config.general.init_screen\nconfig_general.push({ \"parametro\": \"init_screen\", \"valor\": read })\nflow.set(\"init_screen\", read);\n\n\n\n//-------------------------------------------------------\nread = config.general.totem_serial // serial asignado a este dispositivo\nconfig_general.push({ \"parametro\": \"totem_serial\", \"valor\": read })\nflow.set(\"totem_serial\", read);\n\nread = config.general.dias_en_BD // numero de dias maximo de un record en base de datos\nconfig_general.push({ \"parametro\": \"dias_en_BD\", \"valor\": read })\nflow.set(\"dias_en_BD\", read);\n\n\n//----------------------------------------- Textos Predefinidos -------------------------------------------------------------------------\nread = config.mensajes.men_pesaje_listo;\nconfig_mensajes.push({ \"parametro\": \"men_pesaje_listo\", \"valor\": read })\nflow.set(\"men_pesaje_listo\", read);\nread = config.mensajes.men_peso_estable;\nconfig_mensajes.push({ \"parametro\": \"men_peso_estable\", \"valor\": read })\nflow.set(\"men_peso_estable\", read);\nread = config.mensajes.men_identificacion;\nconfig_mensajes.push({ \"parametro\": \"men_identificacion\", \"valor\": read })\nflow.set(\"men_identificacion\", read);\nread = config.mensajes.men_salida;\nconfig_mensajes.push({ \"parametro\": \"men_salida\", \"valor\": read })\nflow.set(\"men_salida\", read);\nread = config.mensajes.id_titulo_matricula;\nconfig_mensajes.push({ \"parametro\": \"id_titulo_matricula\", \"valor\": read })\nflow.set(\"id_titulo_matricula\", read);\nread = config.mensajes.id_titulo_id;\nconfig_mensajes.push({ \"parametro\": \"id_titulo_id\", \"valor\": read })\nflow.set(\"id_titulo_id\", read);\nread = config.mensajes.id_leido;\nconfig_mensajes.push({ \"parametro\": \"id_leido\", \"valor\": read })\nflow.set(\"id_leido\", read);\nread = config.mensajes.id_no_leido;\nconfig_mensajes.push({ \"parametro\": \"id_no_leido\", \"valor\": read })\nflow.set(\"id_no_leido\", read);\n\n//===================================================CONEXION / CAMARA / SENSOR =====================================================\n//====================================================================================================================================\n\n\n//-----------------------------Gestion de IP fija / IP DHCP------------------------------------------------------\nvar ipdevice_flag = false // variables temporales para saber si hay cambios en las ip\nvar iprouter_flag = false\nread = config.conexion.ipdevice // direccion IP del dispositivo\nconfig_conex.push({ \"parametro\": \"ipdevice\", \"valor\": read })\nif (flow.get(\"ipdevice\") != read) {\n flow.set(\"ipdevice\", read);\n ipdevice_flag = true;\n}\nread = config.conexion.iprouter // direccion IP del router\nconfig_conex.push({ \"parametro\": \"iprouter\", \"valor\": read })\nif (flow.get(\"iprouter\") != read) {\n flow.set(\"iprouter\", read);\n iprouter_flag = true;\n}\nread = config.conexion.dhcp_flag // flag del DHCP\nconfig_conex.push({ \"parametro\": \"dhcp_flag\", \"valor\": read })\nif (flow.get(\"dhcp_flag\") != read) {\n flow.set(\"dhcp_flag\", read); // el dhcp_flag ha cambiado\n if (flow.get(\"dhcp_flag\")) {\n node.send([null, { payload: \". /home/trypton/scripts/enable_dhcp.sh\" }])\n }\n else {\n node.send([null, { payload: \". /home/trypton/scripts/enable_ipstatic.sh \" + flow.get(\"ipdevice\") + \"/24 \" + flow.get(\"iprouter\") }])\n }\n}\nelse { // el dhcp_flag no ha cambiado\n if (!flow.get(\"dhcp_flag\")) {\n if (ipdevice_flag || iprouter_flag) {\n node.send([null, { payload: \". /home/trypton/scripts/enable_ipstatic.sh \" + flow.get(\"ipdevice\") + \"/24 \" + flow.get(\"iprouter\") }])\n }\n }\n else {\n node.send([null, { payload: \". /home/trypton/scripts/enable_dhcp.sh\" }])\n }\n}\nflow.set(\"dhcpname_on\", \"IP Fija\") // texto que sale en el boton de la pantalla de mantenimiento\nflow.set(\"dhcpname_off\", \"DHCP\")\n\n//----------------------------------- ENVIO DE MOVIMIENTOS --------------------------------------------------------------------------------------\nread = config.conexion.movimientosxmqtt;\nconfig_conex.push({ \"parametro\": \"movimientosxmqtt\", \"valor\": read })\nflow.set(\"movimientosxmqtt\", read); // Modo de envio de movimientos\n\nread = config.conexion.movimientosxapi;\nconfig_conex.push({ \"parametro\": \"movimientosxapi\", \"valor\": read })\nflow.set(\"movimientosxapi\", read); // Modo de envio de movimientos\n\nread = config.conexion.path_images;\nconfig_conex.push({ \"parametro\": \"path_images\", \"valor\": read })\nflow.set(\"path_images\", read); // raiz de la carpeta de guardar imagenes\n\nread = config.conexion.path_firmas;\nconfig_conex.push({ \"parametro\": \"path_firmas\", \"valor\": read })\nflow.set(\"path_firmas\", read); // raiz de la carpeta de guardar imagenes de las firmas\n\n//-------------------------------------------------- URL de la API de URBASER(Trypton) ------------------------------------------------------------------------------\nread = config.conexion.url_API;\nconfig_conex.push({ \"parametro\": \"url_API\", \"valor\": read })\nflow.set(\"url_API\", read); // url de llamado a la api para leer tablas de bd\nflow.set(\"semaforo\", \"green\")\n\nread = config.conexion.tokenapi;\nconfig_conex.push({ \"parametro\": \"tokenapi\", \"valor\": read })\nflow.set(\"tokenapi\", read); // token del totem utilizado durante la lectura de tablas de la bd\n\nread = config.conexion.IdCentroProveedor;\nconfig_conex.push({ \"parametro\": \"IdCentroProveedor\", \"valor\": read })\nflow.set(\"IdCentroProveedor\", read); // token del proveedor utilizado durante la lectura de tablas de la bd y el envio de movimientos\n\n//----------------------------------- DEFINICION DEL BROKER DE MQTT --------------------------------------------------------------------------------------\n/*\nread = config.conexion.broker_url;\nconfig_conex.push({ \"parametro\": \"broker_url\", \"valor\": read })\nflow.set(\"broker_url\", read); // direccion del broker\n\nread = config.conexion.broker_port;\nconfig_conex.push({ \"parametro\": \"broker_port\", \"valor\": read })\nflow.set(\"broker_port\", read); // puerto del broker\n\nread = config.conexion.broker_username;\nconfig_conex.push({ \"parametro\": \"broker_username\", \"valor\": read })\nflow.set(\"broker_username\", read); // username del broker\n\nread = config.conexion.broker_pass;\nconfig_conex.push({ \"parametro\": \"broker_pass\", \"valor\": read })\nflow.set(\"broker_pass\", read); // password del broker\n*/\n\n\n//------------------------------------Lector de QR / RF----------------------------------------------------------\nread = config.sensores.tipo_de_lector\nconfig_sensor.push({ \"parametro\": \"tipo_de_lector\", \"valor\": read })\nflow.set(\"tipo_de_lector\", read); //tipo de lector QR / RF\nread = config.sensores.rfid_invertido\nconfig_sensor.push({ \"parametro\": \"rfid_invertido\", \"valor\": read })\nflow.set(\"rfid_invertido\", read);\nflow.set(\"lector_activo\", true) // activamos el lector\nflow.set(\"ID_enviado\", false) // variable para controlar un solo envio de RFID / QR\n\n\n//---------------------------------------Totems y Camara------------------------------------------------------------------------------\nread = config.conexion.cantidad_viales;\nconfig_conex.push({ \"parametro\": \"cantidad_viales\", \"valor\": read })\nflow.set(\"cantidad_viales\", read);\n\n\n\n//---------------------------------------Lectura de Matricula-----------------------------------------------------------------------\nread = config.tiempos.time_max_2_bascula\nconfig_tiempos.push({ \"parametro\": \"time_max_2_bascula\", \"valor\": read })\nflow.set(\"time_max_2_bascula\", read);\nread = config.tiempos.wait_time_to_send_plate\nconfig_tiempos.push({ \"parametro\": \"wait_time_to_send_plate\", \"valor\": read })\nflow.set(\"wait_time_to_send_plate\", read);\nread = config.tiempos.max_wait_time_to_send_plate\nconfig_tiempos.push({ \"parametro\": \"max_wait_time_to_send_plate\", \"valor\": read })\nflow.set(\"max_wait_time_to_send_plate\", read);\nread = config.tiempos.max_time_bascula_sensor\nconfig_tiempos.push({ \"parametro\": \"max_time_bascula_sensor\", \"valor\": read })\nflow.set(\"max_time_bascula_sensor\", read);\nread = config.tiempos.time_to_snapshot\nconfig_tiempos.push({ \"parametro\": \"time_to_snapshot\", \"valor\": read })\nflow.set(\"time_to_snapshot\", read);\nflow.set(\"plate_sent\", false);\nflow.set(\"anpr0_flag\", false);\n\n\n//---------------------------------------- Flags edicion de matricula ---------------------------------------------------------------------------------------------\nflow.set(\"badplate\", false) // inicializa flag de solicitu de modificación de matricula\nflow.set(\"prechar\", \"*\") // inicializa variable con caracter de prefijo para el $watch del nodo display de edicion de matricula\n\nflow.set(\"config_general\", config_general)\nflow.set(\"config_tiempos\", config_tiempos)\nflow.set(\"config_mensajes\", config_mensajes)\nflow.set(\"config_conex\", config_conex)\n//flow.set(\"config_camara\", config_camara)\nflow.set(\"config_sensor\", config_sensor)\n\nreturn;\n\n\n//*********************************************************** FUNCIONES **************************************************/\nfunction toObject(array) {\n \n const obj = {};\n array.forEach(el => {\n obj[el.parametro] = el.valor;\n\n });\n return obj;\n}\n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1110, + "y": 1120, + "wires": [ + [ + "cc890d832924b5d5", + "1279bc86f850f792" + ], + [ + "844805b92ed8fc5c" + ] + ] + }, + { + "id": "cc890d832924b5d5", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "800484a81d9dc5ee", + "name": "debug 28", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1260, + "y": 1040, + "wires": [] + }, + { + "id": "73752ba2ae415953", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "debug 2", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1080, + "y": 1360, + "wires": [] + }, + { + "id": "182712df6587926f", + "type": "json", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "", + "property": "payload", + "action": "", + "pretty": false, + "x": 830, + "y": 1440, + "wires": [ + [ + "73752ba2ae415953", + "de78dc5fb92df251" + ] + ] + }, + { + "id": "de78dc5fb92df251", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "comandos \\n via \\n websocket", + "func": "if (msg.payload.device == \"supervisor\") {\n msg.numero_vial = msg.payload.numero_vial\n msg.totem_id = msg.payload.totem_id\n \n let msg_ack = {payload:{ack: true}}\n switch (msg.payload.accion) {\n case \"check\": // hace check de la matricula en la BD.\n msg.matricula = msg.payload.matricula\n \n /*Solicitamos a la BD si existe la matricula en la tabla Matriculas_Identificadores. Si esa matricula solo esta una vez\n entonces recogemos el id correspondiente y con ese id buscamosla descripcion en la tabla \"Identificadores\"\n Si el campo descripcion contiene una coma \",\" entonces quiere decir que no hay menu y que esa entrada corresponde a un QR / RFID\n por lo tanto en el mensaje que enviamos al totem indicamos que hay que mostrar una identificacion.\n en caso que no este registrada la matricula, la BD nos entrega un mensaje con el parametro \"registrada\" = 0.\n */\n msg.topic = `\n WITH input AS (SELECT TRIM('${msg.payload.matricula}') AS MatriculaInput)\n SELECT\n CASE WHEN ma.matricula IS NOT NULL THEN 1 ELSE 0 END AS registrada,\n CAST(EXISTS(SELECT 1 FROM movimientos m JOIN input i ON UPPER(TRIM(m.matricula)) = UPPER(i.MatriculaInput) LIMIT 1) AS INTEGER) AS transito,\n ma.tara, ma.pma, (SELECT CASE WHEN COUNT(*)=1 THEN MAX(idf.descripcion) ELSE NULL END FROM Matriculas_Identificadores mi JOIN input i ON UPPER(TRIM(mi.matricula)) = UPPER(i.MatriculaInput) LEFT JOIN Identificadores idf ON idf.id = mi.id) AS descripcion,\n (SELECT COUNT(*) FROM Matriculas_Identificadores mi JOIN input i ON UPPER(TRIM(mi.matricula)) = UPPER(i.MatriculaInput)) AS repeticion_matricula\n FROM input i\n LEFT JOIN Matriculas ma ON UPPER(TRIM(ma.matricula)) = UPPER(i.MatriculaInput)\n LIMIT 1;\n `;\n msg.payload = \"\"\n node.send([msg, null, null, null, null, null, null])\n break;\n\n case \"qr/rfid\":\n msg.tipo = msg.payload.tipo\n msg.id = msg.payload.id\n msg.payload = msg.payload.id\n node.send([null, msg, null, null, null, null, null])\n break;\n\n case \"movimiento\": // tenemos que enviar un ACK para asegurar que el supervisor recibio el comando\n let new_register = msg.payload.movimiento\n flow.set(\"new_register\", new_register) // guardamos ultimo registro en memoria local para uso durante el analisis\n let imagen_matricula = new_register.imagen\n let nombre_imagen = new_register.nombre_imagen\n let imagen_firma = new_register.firma\n let nombre_firma = new_register.nombre_firma\n let destareautomatico = new_register.destareautomatico\n let tara = new_register.tara\n if (tara > 0){\n //enviamos este movimiento como el cierre\n msg = {}\n msg.topic = \"movimiento_tarado\"\n node.send([null, null, null, msg, null, null, null])\n\n }\n else {\n if (msg.payload.tipomovimiento == \"inicial\") {\n new_register.tipomovimiento = \"inicial\" // indicamos que este registro corresponde a un movimiento de inicio\n //guardamos el movimiento en la tabla de movimientos para que posteriormente el supervisor los analice\n let msg_plate = { payload: imagen_matricula, filename: flow.get(\"path_images\") + nombre_imagen } // guardamos archivo con imagen en disco local\n if (nombre_firma != \"No Habilitada\"){\n let msg_firma = { payload: imagen_firma, filename: flow.get(\"path_firmas\") + nombre_firma } // guardamos archivo con imagen en disco local\n node.send([null, null, null, null, null, msg_firma, null])\n }\n let registro = `INSERT INTO movimientos (uuid, horaentrada, matricula, matriculaeditada, nombre_imagen, pesoestable, qr, rfid, vial, totem, horasalida,\n sentido, modo, botonvalidacion, nombre_firma, firmaobligatoria, timestamp, id_seleccionado, tipo) \n VALUES ('${new_register.uuid}', '${new_register.horaentrada}', '${new_register.matricula}', ${new_register.matriculaeditada}, '${new_register.nombre_imagen}', ${new_register.pesoestable}, \n '${new_register.qr}', '${new_register.rfid}', '${new_register.vial}', '${new_register.totem}', \n '${new_register.horasalida}', '${new_register.sentido}', '${new_register.modo}', ${new_register.botonvalidacion}, '${new_register.nombre_firma}',\n ${new_register.firmaobligatoria}, '${new_register.timestamp}', '${new_register.id_seleccionado}', '${new_register.tipomovimiento}')`;\n\n // Hacemos un json especial para enviar al Servidor con el movimiento inicial\n let msg_inicial = {}\n msg_inicial.payload = {}\n msg_inicial.payload.IdMovimiento = new_register.uuid\n msg_inicial.payload.matricula = new_register.matricula\n msg_inicial.payload.horaentrada_i = new_register.horaentrada\n msg_inicial.payload.horasalida_i = new_register.horasalida\n msg_inicial.payload.nombre_imagen_i = new_register.nombre_imagen\n msg_inicial.payload.nombre_firma = new_register.nombre_firma\n msg_inicial.payload.pesoEntrada = new_register.pesoestable\n msg_inicial.payload.rfid_i = new_register.rfid || \" \"\n msg_inicial.payload.qr_i = new_register.qr || \" \"\n msg_inicial.payload.IdCentroServicio = new_register.id_seleccionado\n msg_inicial.payload.vial = new_register.vial\n msg_inicial.payload.totem = new_register.totem\n msg_inicial.payload.ts_cierre = new_register.timestamp\n msg_inicial.payload.IdCentroProveedor = flow.get(\"IdCentroProveedor\") \n msg_inicial.payload.imagen_entrada = new_register.imagen || \" \"\n msg_inicial.payload.imagen_firma = new_register.firma || \" \"\n \n \n msg_inicial.url = flow.get(\"url_API\") + `api/Totem/InsertarMovimientos?token=${flow.get(\"tokenapi\")}`\n //msg_inicial.requestTimeout = \"20000\"\n \n node.send([null, null, null, null, msg_plate, null,{ topic: registro }, msg_inicial])\n }\n else if (msg.payload.tipomovimiento == \"final\") {\n new_register.tipomovimiento = \"final\" // indicamos que este registro corresponde a un movimiento final para cierre\n flow.set(\"matricula_movimiento\", new_register.matricula)\n let matricula = flow.get(\"new_register\").matricula\n msg.topic = `SELECT * FROM movimientos WHERE matricula = '${matricula}' AND tipo = 'inicial'`\n msg.payload = \"\"\n node.send([null, null, msg, null, null, null, null]) // inicia el analisis\n /*El objetivo del analisis es revisar si la matricula recibida en el movimiento de salida\n tiene un movimiento de entrada registrado en la tabla de movimientos de la BD.\n */\n\n }\n }\n break;\n }\n\n}\nreturn;", + "outputs": 8, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1090, + "y": 1460, + "wires": [ + [ + "6703fb916aef8f4b" + ], + [ + "4e1f0ff54eef4e7b" + ], + [ + "2dc5c2942966b097" + ], + [ + "8a7201a76e53897f" + ], + [ + "3d7afdefd9a5d353" + ], + [ + "9fa72afe7d97828e" + ], + [ + "5c4679ed20761208" + ], + [ + "138317044e960cfa", + "dc3fe6bd2494cab0" + ] + ] + }, + { + "id": "6703fb916aef8f4b", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "check", + "mode": "link", + "links": [ + "a1ac4bf3b8c14b21" + ], + "x": 1315, + "y": 1400, + "wires": [] + }, + { + "id": "b264dc44d4344d72", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{\"device\":\"supervisor\",\"totem_id\":\"totem_103\",\"accion\":\"check\",\"matricula\":\"6234FLH\"}", + "payloadType": "json", + "x": 830, + "y": 1500, + "wires": [ + [ + "de78dc5fb92df251" + ] + ] + }, + { + "id": "4e1f0ff54eef4e7b", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "QR/RFID", + "mode": "link", + "links": [ + "8e56735bbb37a598" + ], + "x": 1315, + "y": 1440, + "wires": [] + }, + { + "id": "2dc5c2942966b097", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "mov", + "mode": "link", + "links": [ + "c674aac5bef7594a" + ], + "x": 1315, + "y": 1480, + "wires": [] + }, + { + "id": "3d7afdefd9a5d353", + "type": "file", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "guarda imagenes", + "filename": "filename", + "filenameType": "msg", + "appendNewline": false, + "createDir": true, + "overwriteFile": "true", + "encoding": "none", + "x": 1410, + "y": 1560, + "wires": [ + [] + ] + }, + { + "id": "138317044e960cfa", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "debug 24", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 1400, + "y": 1680, + "wires": [] + }, + { + "id": "4f30b9755ed381bb", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "debug 25", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1560, + "y": 1720, + "wires": [] + }, + { + "id": "66630f04db34f483", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "salida", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{\"device\":\"supervisor\",\"totem_id\":\"totem_103\",\"accion\":\"movimiento\",\"tipomovimiento\":\"final\",\"movimiento\":{\"uuid\":\"ae0f5823-2d12-4492-9e80-23c7990afee5\",\"horaentrada\":\"27/10/2025,21:16:29\",\"vial\":1,\"totem\":\"totem_103\",\"modo\":\"A\",\"identificacion_esp\":true,\"firmaobligatoria\":true,\"id_seleccionado\":\"083853B1-E17F-4833-B87E-6317D93B54FB\",\"nombre_firma\":\"firma-6234FLH-20251027211632.jpg\",\"QR\":\"-\",\"RFID\":\"-\",\"nombre_imagen\":\"6234FLH-20251027211632.jpg\",\"matricula\":\"6234FLH\",\"matriculaeditada\":false,\"botonvalidacion\":false,\"horasalida\":\"27/10/2025,21:17:02\",\"pesoestable\":14570,\"timestamp\":1761596222}}", + "payloadType": "json", + "x": 830, + "y": 1620, + "wires": [ + [ + "de78dc5fb92df251" + ] + ] + }, + { + "id": "2d857220b76dda21", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "entrada", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{\"device\":\"supervisor\",\"totem_id\":\"totem_103\",\"accion\":\"movimiento\",\"tipomovimiento\":\"inicial\",\"movimiento\":{\"uuid\":\"ae0f5823-2d12-4492-9e80-23c7990afee5\",\"horaentrada\":\"27/10/2025,21:16:29\",\"vial\":1,\"totem\":\"totem_103\",\"modo\":\"A\",\"identificacion_esp\":true,\"firmaobligatoria\":true,\"id_seleccionado\":\"083853B1-E17F-4833-B87E-6317D93B54FB\",\"nombre_firma\":\"firma-8305LSM-20251027211632.jpg\",\"QR\":\"-\",\"RFID\":\"-\",\"nombre_imagen\":\"8305LSM-20251027211632.jpg\",\"matricula\":\"8305LSM\",\"matriculaeditada\":false,\"botonvalidacion\":false,\"horasalida\":\"27/10/2025,21:17:02\",\"pesoestable\":14570,\"timestamp\":1761596222}}", + "payloadType": "json", + "x": 830, + "y": 1660, + "wires": [ + [ + "de78dc5fb92df251" + ] + ] + }, + { + "id": "8a7201a76e53897f", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "movcerrar", + "mode": "link", + "links": [ + "0ba07aef43c4a226" + ], + "x": 1315, + "y": 1520, + "wires": [] + }, + { + "id": "1b2d9af6c1e13e5f", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "1234ABC", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{\"device\":\"supervisor\",\"totem_id\":\"totem_103\",\"accion\":\"check\",\"matricula\":\"1234ABC\"}", + "payloadType": "json", + "x": 840, + "y": 1540, + "wires": [ + [ + "de78dc5fb92df251" + ] + ] + }, + { + "id": "5c4679ed20761208", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "mydb": "1c8613764b7f685e", + "name": "guarda movimiento", + "x": 1410, + "y": 1640, + "wires": [ + [] + ] + }, + { + "id": "c674aac5bef7594a", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "name": "analisis", + "links": [ + "2dc5c2942966b097" + ], + "x": 655, + "y": 1880, + "wires": [ + [ + "cc229c10e3f4e423" + ] + ] + }, + { + "id": "405f569c2efbf22d", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "name": "debug 22", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 920, + "y": 1820, + "wires": [] + }, + { + "id": "417e71d002f16583", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "name": "movimiento cerrado", + "func": "msg.payload.tipoenvio = \"api\"\nif (msg.topic != \"movimiento tarado\") {\n if (msg.payload.length > 0) {\n //existe al menos un movimiento de entrada asociado a esta matricula y tomamos el utltimo\n let movinicial = msg.payload[0]\n let movfinal = flow.get(\"new_register\") // almacenado durante el ultimo movimiento recibido de salida\n \n // a continuacion tenemos que configurar el json para enviar a la api como movimiento cerrado\n msg.payload = {}\n msg.payload.IdMovimiento = movinicial.uuid\n msg.payload.matricula = movinicial.matricula\n msg.payload.horaentrada_i = movinicial.horaentrada\n msg.payload.horasalida_i = movinicial.horasalida\n msg.payload.horaentrada_f = movfinal.horaentrada\n msg.payload.horasalida_f = movfinal.horasalida\n msg.payload.nombre_imagen_i = movinicial.nombre_imagen\n msg.payload.nombre_imagen_f = movfinal.nombre_imagen\n msg.payload.nombre_firma = movinicial.nombre_firma\n msg.payload.pesoEntrada = movinicial.pesoestable\n msg.payload.pesoSalida = movfinal.pesoestable\n msg.payload.rfid_i = movinicial.rfid || \" \"\n msg.payload.qr_i = movinicial.qr || \" \"\n msg.payload.qr_f = movfinal.qr || \" \"\n msg.payload.rfid_f = movfinal.rfid || \" \"\n msg.payload.IdCentroServicio = movinicial.id_seleccionado\n msg.payload.vial = movinicial.vial\n msg.payload.totem = movinicial.totem\n msg.payload.ts_cierre = movfinal.timestamp\n msg.payload.IdCentroProveedor = flow.get(\"IdCentroProveedor\") \n msg.payload.imagen_entrada = movinicial.imagen || \" \"\n msg.payload.imagen_salida = movfinal.imagen || \" \"\n msg.payload.imagen_firma = movinicial.firma || \" \"\n flow.set(\"movimiento_cerrado_temp\", msg.payload)\n \n let msg_imagen = { topic: \"imagen_matricula\", filename: flow.get(\"path_images\") + movinicial.nombre_imagen }\n flow.set(\"imagen_matricula_rx\", false)\n node.send([msg_imagen, null, null])\n let msg_firma = {}\n node.warn(\"nombre firma = \" + movinicial.nombre_firma)\n flow.set(\"movinicial_firma\", false) // lo colacamos para saber en el proximo nodo si colocamos imagen en el envio hacia api\n if (movinicial.nombre_firma != \"No Habilitada\"){\n node.warn(\"2-nombre firma = \" + movinicial.nombre_firma)\n msg_firma = { topic: \"imagen_firma\", filename: flow.get(\"path_firmas\") + movinicial.nombre_firma }\n flow.set(\"imagen_firma_rx\", false)\n flow.set(\"movinicial_firma\", true) // utilizado en el proximo nodo para saber si existe firma en el movimiento inicial \n node.send([null, msg_firma, null])\n }\n else{\n flow.set(\"imagen_firma_rx\", true)\n }\n \n //flow.set(\"record_test\", msg.payload)\n \n let msg_final = {}\n // guardamos el movimiento cerrado en la tabla \"movimientos_cerrados\" y borramos el movimiento inicial de la tabla de \"movimientos\"\n\n const esc = s => (s ?? '').toString().replace(/'/g, \"''\"); // escapar comillas simples\n\n msg_final.topic = `\nINSERT INTO movimientos_cerrados(\n uuid, matricula, nombre_imagen_i, nombre_imagen_f, nombre_firma,\n horaentrada_i, horasalida_i, horaentrada_f, horasalida_f,\n pesoentrada, pesosalida, qr_i, rfid_i, qr_f, rfid_f, vial, totem, idseleccionado, sincro, ts_cierre\n) VALUES(\n '${esc(movinicial.uuid)}',\n '${esc(movinicial.matricula)}',\n '${esc(movinicial.nombre_imagen)}',\n '${esc(movfinal.nombre_imagen)}',\n '${esc(movinicial.nombre_firma)}',\n '${esc(movinicial.horaentrada)}',\n '${esc(movinicial.horasalida)}',\n '${esc(movfinal.horaentrada)}',\n '${esc(movfinal.horasalida)}',\n ${Number(movinicial.pesoestable) || 0},\n ${Number(movfinal.pesoestable) || 0},\n '${esc(movinicial.qr)}',\n '${esc(movinicial.rfid)}',\n '${esc(movfinal.qr)}',\n '${esc(movfinal.rfid)}', \n ${Number(movinicial.vial) || 0},\n '${esc(movinicial.totem)}',\n '${esc(movinicial.id_seleccionado)}',\n 0,\n ${Number(movfinal.timestamp) || 0});`;\n\n node.send([null, null, msg_final])\n \n flow.set(\"matriculafordelete\", movinicial.matricula)\n setTimeout(function () { // borramos el movimiento inicial en tabla de movimientos\n let msg_delete = {}\n msg_delete.topic = `DELETE FROM movimientos WHERE matricula = '${esc(flow.get(\"matriculafordelete\"))}'`;\n //node.send([null, null, msg_delete]) \n }, 1000)\n \n }\n else {\n //no hay movimiento de entrada asociado a esta matricula\n }\n}\nelse { // Aqui llegamos si tenemos que enviar el movimiento inicial como cerrado porque es un movimiento tarado\n msg.payload = flow.get(\"new_register\") // new register contiene el ultimo movimiento recibido por el supervisor (ver nodo comandos via websocket en el case movimientos)\n msg.payload.tarado = true\n}\nreturn;", + "outputs": 3, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1010, + "y": 1880, + "wires": [ + [ + "a1c2fb59c438c1c5" + ], + [ + "2e4cf9271d9a78d8" + ], + [ + "65bfea0283dfab05" + ] + ] + }, + { + "id": "b1b9ac8e8b543dbb", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "name": "link out 2", + "mode": "link", + "links": [ + "c858b37cb0eeba4f" + ], + "x": 1655, + "y": 1820, + "wires": [] + }, + { + "id": "0ba07aef43c4a226", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "name": "cerrar movimiento", + "links": [ + "8a7201a76e53897f" + ], + "x": 855, + "y": 1980, + "wires": [ + [ + "417e71d002f16583" + ] + ] + }, + { + "id": "cc229c10e3f4e423", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "mydb": "1c8613764b7f685e", + "name": "analisis", + "x": 780, + "y": 1880, + "wires": [ + [ + "405f569c2efbf22d", + "417e71d002f16583" + ] + ] + }, + { + "id": "65bfea0283dfab05", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "mydb": "1c8613764b7f685e", + "name": "guarda movimiento_c", + "x": 1260, + "y": 1980, + "wires": [ + [] + ] + }, + { + "id": "81f447b69bd2c603", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "6fd6ce44bb173af7", + "name": "function 1", + "func": "let mat = \"0131HLN\"\nmsg.topic = `\nWITH\nseeds AS (\n SELECT mi.id AS id_root\n FROM Matriculas_Identificadores mi\n WHERE UPPER(TRIM(mi.matricula)) = UPPER(TRIM('${mat}'))\n)\nSELECT\n 0 AS nivel,\n i.descripcion,\n i.id,\n NULL AS id_padre\nFROM Identificadores i\nJOIN seeds s ON i.id = s.id_root\n\nUNION ALL\n\nSELECT\n 1 AS nivel,\n c.descripcion,\n c.id,\n c.id_padre\nFROM Identificadores c\nJOIN seeds s ON c.id_padre = s.id_root\n\nORDER BY nivel, id;\n`;\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 960, + "y": 2220, + "wires": [ + [ + "e22e7f0f1cea9cb5" + ] + ] + }, + { + "id": "d2082aabd7021a2e", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "6fd6ce44bb173af7", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 730, + "y": 2220, + "wires": [ + [ + "81f447b69bd2c603" + ] + ] + }, + { + "id": "1d44274163bba745", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "6fd6ce44bb173af7", + "name": "debug 34", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1300, + "y": 2220, + "wires": [] + }, + { + "id": "17b55fb17b52609b", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "6fd6ce44bb173af7", + "name": "seed", + "func": "let mat = \"0131HLN\"\nmsg.topic = `\nSELECT mi.id AS id_root\nFROM Matriculas_Identificadores mi\nWHERE UPPER(TRIM(mi.matricula)) = UPPER(TRIM('${mat}')) ;\n`;\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 970, + "y": 2300, + "wires": [ + [ + "e22e7f0f1cea9cb5" + ] + ] + }, + { + "id": "f769af64083450ed", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "6fd6ce44bb173af7", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 730, + "y": 2300, + "wires": [ + [ + "17b55fb17b52609b" + ] + ] + }, + { + "id": "a550322a7e6cafd2", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "6fd6ce44bb173af7", + "name": "function 4", + "func": "let qr = msg.payload\nmsg.topic = `WITH RECURSIVE jerarquia AS (\n -- Primer nivel: comenzar con el id que recibimos\n SELECT id, id_padre, nivel, descripcion\n FROM Identificadores\n WHERE id = '${qr}'\n\n UNION ALL\n\n -- Subir en la jerarquía buscando el nivel 0\n SELECT i.id, i.id_padre, i.nivel, i.descripcion\n FROM Identificadores i\n JOIN jerarquia j ON i.id = j.id_padre\n WHERE i.nivel >= 0\n)\nSELECT\n -- La descripción del id original\n (SELECT descripcion FROM jerarquia WHERE id = '${qr}') AS descripcion_inicial,\n -- La descripción del primer registro con nivel 0 en la jerarquía ascendente\n (SELECT descripcion FROM jerarquia WHERE nivel = 0 LIMIT 1) AS descripcion_nivel_0;`;\n\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 960, + "y": 2380, + "wires": [ + [ + "e22e7f0f1cea9cb5" + ] + ] + }, + { + "id": "9e4bc4c1c4148e95", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "6fd6ce44bb173af7", + "name": "id", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "D3220E78-2338-4738-B0D2-499D0223EFFF", + "payloadType": "str", + "x": 730, + "y": 2380, + "wires": [ + [ + "a550322a7e6cafd2" + ] + ] + }, + { + "id": "e22e7f0f1cea9cb5", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "6fd6ce44bb173af7", + "mydb": "1c8613764b7f685e", + "name": "analisis", + "x": 1140, + "y": 2220, + "wires": [ + [ + "1d44274163bba745" + ] + ] + }, + { + "id": "6a066066436f56ef", + "type": "exec", + "z": "744bdf3a0e08d754", + "g": "e03fd8b52121427a", + "command": " ", + "addpay": "payload", + "append": "", + "useSpawn": "false", + "timer": "", + "oldrc": false, + "name": "Screen ON/OFF", + "x": 940, + "y": 2600, + "wires": [ + [], + [], + [] + ] + }, + { + "id": "245c965f41b35b43", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "e03fd8b52121427a", + "name": "screen off", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "echo 1 | sudo tee /sys/class/backlight/0-0045/bl_power", + "payloadType": "str", + "x": 720, + "y": 2580, + "wires": [ + [ + "6a066066436f56ef" + ] + ] + }, + { + "id": "bad546e70128edf8", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "e03fd8b52121427a", + "name": "screen on", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "echo 0 | sudo tee /sys/class/backlight/0-0045/bl_power", + "payloadType": "str", + "x": 720, + "y": 2540, + "wires": [ + [ + "6a066066436f56ef" + ] + ] + }, + { + "id": "ff7604277bbc1810", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "e03fd8b52121427a", + "name": "reboot", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "sudo shutdown -r now", + "payloadType": "str", + "x": 710, + "y": 2620, + "wires": [ + [ + "6a066066436f56ef" + ] + ] + }, + { + "id": "0a768660b1756f9a", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "e03fd8b52121427a", + "name": "poweroff", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "sudo poweroff", + "payloadType": "str", + "x": 720, + "y": 2660, + "wires": [ + [ + "6a066066436f56ef" + ] + ] + }, + { + "id": "1ca5e534fa4962de", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "e03fd8b52121427a", + "name": "reboot", + "links": [ + "e4f5cd90a27363a0" + ], + "x": 815, + "y": 2520, + "wires": [ + [ + "6a066066436f56ef" + ] + ] + }, + { + "id": "c61c445897350722", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "entrada", + "links": [ + "63dfdbc95bd896d0" + ], + "x": 655, + "y": 1440, + "wires": [ + [ + "182712df6587926f" + ] + ] + }, + { + "id": "7e099ddf6bfcb9fb", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "link in 2", + "links": [], + "x": 3775, + "y": 720, + "wires": [ + [ + "0f3cd31839136f10" + ] + ] + }, + { + "id": "dd15174f0db1d58c", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "610fa44452654078", + "name": "entrada uibuilder-1", + "links": [ + "58bb0cd40f89ba0d" + ], + "x": 3775, + "y": 640, + "wires": [ + [ + "0f3cd31839136f10" + ] + ] + }, + { + "id": "a1c2fb59c438c1c5", + "type": "file in", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "name": "imagen matricula", + "filename": "filename", + "filenameType": "msg", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 1250, + "y": 1860, + "wires": [ + [ + "dd4decb988977915" + ] + ] + }, + { + "id": "2e4cf9271d9a78d8", + "type": "file in", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "name": "imagen firma", + "filename": "filename", + "filenameType": "msg", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 1230, + "y": 1900, + "wires": [ + [ + "dd4decb988977915" + ] + ] + }, + { + "id": "dd4decb988977915", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "name": "mensaje para API", + "func": "if (msg.topic == \"imagen_matricula\"){\n flow.set(\"imagen_matricula_rx\", true)\n flow.set(\"imagen_leida_i\", msg.payload)\n}\nif (msg.topic == \"imagen_firma\"){\n flow.set(\"imagen_firma_rx\", true)\n flow.set(\"imagen_leida_firma\", msg.payload)\n}\nif (flow.get(\"imagen_matricula_rx\") && flow.get(\"imagen_firma_rx\") ){\n \n msg.payload = flow.get(\"movimiento_cerrado_temp\")\n msg.payload.imagen_entrada = flow.get(\"imagen_leida_i\")\n if (flow.get(\"movinicial_firma\")){\n msg.payload.imagen_firma = flow.get(\"imagen_leida_firma\")\n }\n node.send (msg)\n}\nreturn ;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1510, + "y": 1880, + "wires": [ + [ + "44c64f7eb7bdf346", + "b1b9ac8e8b543dbb" + ] + ] + }, + { + "id": "9fa72afe7d97828e", + "type": "file", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "guarda firmas", + "filename": "filename", + "filenameType": "msg", + "appendNewline": false, + "createDir": true, + "overwriteFile": "true", + "encoding": "none", + "x": 1400, + "y": 1600, + "wires": [ + [] + ] + }, + { + "id": "dc3fe6bd2494cab0", + "type": "https-node", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "Enviar a API", + "method": "POST", + "ret": "txt", + "paytoqs": "ignore", + "url": "", + "tls": "", + "persist": false, + "proxy": "", + "authType": "", + "senderr": false, + "x": 1390, + "y": 1720, + "wires": [ + [ + "4f30b9755ed381bb" + ] + ] + }, + { + "id": "1dae889e8697bf15", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "f0815d4d5c1e13e0", + "name": "UbicacionesInternas", + "func": "// Verifica que payload sea un array\nif (!Array.isArray(msg.payload.datos) || msg.payload.datos.length === 0) {\n node.warn(\"msg.payload.datos no es un array o está vacío.\");\n return null;\n}\n\n// Función para escapar comillas simples\nconst esc = s => (s ?? \"\").toString().replace(/'/g, \"''\");\n\n// Construimos las tuplas\nflow.set(\"fosos_size\", msg.payload.datos.length)\nconst valuesArray = msg.payload.datos.map(obj => {\n const id = esc(obj.id || \"\").trim().toUpperCase();\n const descripcion = obj.descripcion || \"\";\n \n return `('${id}', '${descripcion}')`;\n});\nflow.set(\"ubicacionesinternas_counter\", valuesArray.length )\n// Query final\n//Borra la tabla actual y si la tabla recibida es diferente de 0 records guarda la nueva tabla, si no pone \"\"\n// finalmente hace un commit\nmsg.topic = `\nSTART TRANSACTION;\nDELETE FROM UbicacionesInternas;\n${valuesArray.length ? `\nINSERT INTO UbicacionesInternas (id, descripcion)\nVALUES ${valuesArray.join(\",\")};\n` : \"\"}\nCOMMIT;\n`;\nmsg.payload = \"\"\nreturn msg;\n\n\n\n\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2840, + "y": 1780, + "wires": [ + [ + "7aee3c3ea10fd9a5", + "afd34cb6f61a6475" + ] + ] + }, + { + "id": "eca4e2e077d64f16", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "0a78f1a176dd7ad5", + "name": "UbicacionesInternas", + "func": "\nmsg.url = `api/Totem/SeleccionarUbicacionesInternas?token=${flow.get(\"tokenapi\")}&idCentroProveedor=${flow.get(\"IdCentroProveedor\") }`\nmsg.url = flow.get(\"url_API\") + msg.url \nmsg.tabla = \"UbicacionesInternas\"\nreturn msg;\n\n\n//https://pre-urbgesbas.urbaser.com/WebService/api/Totem/SeleccionarUbicacionesInternas?token=FDEB4C00-5F58-4C2D-AD80-9E7064D8FC4E&idCentroProveedor=02C9381D-D4A0-4503-B9A4-890BFABD9608\n\n//https://pre-urbgesbas.urbaser.com/WebService/api/Totem/SeleccionarMatriculasTotem?token=FDEB4C00-5F58-4C2D-AD80-9E7064D8FC4E&idCentroProveedor=A1ADAC83-AEFA-4E1F-8944-56722AC43F4F", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2420, + "y": 2720, + "wires": [ + [ + "e87018791b56046f" + ] + ] + }, + { + "id": "cc2d5ea437442925", + "type": "inject", + "z": "744bdf3a0e08d754", + "name": "UbicacionesInternas", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "UbicacionesInternas", + "payloadType": "str", + "x": 1850, + "y": 2800, + "wires": [ + [ + "c95d9408ac6b1595" + ] + ] + }, + { + "id": "a4b79d82d1b9e587", + "type": "inject", + "z": "576f3807f61f5098", + "g": "967aada409fe59f3", + "name": "Manual", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "MODE", + "payload": "MANUAL", + "payloadType": "str", + "x": 1760, + "y": 1020, + "wires": [ + [ + "06c0e45f61e0d364" + ] + ] + }, + { + "id": "b5cb8dc6dff15a6f", + "type": "inject", + "z": "576f3807f61f5098", + "g": "967aada409fe59f3", + "name": "Auto", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "MODE", + "payload": "AUTO", + "payloadType": "str", + "x": 1760, + "y": 1060, + "wires": [ + [ + "06c0e45f61e0d364" + ] + ] + }, + { + "id": "63ef47eeab45cd59", + "type": "inject", + "z": "576f3807f61f5098", + "g": "967aada409fe59f3", + "name": "S1 Green", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "MAN/SET_SEMS", + "payload": "{\"S1\":\"GREEN\", \"S2\":\"RED\", \"S3\":\"RED\"}", + "payloadType": "json", + "x": 1770, + "y": 1100, + "wires": [ + [ + "49a1dfc2d343bdbb" + ] + ] + }, + { + "id": "406737da0cea85cd", + "type": "inject", + "z": "576f3807f61f5098", + "g": "967aada409fe59f3", + "name": "S2 Green", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "MAN/SET_SEMS", + "payload": "{\"S1\":\"RED\", \"S2\":\"GREEN\", \"S3\":\"RED\"}", + "payloadType": "json", + "x": 1770, + "y": 1140, + "wires": [ + [ + "49a1dfc2d343bdbb" + ] + ] + }, + { + "id": "0626ad89ffb60829", + "type": "inject", + "z": "576f3807f61f5098", + "g": "967aada409fe59f3", + "name": "S3 Green", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "MAN/SET_SEMS", + "payload": "{\"S1\":\"RED\", \"S2\":\"RED\", \"S3\":\"GREEN\"}", + "payloadType": "json", + "x": 1770, + "y": 1180, + "wires": [ + [ + "49a1dfc2d343bdbb" + ] + ] + }, + { + "id": "18202cf1d5012bd6", + "type": "inject", + "z": "576f3807f61f5098", + "g": "967aada409fe59f3", + "name": "Todo RED", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "MAN/SET_SEMS", + "payload": "{\"S1\":\"RED\", \"S2\":\"RED\", \"S3\":\"RED\"}", + "payloadType": "json", + "x": 1770, + "y": 1220, + "wires": [ + [ + "49a1dfc2d343bdbb" + ] + ] + }, + { + "id": "e4e47d1f029304cb", + "type": "inject", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "Sim L1 ON", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "lazo1", + "payload": "1", + "payloadType": "num", + "x": 1000, + "y": 340, + "wires": [ + [ + "e4a843aa2e13ce4d" + ] + ] + }, + { + "id": "20a7ce11f2e8216a", + "type": "inject", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "Sim L1 OFF", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "lazo1", + "payload": "0", + "payloadType": "num", + "x": 1000, + "y": 380, + "wires": [ + [ + "e4a843aa2e13ce4d" + ] + ] + }, + { + "id": "e4a843aa2e13ce4d", + "type": "rbe", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "RBE L1", + "func": "rbe", + "gap": "", + "start": "", + "inout": "out", + "septopics": true, + "property": "payload", + "topi": "topic", + "x": 1160, + "y": 360, + "wires": [ + [ + "46f02d9f8d41ee03", + "c79d32e49f4cf252" + ] + ] + }, + { + "id": "46f02d9f8d41ee03", + "type": "function", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "L1 -> EV", + "func": "const v = Number(msg.payload)?1:0;\nflow.set('L1',v);\nmsg.topic = v ? 'EV/L1_ON' : 'EV/L1_OFF';\nmsg.payload = {src:'L1',value:v,ts:Date.now()};\nif(v == 0){\n node.status({fill:\"green\",shape:\"dot\",text:\"LIBRE\"});\n}\nelse{\n node.status({fill:\"red\",shape:\"dot\",text:\"OCUPADO\"});\n}\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1340, + "y": 360, + "wires": [ + [ + "c4159a812594fc62" + ] + ] + }, + { + "id": "d4986d948f5f65a7", + "type": "inject", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "Sim L2 ON", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "lazo2", + "payload": "1", + "payloadType": "num", + "x": 1000, + "y": 440, + "wires": [ + [ + "69f5087384c48122" + ] + ] + }, + { + "id": "15c2a5fd76e103ac", + "type": "inject", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "Sim L2 OFF", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "lazo2", + "payload": "0", + "payloadType": "num", + "x": 1000, + "y": 480, + "wires": [ + [ + "69f5087384c48122" + ] + ] + }, + { + "id": "69f5087384c48122", + "type": "rbe", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "RBE L2", + "func": "rbe", + "gap": "", + "start": "", + "inout": "out", + "septopics": true, + "property": "payload", + "topi": "topic", + "x": 1160, + "y": 460, + "wires": [ + [ + "f38d6d27851d0e2b" + ] + ] + }, + { + "id": "f38d6d27851d0e2b", + "type": "function", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "L2 -> EV", + "func": "const v = Number(msg.payload)?1:0;\nflow.set('L2',v);\nmsg.topic = v ? 'EV/L2_ON' : 'EV/L2_OFF';\nmsg.payload = {src:'L2',value:v,ts:Date.now()};\nif(v == 0){\n node.status({fill:\"green\",shape:\"dot\",text:\"LIBRE\"});\n}\nelse{\n node.status({fill:\"red\",shape:\"dot\",text:\"OCUPADO\"});\n}\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1340, + "y": 460, + "wires": [ + [ + "c4159a812594fc62" + ] + ] + }, + { + "id": "be21a62a431574d0", + "type": "inject", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "Sim L3 ON", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "lazo3", + "payload": "1", + "payloadType": "num", + "x": 1000, + "y": 540, + "wires": [ + [ + "fb6d5180204e6bce" + ] + ] + }, + { + "id": "0dde5e928354be6c", + "type": "inject", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "Sim L3 OFF", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "lazo3", + "payload": "0", + "payloadType": "num", + "x": 1000, + "y": 580, + "wires": [ + [ + "fb6d5180204e6bce" + ] + ] + }, + { + "id": "fb6d5180204e6bce", + "type": "rbe", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "RBE L3", + "func": "rbe", + "gap": "", + "start": "", + "inout": "out", + "septopics": true, + "property": "payload", + "topi": "topic", + "x": 1160, + "y": 560, + "wires": [ + [ + "918df2f63a69c6c9" + ] + ] + }, + { + "id": "918df2f63a69c6c9", + "type": "function", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "L3 -> EV", + "func": "const v = Number(msg.payload)?1:0;\nflow.set('L3',v);\nmsg.topic = v ? 'EV/L3_ON' : 'EV/L3_OFF';\nmsg.payload = {src:'L3',value:v,ts:Date.now()};\nif(v == 0){\n node.status({fill:\"green\",shape:\"dot\",text:\"LIBRE\"});\n}\nelse{\n node.status({fill:\"red\",shape:\"dot\",text:\"OCUPADO\"});\n}\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1340, + "y": 560, + "wires": [ + [ + "c4159a812594fc62" + ] + ] + }, + { + "id": "c4159a812594fc62", + "type": "link out", + "z": "576f3807f61f5098", + "g": "3276a28141ee1362", + "name": "EV BUS (desde LAZOS)", + "mode": "link", + "links": [ + "e7754eeae28a6256", + "d95551b3e1091e71" + ], + "x": 1525, + "y": 460, + "wires": [] + }, + { + "id": "a7c59fef860efa52", + "type": "inject", + "z": "576f3807f61f5098", + "g": "f4f4ca47869551e3", + "name": "Sim Peso 0", + "props": [ + { + "p": "payload.peso", + "v": "", + "vt": "num" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "", + "x": 1010, + "y": 720, + "wires": [ + [ + "29994ee556e17a85" + ] + ] + }, + { + "id": "e0b22a42652448d3", + "type": "inject", + "z": "576f3807f61f5098", + "g": "f4f4ca47869551e3", + "name": "Sim Peso 8000", + "props": [ + { + "p": "payload.peso", + "v": "8000", + "vt": "num" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "", + "x": 1020, + "y": 760, + "wires": [ + [ + "29994ee556e17a85" + ] + ] + }, + { + "id": "29994ee556e17a85", + "type": "function", + "z": "576f3807f61f5098", + "g": "f4f4ca47869551e3", + "name": "Peso -> EV/B_OCC (histeresis + confirm)", + "func": "const P_on = flow.get('P_on') ?? 2000;\nconst P_off = flow.get('P_off') ?? 1200;\nconst T = flow.get('T_confirm_ms') ?? 400;\n\nconst W = Number(msg.payload.peso); // lectura del peso\nflow.set('lastW', W);\nlet B = flow.get('B_OCC') ?? 0;\n\nfunction clearPending(){\n const p = flow.get('B_pending');\n if (p && p.tid) clearTimeout(p.tid);\n flow.set('B_pending', null);\n}\nfunction arm(kind){\n clearPending();\n const tid = setTimeout(() => {\n const ww = Number(flow.get('lastW') ?? W);\n let bb = flow.get('B_OCC') ?? 0;\n if (kind==='on' && bb===0 && ww>=P_on){\n flow.set('B_OCC',1);\n node.send({topic:'EV/B_OCC_ON', payload:{src:'W',W:ww,ts:Date.now()}});\n node.status({fill:\"red\",shape:\"dot\",text:\"OCUPADA\"});\n }\n if (kind==='off' && bb===1 && ww<=P_off){\n flow.set('B_OCC',0);\n node.send({topic:'EV/B_OCC_OFF', payload:{src:'W',W:ww,ts:Date.now()}});\n node.status({fill:\"green\",shape:\"dot\",text:\"LIBRE\"});\n }\n flow.set('B_pending', null);\n }, T);\n flow.set('B_pending', {kind, tid});\n}\n\nif (B===0){\n if (W>=P_on){\n const p=flow.get('B_pending');\n if (!p || p.kind!=='on') arm('on');\n } else {\n const p=flow.get('B_pending');\n if (p && p.kind==='on') clearPending();\n }\n} else {\n if (W<=P_off){\n const p=flow.get('B_pending');\n if (!p || p.kind!=='off') arm('off');\n } else {\n const p=flow.get('B_pending');\n if (p && p.kind==='off') clearPending();\n }\n}\nreturn null;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1320, + "y": 740, + "wires": [ + [ + "74ea72530de212a4" + ] + ] + }, + { + "id": "74ea72530de212a4", + "type": "link out", + "z": "576f3807f61f5098", + "g": "f4f4ca47869551e3", + "name": "EV BUS (desde PESO)", + "mode": "link", + "links": [ + "e7754eeae28a6256", + "d95551b3e1091e71" + ], + "x": 1535, + "y": 740, + "wires": [] + }, + { + "id": "04796f17dd930bad", + "type": "inject", + "z": "576f3807f61f5098", + "d": true, + "g": "6457918bac63ba59", + "name": "INIT", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.2, + "topic": "INIT", + "payload": "1", + "payloadType": "num", + "x": 1730, + "y": 340, + "wires": [ + [ + "c9d524190cbe7f96" + ] + ] + }, + { + "id": "c9d524190cbe7f96", + "type": "function", + "z": "576f3807f61f5098", + "d": true, + "g": "6457918bac63ba59", + "name": "Init params (ajusta aquí)", + "func": "flow.set('P_on', 2000);\nflow.set('P_off', 1200);\nflow.set('T_confirm_ms', 400);\n\nflow.set('N2', 5); flow.set('N3', 3); flow.set('N1', 2);\n// Prioridad / orden de servicio (WRR)\nflow.set('order', [3,1,2]) // Taller = 3; Entrada = 1: Fosos = 2\n// ventana tiempos de transito\nflow.set('t1_ms', 8000);\nflow.set('t2_ms', 8000);\nflow.set('t3_ms', 8000);\n// ventana tiempos de semaforo verde\nflow.set('g1_ms', 10000);\nflow.set('g2_ms', 10000);\nflow.set('g3_ms', 10000);\n\nflow.set('state','IDLE');\nflow.set('B_OCC',0);\nflow.set('L1',0); flow.set('L2',0); flow.set('L3',0);\n\n// Accesos bloqueados por timeout de verde (Gi)\n// Si blocked[1] = true, el acceso 1 NO puede recibir verde hasta que L1 haga OFF.\nflow.set('blocked', { 1:false, 2:false, 3:false });\n\nflow.set('mode', \"AUTO\")\n\nreturn {topic:'CMD/SET_SEMS', payload:{S1:'RED',S2:'RED',S3:'RED',ts:Date.now()}};", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1950, + "y": 340, + "wires": [ + [] + ] + }, + { + "id": "e7754eeae28a6256", + "type": "link in", + "z": "576f3807f61f5098", + "g": "6457918bac63ba59", + "name": "EV IN (FSM)", + "links": [ + "c4159a812594fc62", + "74ea72530de212a4", + "15131de35b7d0425", + "2dc673e9f13e0f0b" + ], + "x": 1715, + "y": 440, + "wires": [ + [ + "0e30fb1a5ee58746" + ] + ] + }, + { + "id": "0e30fb1a5ee58746", + "type": "function", + "z": "576f3807f61f5098", + "g": "6457918bac63ba59", + "name": "FSM Dispatcher", + "func": "/*\n Nodo Function: FSM Dispatcher (Despachador de estados)\n\n OBJETIVO\n --------\n Este nodo es el “cerebro” del sistema. Recibe eventos (EV/*) y decisiones del scheduler (RSP/PICK),\n mantiene el estado global de la máquina (FSM) y emite comandos (CMD/*) hacia:\n - Semáforos (CMD/SET_SEMS)\n - Timers (CMD/START_* y CMD/STOP_*)\n - Scheduler (CMD/PICK_NEXT y CMD/SERVED)\n\n IDEA GENERAL (cómo fluye todo)\n ------------------------------\n 1) Sensores (lazos y peso) generan eventos EV/*:\n EV/L1_ON, EV/L1_OFF, ..., EV/B_OCC_ON, EV/B_OCC_OFF\n 2) Timers generan EV/*:\n EV/Ti_TIMEOUT, EV/Gi_TIMEOUT\n 3) El scheduler responde con:\n RSP/PICK { i: 1|2|3 }\n 4) Este Dispatcher decide transiciones de estado y manda CMD/*:\n - A semáforos: CMD/SET_SEMS {S1,S2,S3}\n - A timers: CMD/START_Ti, CMD/STOP_Ti, CMD/START_Gi, CMD/STOP_Gi\n - A scheduler: CMD/PICK_NEXT {demands} y CMD/SERVED {i}\n\n SOBRE \"??\" (Nullish Coalescing)\n -------------------------------\n a ?? b => usa a si NO es null/undefined; si lo es, usa b.\n Se usa para inicializar variables de contexto sin pisar valores válidos como 0.\n\n VARIABLES EN flow CONTEXT (memoria global del flow)\n ---------------------------------------------------\n - flow.state : estado actual de la FSM (string)\n Estados usados:\n 'IDLE' : sistema libre para decidir a quién dar paso\n 'DANDO_1' : semáforo 1 en verde, esperando L1_OFF\n 'DANDO_2' : semáforo 2 en verde, esperando L2_OFF\n 'DANDO_3' : semáforo 3 en verde, esperando L3_OFF\n 'TRANS_1' : camión del acceso 1 en tránsito (timer T1 corriendo)\n 'TRANS_2' : camión del acceso 2 en tránsito (timer T2 corriendo)\n 'TRANS_3' : camión del acceso 3 en tránsito (timer T3 corriendo)\n 'OCC' : báscula ocupada (B_OCC=1), todos los semáforos en rojo\n\n - flow.B_OCC : 0/1 estado de ocupación de la báscula (lo mantiene el nodo de peso)\n - flow.L1, flow.L2, flow.L3 : 0/1 demanda actual (hay camión en el lazo)\n (los actualizan los nodos de lazo)\n\n NOTA: flow.cur se puede usar para depuración, pero aquí se guarda de forma implícita en state\n (cuando state es DANDO_2 o TRANS_2, el \"cur\" es 2).\n\n ENTRADAS (msg.topic)\n --------------------\n - EV/B_OCC_ON, EV/B_OCC_OFF\n - EV/L1_ON, EV/L1_OFF ... EV/L3_OFF\n - EV/T1_TIMEOUT ... EV/T3_TIMEOUT\n - EV/G1_TIMEOUT ... EV/G3_TIMEOUT\n - RSP/PICK (respuesta del scheduler)\n\n SALIDAS (3 puertos)\n -------------------\n Output 1: comandos a semáforos (CMD/SET_SEMS)\n Output 2: comandos a timers (CMD/START_*, CMD/STOP_*)\n (se emite como ARRAY de mensajes, para mandar varios START/STOP en un ciclo)\n Output 3: comandos a scheduler (CMD/PICK_NEXT, CMD/SERVED)\n (también puede ser ARRAY de mensajes)\n\n IMPORTANTE: filosofía del diseño\n -------------------------------\n - “Event-driven”: este nodo reacciona a eventos, no hace polling.\n - “Exclusión”: durante TRANS_i no se permite dar otro verde.\n - “Seguridad”: si B_OCC pasa a 1 en cualquier momento, se fuerza estado OCC.\n - “Competencia”: el scheduler decide a quién dar paso cuando estamos en IDLE y hay demanda.\n*/\n\n// ------------------------------\n// Helpers de lectura de estado\n// ------------------------------\nconst topic = msg.topic || '';\nconst now = Date.now();\n\n// --- Gestión de bloqueos ---\nlet blocked = flow.get('blocked') ?? {1:false,2:false,3:false};\n\n// Si un lazo pasa a OFF, lo desbloqueamos (se resetea el fallo)\nif (topic === 'EV/L1_OFF') blocked[1] = false;\nif (topic === 'EV/L2_OFF') blocked[2] = false;\nif (topic === 'EV/L3_OFF') blocked[3] = false;\n\nflow.set('blocked', blocked);\n\n// Devuelve demanda actual (hay camión esperando) según los lazos guardados en flow\nfunction getDemands() {\n const blocked = flow.get('blocked') ?? {1:false,2:false,3:false};\n\n return {\n 1: (flow.get('L1') ? 1 : 0) && !blocked[1] ? 1 : 0,\n 2: (flow.get('L2') ? 1 : 0) && !blocked[2] ? 1 : 0,\n 3: (flow.get('L3') ? 1 : 0) && !blocked[3] ? 1 : 0\n };\n}\n\n// ¿Hay algún lazo ocupado?\nfunction anyDemand(d) {\n return (d[1] || d[2] || d[3]) ? 1 : 0;\n}\n\n// Guardar estado de la FSM en flow\nfunction setState(st) {\n flow.set('state', st);\n}\n\n// ------------------------------\n// Helpers para construir comandos\n// ------------------------------\n\n// Comando a semáforos: un solo mensaje con los 3 estados\nfunction cmdSetSems(s1, s2, s3) {\n // Guardar también aquí el \"estado ordenado\" para que el status del dispatcher sea coherente\n flow.set('S1', s1);\n flow.set('S2', s2);\n flow.set('S3', s3);\n return { topic: 'CMD/SET_SEMS', payload: { S1: s1, S2: s2, S3: s3, ts: now } };\n}\n\n// Comando a timers: START/STOP_Ti o START/STOP_Gi\nfunction cmdTimer(topic) {\n return { topic, payload: { ts: now } };\n}\n\n// Comando al scheduler: pedir elección o notificar servido\nfunction cmdSched(topic, payload = {}) {\n return { topic, payload: { ...payload, ts: now } };\n}\n\n// Si es posible (báscula libre y hay demanda), pedimos al scheduler que elija el siguiente acceso.\nfunction requestPickIfPossible() {\n const B = flow.get('B_OCC') ? 1 : 0;\n const d = getDemands();\n if (!B && anyDemand(d)) {\n return cmdSched('CMD/PICK_NEXT', { demands: d });\n }\n return null;\n}\n\n// ------------------------------\n// Estado actual (con defaults)\n// ------------------------------\nlet state = flow.get('state') ?? 'IDLE';\nlet B_OCC = flow.get('B_OCC') ? 1 : 0;\n\n// Mantener B_OCC actualizado si entran eventos de ocupación (por robustez)\nif (topic === 'EV/B_OCC_ON') { flow.set('B_OCC', 1); B_OCC = 1; }\nif (topic === 'EV/B_OCC_OFF') { flow.set('B_OCC', 0); B_OCC = 0; }\n\n// Buffers de salida: podemos emitir varios comandos en una misma transición\nlet outSems = null; // 1 mensaje (o null)\nlet outTimers = []; // array de mensajes (o vacío)\nlet outSched = []; // array de mensajes (o vacío)\n\n// =========================================================\n// REGLA DE SEGURIDAD GLOBAL:\n// Si la báscula está ocupada, forzamos OCC y todos en ROJO.\n// =========================================================\nif (B_OCC && state !== 'OCC') {\n // Nota: aquí NO paramos timers explícitamente en esta versión (se podría).\n // La filosofía es: en OCC no se da paso a nadie, y cuando se libere se re-arbitra.\n outSems = cmdSetSems('RED', 'RED', 'RED');\n setState('OCC');\n state = 'OCC';\n}\n\n// =========================================================\n// ESTADO: OCC (báscula ocupada)\n// - mientras esté ocupada: nadie en verde\n// - cuando se libera: pasamos a IDLE y pedimos pick si hay demanda\n// =========================================================\nif (state === 'OCC') {\n if (topic === 'EV/B_OCC_OFF') {\n setState('IDLE');\n const pick = requestPickIfPossible();\n if (pick) outSched.push(pick);\n }\n updateStatus();\n return [\n outSems,\n outTimers.length ? outTimers : null,\n outSched.length ? outSched : null\n ];\n}\n\n// =========================================================\n// ESTADO: IDLE (sistema libre para decidir)\n// - si llega cualquier EV/*, intentamos pedir PICK_NEXT si procede\n// - si llega RSP/PICK, encendemos el verde del acceso elegido y arrancamos su G_i\n// =========================================================\nif (state === 'IDLE') {\n if (!B_OCC) {\n // Respuesta del scheduler: ya sabemos a quién dar paso\n if (topic === 'RSP/PICK' && msg.payload && msg.payload.i) {\n const i = Number(msg.payload.i);\n\n // Poner solo un semáforo en verde, el resto en rojo\n outSems = cmdSetSems(\n i === 1 ? 'GREEN' : 'RED',\n i === 2 ? 'GREEN' : 'RED',\n i === 3 ? 'GREEN' : 'RED'\n );\n\n // Arrancar timer de verde máximo del acceso elegido (Gi)\n outTimers.push(cmdTimer(`CMD/START_G${i}`));\n\n // Cambiar estado a DANDO_i\n setState(`DANDO_${i}`);\n }\n // Cualquier evento puede disparar un nuevo arbitraje\n else if (topic.startsWith('EV/')) {\n const pick = requestPickIfPossible();\n if (pick) outSched.push(pick);\n }\n }\n updateStatus();\n return [\n outSems,\n outTimers.length ? outTimers : null,\n outSched.length ? outSched : null\n ];\n}\n\n// =========================================================\n// ESTADO: DANDO_i\n// Semáforo i en VERDE, esperando que el camión SALGA del lazo (Li_OFF)\n// Eventos relevantes:\n// - EV/Li_OFF => cerrar verde, parar Gi, arrancar Ti, notificar served, pasar a TRANS_i\n// - EV/Gi_TIMEOUT => timeout verde (camión no se movió), forzar rojo, volver a IDLE y re-arbitrar\n// - EV/B_OCC_ON => (robustez) si se ocupa báscula, forzar OCC\n// =========================================================\n\nif (state.startsWith('DANDO_')) {\n const i = Number(state.split('_')[1]); // extrae i de \"DANDO_2\"\n\n if (topic === `EV/L${i}_OFF`) {\n // El camión salió del lazo -> cerramos verde inmediatamente\n outSems = cmdSetSems('RED', 'RED', 'RED');\n\n // Parar el timer de verde máximo (Gi)\n outTimers.push(cmdTimer(`CMD/STOP_G${i}`));\n\n // Arrancar el timer de tránsito (Ti)\n outTimers.push(cmdTimer(`CMD/START_T${i}`));\n\n // Notificar al scheduler que se sirvió un camión de i\n // (para contar cupos WRR cuando hay competencia)\n outSched.push(cmdSched('CMD/SERVED', { i }));\n\n // Pasar a estado de tránsito\n setState(`TRANS_${i}`);\n updateStatus();\n return [outSems, outTimers, outSched];\n }\n\n if (topic === `EV/G${i}_TIMEOUT`) {\n // Timeout de verde: el camión no salió del lazo\n // Por seguridad ponemos todo en rojo y reintentamos arbitraje\n outSems = cmdSetSems('RED', 'RED', 'RED');\n outTimers.push(cmdTimer(`CMD/STOP_G${i}`)); // por limpieza, aunque ya venció\n setState('IDLE');\n\n // Bloqueo del acceso i hasta que Li haga OFF\n let blocked = flow.get('blocked') ?? {1:false,2:false,3:false};\n blocked[i] = true;\n flow.set('blocked', blocked);\n \n const pick = requestPickIfPossible();\n if (pick) outSched.push(pick);\n updateStatus();\n return [\n outSems,\n outTimers.length ? outTimers : null,\n outSched.length ? outSched : null\n ];\n }\n\n if (topic === 'EV/B_OCC_ON') {\n // Si se detecta ocupación, forzamos OCC\n outSems = cmdSetSems('RED', 'RED', 'RED');\n outTimers.push(cmdTimer(`CMD/STOP_G${i}`));\n setState('OCC');\n updateStatus();\n return [outSems, outTimers, outSched.length ? outSched : null];\n }\n\n return [null, null, null];\n}\n\n// =========================================================\n// ESTADO: TRANS_i\n// Durante el tránsito no se da paso a otros accesos.\n// Eventos relevantes:\n// - EV/B_OCC_ON => la báscula se ocupó antes de expirar Ti -> parar Ti y pasar a OCC\n// - EV/Ti_TIMEOUT => expiró Ti; si sigue libre, volver a IDLE para re-arbitrar\n// =========================================================\nif (state.startsWith('TRANS_')) {\n const i = Number(state.split('_')[1]);\n\n if (topic === 'EV/B_OCC_ON') {\n // La báscula se ocupó: detener timer de tránsito y pasar a OCC\n outTimers.push(cmdTimer(`CMD/STOP_T${i}`));\n outSems = cmdSetSems('RED', 'RED', 'RED');\n setState('OCC');\n updateStatus();\n return [outSems, outTimers, null];\n }\n\n if (topic === `EV/T${i}_TIMEOUT`) {\n // Transito agotado: si báscula sigue libre, volver a IDLE para dar paso al siguiente\n if (!B_OCC) {\n setState('IDLE');\n const pick = requestPickIfPossible();\n if (pick) outSched.push(pick);\n updateStatus();\n return [null, null, outSched.length ? outSched : null];\n }\n }\n updateStatus();\n return [null, null, null];\n}\n\n// Si por alguna razón llegamos aquí, no hacemos nada\nreturn [null, null, null];\n\n\n//***************************** FUNCION PARA VER STATUS ***********************************************************\n\nfunction updateStatus() {\n // --- StatusText para el Dispatcher (incluye bloqueos) ---\n\nconst state = flow.get('state') ?? 'IDLE';\nconst B = flow.get('B_OCC') ? 1 : 0;\n\nconst L1 = flow.get('L1') ? 1 : 0;\nconst L2 = flow.get('L2') ? 1 : 0;\nconst L3 = flow.get('L3') ? 1 : 0;\n\nconst blocked = flow.get('blocked') ?? {1:false,2:false,3:false};\nconst BLK = `${blocked[1]?1:0}${blocked[2]?1:0}${blocked[3]?1:0}`;\n\n// Si guardas estados de semáforos en flow desde el handler\nconst S1 = flow.get('S1') ?? 'RED';\nconst S2 = flow.get('S2') ?? 'RED';\nconst S3 = flow.get('S3') ?? 'RED';\n\n// Emojis (🟥 suele verse rojo consistente en node.status)\nconst dot = (c) => (c === 'GREEN' ? '🟢' : '🟥');\n\nlet statusText =\n ` st:${state} ` +\n ` B:${B} ` +\n ` L:${L1}${L2}${L3} ` +\n ` BLK:${BLK} ` +\n ` S:${dot(S1)}${dot(S2)}${dot(S3)}`;\n\n// Opcional: último peso si lo guardas como lastW\nconst lastW = flow.get('lastW');\nif (lastW !== undefined && lastW !== null) statusText += ` W:${lastW}`;\n\nnode.status({\n fill: B ? 'red' : 'green',\n shape: 'dot',\n text: statusText\n});\n\n}\n", + "outputs": 3, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1960, + "y": 440, + "wires": [ + [ + "b017ceea4285eb76" + ], + [ + "798cfaee0987cdca" + ], + [ + "67e977418231666d" + ] + ] + }, + { + "id": "67e977418231666d", + "type": "link out", + "z": "576f3807f61f5098", + "g": "6457918bac63ba59", + "name": "CMD -> SCHED", + "mode": "link", + "links": [ + "606dc6f9453f5e0d" + ], + "x": 2195, + "y": 440, + "wires": [] + }, + { + "id": "798cfaee0987cdca", + "type": "link out", + "z": "576f3807f61f5098", + "g": "6457918bac63ba59", + "name": "CMD -> TIMERS", + "mode": "link", + "links": [ + "00ce5615a6247bec" + ], + "x": 2195, + "y": 400, + "wires": [] + }, + { + "id": "b017ceea4285eb76", + "type": "link out", + "z": "576f3807f61f5098", + "g": "6457918bac63ba59", + "name": "CMD -> SEMS", + "mode": "link", + "links": [ + "8da927c794b40546" + ], + "x": 2195, + "y": 360, + "wires": [] + }, + { + "id": "606dc6f9453f5e0d", + "type": "link in", + "z": "576f3807f61f5098", + "g": "e51291e8f6dc7282", + "name": "CMD IN (SCHED)", + "links": [ + "67e977418231666d" + ], + "x": 1685, + "y": 580, + "wires": [ + [ + "891cfa9c8d6e464d" + ] + ] + }, + { + "id": "891cfa9c8d6e464d", + "type": "function", + "z": "576f3807f61f5098", + "g": "e51291e8f6dc7282", + "name": "Scheduler WRR 2>3>1", + "func": "const t=msg.topic||'';\nconst order = flow.get('order') ?? [2,3,1];\nconst quota={2:(flow.get('N2')??5),3:(flow.get('N3')??3),1:(flow.get('N1')??2)};\nlet turn=context.get('turn')??0;\nlet served=context.get('served')??0;\nlet competitive=context.get('competitive')??false;\n\nfunction save(){context.set('turn',turn);context.set('served',served);context.set('competitive',competitive);}\n\nif (t==='CMD/SERVED'){\n const i=Number(msg.payload?.i);\n if (competitive && i===order[turn]) served++;\n save();\n return null;\n}\n\nif (t!=='CMD/PICK_NEXT') return null;\nconst d=msg.payload?.demands || {1:flow.get('L1')?1:0,2:flow.get('L2')?1:0,3:flow.get('L3')?1:0};\nconst act=[1,2,3].filter(i=>d[i]);\ncompetitive = act.length>1;\n\nif (act.length===0){save();return null;}\nif (!competitive){save();return {topic:'RSP/PICK',payload:{i:act[0]}};}\n\nfor (let k=0;k<3;k++){\n const i=order[turn];\n if (d[i] && served,\n \"G2\": ,\n ...\n }\n\n NOTAS IMPORTANTES\n -----------------\n 1) START \"reinicia\" el timer:\n si el timer ya existía, se cancela y se vuelve a arrancar.\n 2) STOP cancela el timer si estaba activo.\n 3) Al expirar, el timer se auto-limpia del objeto context.timers\n 4) node.send() emite el evento de timeout hacia el BUS (link out)\n\n ENTRADA / SALIDA\n ----------------\n Entrada: msg.topic con alguno de los CMD/START_* o CMD/STOP_*\n Salida: no retorna msg (return null). Emite eventos con node.send() al expirar.\n*/\n\nconst topic = msg.topic || \"\";\n\n/* -------------------------\n Leer configuración (ms)\n -------------------------\n Si no existen en flow, se usan defaults.\n Ajusta estos defaults si quieres.\n*/\nconst cfg = {\n T1: flow.get(\"t1_ms\") ?? 8000,\n T2: flow.get(\"t2_ms\") ?? 6000,\n T3: flow.get(\"t3_ms\") ?? 7000,\n G1: flow.get(\"g1_ms\") ?? 30000,\n G2: flow.get(\"g2_ms\") ?? 30000,\n G3: flow.get(\"g3_ms\") ?? 30000\n};\n\n/* -------------------------\n Parsear comando\n -------------------------\n topic esperado: CMD/START_T1, CMD/STOP_G3, etc.\n Regex:\n ^CMD/(START|STOP)_([TG])(\\d)$\n - Grupo1: START o STOP\n - Grupo2: 'T' o 'G'\n - Grupo3: '1' '2' '3'\n*/\nconst m = topic.match(/^CMD\\/(START|STOP)_([TG])(\\d)$/);\nif (!m) {\n // No es un comando de timer, lo ignoramos\n return null;\n}\n\nconst action = m[1]; // \"START\" | \"STOP\"\nconst family = m[2]; // \"T\" | \"G\"\nconst index = m[3]; // \"1\" | \"2\" | \"3\"\nconst key = family + index; // \"T1\"...\"T3\" o \"G1\"...\"G3\"\n\n/* -------------------------\n Almacén de timers\n -------------------------\n Usamos context (no flow) porque:\n - context se asocia al nodo y vive mientras el runtime está activo\n - evita colisiones con otros nodos\n*/\nlet timers = context.get(\"timers\") || {};\n\n/* -------------------------\n Helpers: cancelar / arrancar\n ------------------------- */\n\n// Cancela un timer si existe y lo borra del mapa\nfunction clearTimer(k) {\n if (timers[k]) {\n clearTimeout(timers[k]);\n delete timers[k];\n }\n}\n\n// Arranca un timer (reinicia si ya existía)\n// Cuando expira: emite EV/Ti_TIMEOUT o EV/Gi_TIMEOUT\nfunction armTimer(k, ms) {\n clearTimer(k);\n\n timers[k] = setTimeout(() => {\n // Construcción del topic de evento:\n // k=\"T1\" -> EV/T1_TIMEOUT\n // k=\"G3\" -> EV/G3_TIMEOUT\n const evTopic = `EV/${k}_TIMEOUT`;\n\n node.send({\n topic: evTopic,\n payload: {\n src: \"TIMER\",\n key: k, // \"T1\"...\"T3\" / \"G1\"...\"G3\"\n ms: ms, // duración programada\n ts: Date.now() // timestamp de expiración\n }\n });\n\n // Limpieza: el timer ya expiró\n clearTimer(k);\n context.set(\"timers\", timers);\n }, ms);\n}\n\n/* -------------------------\n Ejecutar acción\n ------------------------- */\n\nif (action === \"STOP\") {\n clearTimer(key);\n context.set(\"timers\", timers);\n return null;\n}\n\nif (action === \"START\") {\n const ms = cfg[key];\n\n // Defensa: si ms no es número o es <=0, evitamos armar un timer inválido\n if (!Number.isFinite(ms) || ms <= 0) {\n node.warn(`Timer ${key} no arrancado: duración inválida (${ms})`);\n return null;\n }\n\n armTimer(key, ms);\n context.set(\"timers\", timers);\n return null;\n}\n\nreturn null;\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1880, + "y": 720, + "wires": [ + [ + "2dc673e9f13e0f0b" + ] + ] + }, + { + "id": "2dc673e9f13e0f0b", + "type": "link out", + "z": "576f3807f61f5098", + "g": "fdde9120f66c6c06", + "name": "EV BUS (desde TIMERS)", + "mode": "link", + "links": [ + "e7754eeae28a6256", + "d95551b3e1091e71" + ], + "x": 2045, + "y": 720, + "wires": [] + }, + { + "id": "8da927c794b40546", + "type": "link in", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "CMD IN (SEMS)", + "links": [ + "b017ceea4285eb76", + "7c6babaed4914e41" + ], + "x": 1715, + "y": 920, + "wires": [ + [ + "49a1dfc2d343bdbb" + ] + ] + }, + { + "id": "bc4418d271c32af0", + "type": "function", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "Semaforos", + "func": "if (msg.topic!=='CMD/SET_SEMS') return null;\nflow.set('S1',msg.payload.S1);\nflow.set('S2',msg.payload.S2);\nflow.set('S3',msg.payload.S3);\n// Mapeo de datos a Modbus.\n// Estados (verde=true, rojo=false)\nlet s1 = (msg.payload.S1 === \"GREEN\"); // true si \"GREEN\" false si es diferente\nlet s2 = (msg.payload.S2 === \"GREEN\");\nlet s3 = (msg.payload.S3 === \"GREEN\");\n// Canales (0..5)\nlet ch1 = Number(flow.get(\"DO_semaforo1\")); // aseguramos que contienen numeros\nlet ch2 = Number(flow.get(\"DO_semaforo2\"));\nlet ch3 = Number(flow.get(\"DO_semaforo3\"));\n\nfunction validCh(ch) {\n return Number.isInteger(ch) && ch >= 0 && ch <= 5; \n}\n// chequeamos que los canales estan dentro del rango del dispositivo \nif (!validCh(ch1) || !validCh(ch2) || !validCh(ch3)) {\n node.warn(`Canal inválido: ch1=${ch1}, ch2=${ch2}, ch3=${ch3} (debe ser 0..5)`);\n return null;\n}\n\n// Detectar canales repetidos\nlet used = new Map();\nfor (const [name, ch] of [[\"S1\", ch1], [\"S2\", ch2], [\"S3\", ch3]]) {\n if (used.has(ch)) node.warn(`Conflicto: ${name} y ${used.get(ch)} usan el mismo canal ${ch}`);\n else used.set(ch, name);\n}\n\nconst unitid = flow.get(\"adam_id\");\nconst baseAddr = 16;\n\nlet m1 = { payload: { fc: 5, unitid: unitid, address: baseAddr + ch1, value: s1, quantity: 1} };\nlet m2 = { payload: { fc: 5, unitid: unitid, address: baseAddr + ch2, value: s2, quantity: 1 } };\nlet m3 = { payload: { fc: 5, unitid: unitid, address: baseAddr + ch3, value: s3, quantity: 1 } };\n\n// Devuelve 3 mensajes (un output): se enviarán en orden\nreturn [[m1, m2, m3]];\n\n\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2140, + "y": 940, + "wires": [ + [ + "e6e4d33e9470fbca", + "2f0ca87e8afc0d73", + "d8112ca72e0a342d" + ] + ] + }, + { + "id": "e6e4d33e9470fbca", + "type": "debug", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "DBG Semaforos", + "active": true, + "tosidebar": true, + "complete": "true", + "targetType": "full", + "x": 2370, + "y": 940, + "wires": [] + }, + { + "id": "2f0ca87e8afc0d73", + "type": "function", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "Status", + "func": "\nlet flag1 = flow.get('S1') || \"RED\";\nlet flag2 = flow.get('S2') || \"RED\";\nlet flag3 = flow.get(\"S3\") || \"RED\";\n\n// Creamos el texto con un dot verde o rojo por flag\nlet statusText = `S1:${flag1 === \"GREEN\" ? '🟢' : '🔴'} S2:${flag2 === \"GREEN\" ? '🟢' : '🔴'} S3:${flag3 === \"GREEN\" ? '🟢' : '🔴'} `;\n\nnode.status({\n fill: \"grey\", \n shape: \"dot\", // Mantén el shape como \"dot\" para no romper estilo\n text: statusText\n});\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2340, + "y": 880, + "wires": [ + [] + ] + }, + { + "id": "06c0e45f61e0d364", + "type": "function", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "Manual / Auto", + "func": "// Espera: msg.payload = \"AUTO\" o \"MANUAL\"\nconst m = (String(msg.payload || '')).toUpperCase();\nif (m !== 'AUTO' && m !== 'MANUAL') return null;\n\nflow.set('mode', m);\nlet statusText = `Modo: ${m} `\nlet color = \"red\"\nif (m === \"AUTO\") color = \"green\";\nnode.status({\n fill: color, \n shape: \"dot\", // Mantén el shape como \"dot\" para no romper estilo\n text: statusText\n});\n\n// Opcional: al entrar en MANUAL forzar rojo por seguridad\nif (m === 'MANUAL') {\n return { topic: 'MAN/SET_SEMS', payload: { S1:'RED', S2:'RED', S3:'RED', reason:'enter_manual', ts:Date.now() } };\n}\n\n// Al volver a AUTO, puedes forzar un re-sync:\n// (a) mandar el último autoSems conocido\nconst autoS = flow.get('autoSems');\nif (autoS) return { topic:'CMD/SET_SEMS', payload:{...autoS, reason:'back_to_auto', ts:Date.now()} };\n\n// (b) o simplemente no mandar nada y dejar que el dispatcher lo haga en el siguiente evento\n\nreturn null;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1930, + "y": 880, + "wires": [ + [ + "49a1dfc2d343bdbb" + ] + ] + }, + { + "id": "49a1dfc2d343bdbb", + "type": "function", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "Selector", + "func": "const mode = flow.get('mode') ?? 'AUTO';\nconst t = msg.topic || '';\n\nconst B = flow.get('B_OCC') ? 1 : 0;\n\n// Guardar “lo último pedido” por cada canal (útil para debug)\nif (t === 'CMD/SET_SEMS') flow.set('autoSems', msg.payload);\nif (t === 'MAN/SET_SEMS') flow.set('manualSems', msg.payload);\n\n// Seguridad opcional (muy recomendada):\n// Si báscula ocupada, no permitas ningún verde (ni en manual)\nfunction forceNoGreen(p){\n return { S1:'RED', S2:'RED', S3:'RED', ts:Date.now(), reason:'B_OCC_safety' };\n}\n\n// ---- MODO AUTO ----\nif (mode === 'AUTO') {\n if (t === 'CMD/SET_SEMS') {\n // En auto, pasan comandos automáticos\n // (si quieres seguridad dura:)\n if (B) msg.payload = forceNoGreen(msg.payload);\n return msg;\n }\n // En auto, ignoramos comandos manuales\n return null;\n}\n\n// ---- MODO MANUAL ----\nif (mode === 'MANUAL') {\n if (t === 'MAN/SET_SEMS') {\n // convertimos MAN/SET_SEMS a CMD/SET_SEMS para que el handler lo acepte\n msg.topic = 'CMD/SET_SEMS';\n if (B) msg.payload = forceNoGreen(msg.payload);\n return msg;\n }\n // En manual, ignoramos CMD/SET_SEMS del dispatcher\n return null;\n}\n\nreturn null;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1970, + "y": 940, + "wires": [ + [ + "bc4418d271c32af0", + "4878484f33b6ea38" + ] + ] + }, + { + "id": "4878484f33b6ea38", + "type": "debug", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "debug 2", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2150, + "y": 880, + "wires": [] + }, + { + "id": "d95551b3e1091e71", + "type": "link in", + "z": "576f3807f61f5098", + "g": "4a6a83419193ac56", + "name": "EV IN (DEBUG)", + "links": [ + "c4159a812594fc62", + "74ea72530de212a4", + "15131de35b7d0425", + "2dc673e9f13e0f0b" + ], + "x": 2215, + "y": 720, + "wires": [ + [ + "3e36b8139450735b" + ] + ] + }, + { + "id": "3e36b8139450735b", + "type": "debug", + "z": "576f3807f61f5098", + "g": "4a6a83419193ac56", + "name": "DBG Eventos", + "active": false, + "tosidebar": true, + "complete": "true", + "targetType": "full", + "x": 2435, + "y": 720, + "wires": [] + }, + { + "id": "8dec1861e45c773b", + "type": "modbus-flex-getter", + "z": "576f3807f61f5098", + "name": "", + "showStatusActivities": false, + "showErrors": false, + "showWarnings": true, + "logIOActivities": false, + "server": "a00b83865450b74d", + "useIOFile": false, + "ioFile": "", + "useIOForPayload": false, + "emptyMsgOnFail": false, + "keepMsgProperties": false, + "delayOnStart": false, + "startDelayTime": "", + "x": 310, + "y": 460, + "wires": [ + [ + "130015e21da95201" + ], + [ + "c47982766953c4b0" + ] + ] + }, + { + "id": "e19c952702b3de9b", + "type": "inject", + "z": "576f3807f61f5098", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{ \"fc\": 2, \"unitid\": 1, \"address\": \"0\", \"quantity\": 3 }", + "payloadType": "json", + "x": 130, + "y": 460, + "wires": [ + [ + "8dec1861e45c773b" + ] + ] + }, + { + "id": "c47982766953c4b0", + "type": "modbus-response", + "z": "576f3807f61f5098", + "name": "", + "registerShowMax": 20, + "x": 510, + "y": 540, + "wires": [] + }, + { + "id": "3845edd7f91846ec", + "type": "debug", + "z": "576f3807f61f5098", + "name": "debug 31", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 780, + "y": 260, + "wires": [] + }, + { + "id": "d8112ca72e0a342d", + "type": "modbus-flex-write", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "", + "showStatusActivities": false, + "showErrors": false, + "showWarnings": true, + "server": "a00b83865450b74d", + "emptyMsgOnFail": false, + "keepMsgProperties": false, + "delayOnStart": false, + "startDelayTime": "", + "x": 2380, + "y": 1000, + "wires": [ + [], + [ + "b6317c2d3d6d070c" + ] + ] + }, + { + "id": "dd43a77de63f2443", + "type": "inject", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{ \"value\":[false,true,true], \"fc\": 15, \"unitid\": 1, \"address\": 16, \"quantity\": 3}", + "payloadType": "json", + "x": 2190, + "y": 1000, + "wires": [ + [ + "d8112ca72e0a342d" + ] + ] + }, + { + "id": "c42bdd023acc8dc3", + "type": "uibuilder", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "", + "topic": "", + "url": "semaforos", + "okToGo": true, + "fwdInMessages": false, + "allowScripts": false, + "allowStyles": false, + "copyIndex": true, + "templateFolder": "blank", + "extTemplate": "", + "showfolder": false, + "reload": true, + "sourceFolder": "src", + "deployedVersion": "7.4.3", + "showMsgUib": false, + "title": "", + "descr": "", + "editurl": "vscode://file/home/trypton/.node-red/projects/raspipeso20_10_25/uibuilder/semaforos/?windowId=_blank", + "x": 3050, + "y": 500, + "wires": [ + [ + "4333f08dfc27e390", + "ad67876e3fd4b4bb" + ], + [] + ] + }, + { + "id": "85b34266c740174d", + "type": "inject", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{\"payload\":{\"pantalla\":\"menu-principal\"}}", + "payloadType": "json", + "x": 2860, + "y": 380, + "wires": [ + [ + "c42bdd023acc8dc3" + ] + ] + }, + { + "id": "4333f08dfc27e390", + "type": "function", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "buttons \\n decoder", + "func": "\nif (msg.topic === \"tipoconfig\"){\n let tiposDatosGlobal = flow.get(\"tiposDatosGlobal\") || {\"1\": [], \"2\": [], \"3\": [], \"4\": [], \"5\": [], \"6\": []};\n\nconst tipo = msg.payload.tipo; // ej: 2\nconst datos = msg.payload.datos; // array con los parámetros\n\ntiposDatosGlobal[tipo] = datos;\n\nflow.set(\"tiposDatosGlobal\", tiposDatosGlobal);\n\nreturn ;\n}\nelse if (msg.payload.hasOwnProperty(\"seccion\")) {\n\n\n /* Los diferentes \"case\" corresponden a la pulsacion de botones de los diferentes menus de configuracion\n Cada menu solicitado mediante el click de un boton devuelve el nombre del \"div\" en el archivo html que debemos habilitar\n para que se muestre en pantalla.\n Si la entrada no consigue ningun case que se ajuste entonces caemos en el \"default\" y ahi buscamos primero si hay\n algun mensaje en \"origen\", lo que corresponde a la actualizacion de alguna de las tablas dinamicas que se generan\n para modificar cada tipo de parametro. Luego se revisan los \"modalCallback\" los cuales no indican que la respuesta\n de la notificacion de alguna accion importante ha sido positiva (\"SI\").\n \n */\n\n switch (msg.payload.seccion) {\n\n case \"menu-principal\": // boton de configuracion en pantalla principal para acceder al menu principal \n flow.set(\"snapshot_cam\", false)\n flow.set(\"escuchar_cam\", false)\n return { payload: { pantalla: \"menu-principal\" } }\n\n case \"editar\": // boton para acceder al menu de edicion de la configuracion\n return { payload: { pantalla: \"editar\" } }\n\n case \"pruebas\": // boton para acceder al menu de pruebas del sistema\n return { payload: { pantalla: \"pruebas\" } }\n\n case \"semaforo\": // boton para acceder al menu prueba del semaforo\n return { payload: { pantalla: \"semaforo\" } }\n\n case \"otras\": // boton para acceder al menu de otras acciones\n return { payload: { pantalla: \"otras\" } }\n\n case \"regresar\": // boton para regresar al menu principal desde otras acciones y desde menu de pruebas del sistema\n return { payload: { pantalla: \"menu-principal\" } }\n\n case \"salvar\": // boton para almacenar los cambios en el archivo de configuracion en el menu de edicion de la configuracion\n return ([{ payload: { pantalla: \"editar\" }, modalText: \"Desea MODIFICAR el archivo de CONFIGURACION ?\", modalCallback: \"modificar\" }, null, null, null])\n\n case \"cancelar\": // boton para cancelar y salir del menu de editar configuracion\n return ([{ payload: { pantalla: \"menu-principal\" } }, null])\n\n case \"semaforo_on\": // boton para poner el semaforo en rojo\n flow.set(\"semaforo\", \"red\")\n return ([{ payload: { pantalla: \"semaforo\" }, semaforo: \"firebrick\" }, null, { payload: 0 }])\n\n case \"semaforo_off\": // boton para poner el semaforo en verde\n flow.set(\"semaforo\", \"green\")\n return ([{ payload: { pantalla: \"semaforo\" }, semaforo: \"green\" }, null, { payload: 1 }])\n\n case \"salir-semaforo\": // boton de salida de la pantalla de prueba del semaforo\n return ({ payload: { pantalla: \"pruebas\" } })\n\n case \"barrera\": // boton de activar barrera\n return ({ payload: { pantalla: \"barrera\" }, barrera: false })\n\n case \"general\": // boton para ver / modificar tabla con los parametros generales\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"general\", tabla: flow.get(\"config_general\") }, null, null])\n\n case \"camara\": // boton para ver / modificar tabla con los parametros de la camara\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"camara\", tabla: flow.get(\"config_camara\") }, null, null])\n\n case \"conexion\": // boton para ver / modificar tabla con los parametros de conexion\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"conexion\", tabla: flow.get(\"config_conex\") }, null, null])\n\n case \"sensores\": // boton para ver / modificar tabla con los parametros de sensores\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"sensores\", tabla: flow.get(\"config_sensor\") }, null, null])\n\n case \"mensajes\": // boton para ver / modificar tabla con los parametros de los mensajes\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"mensajes\", tabla: flow.get(\"config_mensajes\") }, null, null])\n\n case \"tiempos\": // boton para ver / modificar tabla con los parametros del tiempo\n return ([{ payload: { pantalla: \"tabla-general\" }, topic: \"tiempos\", tabla: flow.get(\"config_tiempos\") }, null, null])\n\n case \"aplicar\": // boton para aplicar la configuracion al RPi, en el menu-principal\n return ([{ payload: { pantalla: \"menu-principal\" }, topic: \"menu-principal\", modalText: \"Desea Recargar la CONFIGURACION ?\", modalCallback: \"recargar\" }, null, null])\n\n case \"reboot\": // boton para resetear el RPi en el menu de otras acciones\n return ([{ payload: { pantalla: \"otras\" }, topic: \"otras\", modalText: \"Desea Reiniciar el TOTEM ?\", modalCallback: \"reiniciar\" }, null, null])\n\n case \"apagar\": // boton para apagar el RPi en el menu de otras acciones\n return ([{ payload: { pantalla: \"otras\" }, topic: \"otras\", modalText: \"Desea APAGAR el TOTEM ?\", modalCallback: \"apagar\" }, null, null])\n\n case \"snapshot\": // boton para pedir snapshot\n flow.set(\"snapshot_cam\", true)\n flow.set(\"escuchar_cam\", false)\n let msgrqt = {}\n msgrqt.payload = {}\n msgrqt.url = \"http://\" + flow.get(\"camara_IP\") + \"/cgi-bin/trafficSnap.cgi?action=manSnap&channel=1\"\n msgrqt.topic = \"snapshot_rqtd\"\n return ([{ payload: { pantalla: \"snapshot\" }, topic: \"snapshot\" }, null, null, null, null, msgrqt])\n\n case \"escuchar\": // boton para escuchar camara ( recibir todas la imagenes que envie)\n flow.set(\"snapshot_cam\", false)\n flow.set(\"escuchar_cam\", true)\n return ([{ payload: { pantalla: \"escuchar\" }, topic: \"escuchar\" }, null, null])\n\n case \"left\": // Boton para ir a la imagen anterior en escuchar camara\n flow.set(\"getfromarray\", \"left\")\n return ([{ payload: { pantalla: \"escuchar\" }, topic: \"escuchar\" }, null, null])\n\n case \"right\": // Boton para ir a la siguiente imagen en escuchar camara\n flow.set(\"getfromarray\", \"right\")\n return ([{ payload: { pantalla: \"escuchar\" }, topic: \"escuchar\" }, null, null])\n\n case \"salir-snapshot\": // boton de salida de la pantalla de ver snapshot\n flow.set(\"snapshot_cam\", false)\n flow.set(\"escuchar_cam\", false)\n return ({ payload: { pantalla: \"pruebas\" } })\n\n case \"salir-barrera\": // boton de salida de la barrera\n\n return ({ payload: { pantalla: \"pruebas\" }, barrera: false })\n\n case \"salir-escuchar\": // boton de salida de la pantalla de escuchar la camara\n flow.set(\"snapshot_cam\", false)\n flow.set(\"escuchar_cam\", false)\n flow.set(\"snapshot_array\", [])\n flow.set(\"snapshot_readindex\", 0)\n flow.set(\"snapshot_saveindex\", 0)\n flow.set(\"snapshot_indexmax\", 0)\n flow.set(\"getfromarray\", \"\")\n return ({ payload: { pantalla: \"pruebas\" } })\n\n case \"activar-barrera\": // boton de activar barrera\n node.send([{ payload: { pantalla: \"barrera\" }, barrera: true }, null, null, null, null, null, null, { payload: 0 }])\n setTimeout(function () {\n node.send([null, null, null, null, null, null, null, { payload: 1 }])\n }, flow.get(\"relay2_pulse\"));\n return\n\n case \"cancel-table\": // boton de CANCEL de las tablas de configuracion de parametros\n return ({ payload: { pantalla: \"editar\" } })\n\n case \"salir\": // boton de salida del menu principal\n return ({ payload: { pantalla: \"menu-principal\" } })\n\n case \"tipos\": // boton de salida al menu editar\n msg.payload = {pantalla: \"tipos\", tiposDatos: flow.get(\"tiposDatosGlobal\") || {\"1\": [], \"2\": [], \"3\": [], \"4\": [], \"5\": [], \"6\": []}};\n return (msg)\n\n case \"volver_tipos\": // boton de salida al menu editar\n return ({ payload: { pantalla: \"editar\" } })\n\n case \"ayuda\": // boton de salida al menu editar\n return ({ payload: { pantalla: \"ayuda\" } })\n\n case \"salir-ayuda\": // boton de salida al menu editar\n return ({ payload: { pantalla: \"editar\" } })\n\n default: // chequeamos si tenemos acciones a realizar para lo cual revisamos el \"origen\" (actualizar en memoria sin preguntar usuario) y/o el \"modalCallback\" con esto sabemos de que tabla es el mensaje enviado por el MODAL\n node.send([null, null, null, null, null, null, msg])\n if (msg.origen == \"actualizar\") {// salvar en memoria la configuracion que ha sido modificada (el Topic nos dice cual configuracion)\n let myconfig = msg.payload.table // table contiene el resultado de leer la tabla con todos los valores normalizados en uibuilder\n \n switch (msg.topic) { // guardamos cambios en el tipo de configuracion correspondiente\n case \"general\":\n flow.set(\"config_general\", myconfig)\n break;\n case \"conexion\":\n flow.set(\"config_conex\", myconfig)\n break;\n case \"tiempos\":\n flow.set(\"config_tiempos\", myconfig)\n break;\n }\n return { payload: { pantalla: \"editar\" } }\n }\n\n \n msg.hello = \"hello 3\"\n node.send([null, null, null, null, null, null, msg])\n return null\n\n }\n} // final del SWITCH\nelse if (msg.modalCallback == \"modificar\") {\n /*\n let config_totem = []\n config_totem = flow.get(\"config_general\").concat(flow.get(\"config_camara\"), flow.get(\"config_conex\"), flow.get(\"config_sensor\"), flow.get(\"config_mensajes\"), flow.get(\"config_tiempos\"), flow.get(\"tiposDatosGlobal\"))\n flow.set(\"config_totem\", config_totem)\n let jsontest = toJson(flow.get(\"config_totem\"));\n */\n let config_totem = {\n general: flow.get(\"config_general\") || [],\n camara: flow.get(\"config_camara\") || [],\n conexion: flow.get(\"config_conex\") || [],\n sensores: flow.get(\"config_sensor\") || [],\n mensajes: flow.get(\"config_mensajes\") || [],\n tiempos: flow.get(\"config_tiempos\") || [],\n tipos: flow.get(\"tiposDatosGlobal\") || {}\n };\n\n flow.set(\"config_totem\", config_totem);\n\n msg.payload = JSON.stringify(config_totem, null, 2) + \"\\n\\r\";\n //msg.payload = JSON.stringify(jsontest, null, 2) + \"\\n\\r\"; // el factor 2 agrega 2 espacio al inicio de cada elemento\n //y agrega tambien una linea lo cual es interesante para la legibilidad del formato y para que los clientes SFTP puedan\n //leer bien el archivo, como es el caso de Mobaxterm.\n\n return ([{ payload: { pantalla: \"menu-principal\" } }, msg, null])\n}\nelse if (msg.modalCallback == \"recargar\") {\n return ([null, null, null, { payload: 1 }])\n}\n\nelse if (msg.modalCallback == \"reiniciar\") {\n return ([null, null, null, null, { payload: \"sudo shutdown -r now\" }])\n}\n\nelse if (msg.modalCallback == \"apagar\") {\n return ([null, null, null, null, { payload: \"sudo poweroff\" }])\n}\n\n//============================================== FUNCIONES ==============================================================\n//esta funcion convierte un array con elementos [parametro, valor] en un objeto Json respetando los tipos de parametro (numerico, booleano, texto)\nfunction toJson(data) {\n let newJson = {}\n /*\n data.forEach(item => {\n let param = item.parametro;\n let value = item.valor;\n */\n for (let i = 0; i < data.length; i++) {\n let param = data[i].parametro;\n let value = data[i].valor;\n\n // Parseo de tipos igual que antes\n if (typeof value === 'string') {\n if (value.toLowerCase() === \"true\") {\n newJson[param] = true;\n } else if (value.toLowerCase() === \"false\") {\n newJson[param] = false;\n } else if (!isNaN(value) && value.trim() !== \"\") {\n newJson[param] = Number(value);\n } else {\n newJson[param] = value;\n }\n } else {\n // Por si el value ya viene como booleano o número real\n newJson[param] = value;\n }\n }\n return (newJson)\n}\n\n//=======================================================================================================================================\n\nfunction validarIP(ip) {\n const regex = /^(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)$/;\n return regex.test(ip);\n}", + "outputs": 8, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3270, + "y": 480, + "wires": [ + [ + "1e52a0895d377969", + "2f80166bb47a752f" + ], + [ + "c398ee4ceb30937f", + "136e09e5233e209b" + ], + [ + "adbe70916e20904f" + ], + [ + "6a6d080c74f2da43" + ], + [ + "081628af33270569" + ], + [ + "4847a19f86d704fc" + ], + [ + "711b20eac053af7f" + ], + [ + "7aa2adada5ca928e", + "77eb0df85abf7f16" + ] + ] + }, + { + "id": "ad67876e3fd4b4bb", + "type": "debug", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "debug 33", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3250, + "y": 380, + "wires": [] + }, + { + "id": "adbe70916e20904f", + "type": "link out", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "set semaforo", + "mode": "link", + "links": [], + "x": 3485, + "y": 500, + "wires": [] + }, + { + "id": "1e52a0895d377969", + "type": "link out", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "feedback", + "mode": "link", + "links": [ + "764c5454d21fc52b" + ], + "x": 3435, + "y": 420, + "wires": [] + }, + { + "id": "2f80166bb47a752f", + "type": "debug", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "debug 34", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3470, + "y": 340, + "wires": [] + }, + { + "id": "6a6d080c74f2da43", + "type": "link out", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "aplicar_config", + "mode": "link", + "links": [ + "53019ab29a07f8f5" + ], + "x": 3485, + "y": 540, + "wires": [] + }, + { + "id": "081628af33270569", + "type": "link out", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "reiniciar", + "mode": "link", + "links": [], + "x": 3485, + "y": 580, + "wires": [] + }, + { + "id": "49e2b8c24d0ee245", + "type": "function", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "datos de\\n pesada y \\n snapshot", + "func": "if(msg.payload.hasOwnProperty(\"tiposDatos\")){\n return(msg)\n\n}\nelse {\n let msgmix = {}\n if (flow.get(\"snapshot_cam\") && msg.payload.hasOwnProperty(\"Picture\")) { // si recibimos snapshot extraemos informacion necesaria para mostrar en la pagina de snapshot\n msgmix.plate = msg.payload.Picture.Plate.PlateNumber // guardamos el numero de matricula\n msgmix.picture = \"data:image/png;base64,\" + msg.payload.Picture.NormalPic.Content\n msgmix.plate_time = msg.payload.Picture.SnapInfo.SnapTime\n msgmix.camera_id = msg.payload.Picture.SnapInfo.DeviceID\n }\n\n\n else if (flow.get(\"escuchar_cam\")) { // Si estamos esperando snapshots de la camara o acciones del usuario en botones left o right\n let snapshot_array = flow.get('snapshot_array') || [];\n let snapshot_readindex = flow.get('snapshot_readindex') || 0;\n let snapshot_saveindex = flow.get('snapshot_saveindex') || 0;\n let snapshot_indexmax = flow.get('snapshot_indexmax') || 0;\n\n\n if (flow.get(\"getfromarray\") == \"left\") {\n msgmix.buttons = true // indica al index.js (del uibuilder) que no tiene que inicializar la pantalla de escuchar camara\n if (snapshot_readindex > 0) {\n snapshot_readindex = snapshot_readindex - 1\n flow.set(\"snapshot_readindex\", snapshot_readindex)\n msgmix = snapshot_array[snapshot_readindex]\n msgmix.snapshot_readindex = snapshot_readindex\n msgmix.snapshot_indexmax = snapshot_indexmax\n }\n }\n else if (flow.get(\"getfromarray\") == \"right\") {\n msgmix.buttons = true // indica al index.js (del uibuilder) que no tiene que inicializar la pantalla de escuchar camara\n if (snapshot_readindex < flow.get(\"snapshot_indexmax\") - 1) {\n snapshot_readindex = snapshot_readindex + 1\n flow.set(\"snapshot_readindex\", snapshot_readindex)\n msgmix = snapshot_array[snapshot_readindex]\n msgmix.snapshot_readindex = snapshot_readindex\n msgmix.snapshot_indexmax = snapshot_indexmax\n\n }\n }\n else {\n if (msg.payload.hasOwnProperty(\"Picture\")) { // si recibimos snapshot extraemos informacion necesaria para mostrar en la pagina de escuchar camara\n msgmix.plate = msg.payload.Picture.Plate.PlateNumber // guardamos el numero de matricula\n msgmix.picture = \"data:image/png;base64,\" + msg.payload.Picture.NormalPic.Content\n msgmix.plate_time = msg.payload.Picture.SnapInfo.SnapTime\n msgmix.camera_id = msg.payload.Picture.SnapInfo.DeviceID\n msgmix.buttons = true\n\n\n snapshot_array[snapshot_saveindex] = RED.util.cloneMessage(msgmix); // Clonamos para evitar referencias\n flow.set(\"snapshot_array\", snapshot_array)\n flow.set(\"snapshot_saveindex\", snapshot_saveindex + 1)\n flow.set(\"snapshot_indexmax\", snapshot_indexmax + 1)\n\n if (snapshot_readindex == snapshot_saveindex - 1) {\n flow.set(\"snapshot_readindex\", snapshot_readindex + 1) // en caso que el index de lectura este al final, se incrementa al llegar una nueva imagen\n }\n msgmix.snapshot_indexmax = flow.get(\"snapshot_indexmax\")\n msgmix.snapshot_readindex = flow.get(\"snapshot_readindex\")\n }\n }\n }\n\n else {\n\n let msgmix = {}\n\n }\n flow.set(\"getfromarray\", \"\")\n\n //*****************************************Agrega datos de la pesada para PAGINA PRINCIPAL************************************************************ */\n if (!msg.payload.hasOwnProperty(\"Picture\")) {\n msgmix = Object.assign({}, msg, msgmix); // msg sobrescribe valores si hay conflictos\n }\n msgmix.peso = flow.get(\"peso\")\n msgmix.semaforo_color = flow.get(\"semaforo\")\n msgmix.matricula = flow.get(\"matricula\")\n msgmix.peso_estable_flag = flow.get(\"peso_estable_flag\")\n msgmix.vial = flow.get(\"numero_vial\")\n msgmix.totem_id = flow.get(\"totem_id\")\n msgmix.iplan = flow.get(\"myip\")\n msgmix.ipsupervisor = flow.get(\"websocket_url\")\n msgmix.snapshot_cam = flow.get(\"snapshot_cam\")\n msgmix.escuchar_cam = flow.get(\"escuchar_cam\")\n return msgmix;\n}", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2830, + "y": 500, + "wires": [ + [ + "c42bdd023acc8dc3", + "a4992bd24a1acc12" + ] + ] + }, + { + "id": "a4992bd24a1acc12", + "type": "debug", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "debug 35", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3050, + "y": 400, + "wires": [] + }, + { + "id": "4847a19f86d704fc", + "type": "link out", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "snapshot_rqst", + "mode": "link", + "links": [], + "x": 3485, + "y": 620, + "wires": [] + }, + { + "id": "711b20eac053af7f", + "type": "debug", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "debug 36", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3530, + "y": 660, + "wires": [] + }, + { + "id": "37b8b001f2c455d2", + "type": "comment", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "CONFIGURACION DE PARAMETROS Y PRUEBAS ", + "info": "Este grupo de nodos permiten servir una pagina web para que el usuario desde un movil pueda realizar la gestion de configuracion\nde los principales parametros del Totem asi como realizar algunas pruebas en la camara y en el semaforo.\nEl punto central de este grupo es el paquete UIBUILDER para nodered el cual convive sin inconvenientes con el DASHBOARD de nodered.\nEl uibuilder posee un nodo para cada path de acceso que deseemos habilitar. Cada nodo es independiente del otro en el sentido que \ncada uno dispone de un index.html, un index.js y un index.css ue permiten configurar la pagina que sirve el nodo. \nRecordemos que estos archivos se ejecutan en el cliente, por lo tanto para que el index.js tenga acceso a los datos de nodered y nodered \na los datos generados por el cliente, el nodo uibuilder establece una comunicacion websocket transparente para el usuario de manera\na poder intercambiar informacion entre nodered y el browser. Los elementos fundamentales en index.js para llevar a cabo esta comunicacion\nson las funciones \"uibuilder.onChange()\" para recibir los mensajes de nodered y \"uibuilder.send()\" para enviar a nodered los datos generados \npor el cliente.\nEl nodo \"buttons decoder\" fundamentalmente se dedica a interpretar los comandos del cliente (botones que ha pulsado el usuario) de manera a recopilar\nla informacion adecuada para enviar al uibuilder nuevamente de manera que cambie de pagina o muestre ciertos datos, o tambien a ejecutar\nlos comandos solicitados como reboot, apagar dispositivo, encender / apagar semaforo, ...\nEl nodo de entrada \"datos de pesada y snapshot\" recibe por un lado los snapshots enviados por la camara y extrae la informacion necesaria \npara enviar al cliente, y por otro lado siempre que haya alguna recepcion de datos del Totem se preparan los datos correspondientes para \ncolocarlos en la pantalla principal del uibuilder o pantalla de monitoreo.\nCuando mostramos un array de numeros, la tabla dinamica lo muestra o bien como numero o bien como texto de manera que luego cuando\nlo leemos cometemos un error ya que en el archivo nos va a quedar un texto o unos numeros sueltos y no un array. Para solucionar eso \nhemos hecho una funcion formatForCell(v) que asegura que se muestre en pantalla el arry ejemplo [3,1,2].\nPara la lectura de la tabla dinamica hemos puesto en la funcion SendRow (osea antes de enviar el contenido de la tabla que se recoge de la pantalla)\nun codigo para asegurar que se escriben correctamente tanto los booleano, enteros, flotantes y arrays.", + "x": 2960, + "y": 640, + "wires": [] + }, + { + "id": "7aa2adada5ca928e", + "type": "link out", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "activa-barrera", + "mode": "link", + "links": [], + "x": 3485, + "y": 700, + "wires": [] + }, + { + "id": "77eb0df85abf7f16", + "type": "debug", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "debug 37", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3530, + "y": 740, + "wires": [] + }, + { + "id": "c398ee4ceb30937f", + "type": "debug", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "debug 38", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3590, + "y": 540, + "wires": [] + }, + { + "id": "136e09e5233e209b", + "type": "file", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "guardar config", + "filename": "/home/trypton/.node-red/projects/config_semaforos.txt", + "filenameType": "str", + "appendNewline": true, + "createDir": true, + "overwriteFile": "true", + "encoding": "none", + "x": 3570, + "y": 460, + "wires": [ + [] + ] + }, + { + "id": "d6c429592c6cfdab", + "type": "link in", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "link in 1", + "links": [], + "x": 2725, + "y": 540, + "wires": [ + [ + "49e2b8c24d0ee245" + ] + ] + }, + { + "id": "764c5454d21fc52b", + "type": "link in", + "z": "576f3807f61f5098", + "g": "350484a2e764ef67", + "name": "entrada uibuilder-1", + "links": [ + "1e52a0895d377969" + ], + "x": 2725, + "y": 460, + "wires": [ + [ + "49e2b8c24d0ee245" + ] + ] + }, + { + "id": "a2fcbe68dc4e70bd", + "type": "inject", + "z": "576f3807f61f5098", + "g": "6f7ca09a49c974d3", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "2", + "topic": "", + "payload": "1", + "payloadType": "num", + "x": 2790, + "y": 880, + "wires": [ + [ + "6b89fc341280ad29" + ] + ] + }, + { + "id": "6b89fc341280ad29", + "type": "file in", + "z": "576f3807f61f5098", + "g": "6f7ca09a49c974d3", + "name": "read-file", + "filename": "/home/trypton/.node-red/projects/config_semaforos.txt", + "filenameType": "str", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 2960, + "y": 880, + "wires": [ + [ + "e02d2079825430db" + ] + ] + }, + { + "id": "e02d2079825430db", + "type": "function", + "z": "576f3807f61f5098", + "g": "6f7ca09a49c974d3", + "name": "config edit", + "func": "const config = JSON.parse(msg.payload);\nmsg ={}; // clear msg object\nlet read;\nlet config_general = []\nlet config_tiempos = []\n//let config_mensajes = []\n//let config_sensor = []\n//let config_camara = []\nlet config_conex = []\n//let config_tipos = []\n\nconfig.general = toObject(config.general);\n//config.camara = toObject(config.camara);\nconfig.conexion = toObject(config.conexion);\n//config.sensores = toObject(config.sensores);\n//config.mensajes = toObject(config.mensajes);\nconfig.tiempos = toObject(config.tiempos);\n\n\n\n/*\n// Aseguramos que cada tipo esté como array vacío si no tiene nada\nfor (let i = 1; i <= 6; i++) {\n if (!Array.isArray(tiposOriginales[i])) {\n tiposOriginales[i] = [];\n }\n}\nflow.set(\"tiposDatosGlobal\", tiposOriginales);\n\nconfig.tipos = tiposOriginales;\n*/\n//=================================================== GENERAL =====================================================\nread = config.general.peso_min_on; // peso minimo para definir estado ocupado\nconfig_general.push({ \"parametro\": \"peso_min_on\", \"valor\": read })\nflow.set(\"P_on\", read);\n\nread = config.general.peso_min_off; // peso minimo para definir estado desocupado\nconfig_general.push({ \"parametro\": \"peso_min_off\", \"valor\": read })\nflow.set(\"P_off\", read);\n\nread = config.general.prioridades; \nconfig_general.push({ \"parametro\": \"prioridades\", \"valor\": read })\nflow.set(\"order\", read)\n\nread = config.general.vehiculos_acceso1; \nconfig_general.push({ \"parametro\": \"vehiculos_acceso1\", \"valor\": read })\nflow.set(\"N1\", read)\n\nread = config.general.vehiculos_acceso2; \nconfig_general.push({ \"parametro\": \"vehiculos_acceso2\", \"valor\": read })\nflow.set(\"N2\", read)\n\nread = config.general.vehiculos_acceso3; \nconfig_general.push({ \"parametro\": \"vehiculos_acceso3\", \"valor\": read })\nflow.set(\"N3\", read)\n\nread = config.general.modo; \nconfig_general.push({ \"parametro\": \"modo\", \"valor\": read })\nflow.set(\"mode\", read)\n\n\n//===================================================CONEXION =====================================================\n\nread = config.conexion.DO_semaforo1;\nconfig_conex.push({ \"parametro\": \"DO_semaforo1\", \"valor\": read })\nflow.set(\"DO_semaforo1\", read); \n\nread = config.conexion.DO_semaforo2;\nconfig_conex.push({ \"parametro\": \"DO_semaforo2\", \"valor\": read })\nflow.set(\"DO_semaforo2\", read); \n\nread = config.conexion.DO_semaforo3;\nconfig_conex.push({ \"parametro\": \"DO_semaforo3\", \"valor\": read })\nflow.set(\"DO_semaforo3\", read); \n\nread = config.conexion.DI_Lazo1;\nconfig_conex.push({ \"parametro\": \"DI_Lazo1\", \"valor\": read })\nflow.set(\"DI_Lazo1\", read); \n\nread = config.conexion.DI_Lazo2;\nconfig_conex.push({ \"parametro\": \"DI_Lazo2\", \"valor\": read })\nflow.set(\"DI_Lazo2\", read);\n\nread = config.conexion.DI_Lazo3;\nconfig_conex.push({ \"parametro\": \"DI_Lazo3\", \"valor\": read })\nflow.set(\"DI_Lazo3\", read);\n\nread = config.conexion.nombre_lazo1; \nconfig_conex.push({ \"parametro\": \"nombre_lazo1\", \"valor\": read })\nflow.set(\"nombre_lazo1\", read)\n\nread = config.conexion.nombre_lazo2; \nconfig_conex.push({ \"parametro\": \"nombre_lazo2\", \"valor\": read })\nflow.set(\"nombre_lazo2\", read)\n\nread = config.conexion.nombre_lazo3; \nconfig_conex.push({ \"parametro\": \"nombre_lazo3\", \"valor\": read })\nflow.set(\"nombre_lazo3\", read)\n\nread = config.conexion.adam_id;\nconfig_conex.push({ \"parametro\": \"adam_id\", \"valor\": read })\nflow.set(\"adam_id\", read);\n\nread = config.conexion.base_address;\nconfig_conex.push({ \"parametro\": \"base_address\", \"valor\": read })\nflow.set(\"base_address\", read);\n\nread = config.conexion.numero_vial;\nconfig_conex.push({ \"parametro\": \"numero_vial\", \"valor\": read })\nflow.set(\"numero_vial\", read);\n\n//=================================================== TIEMPOS =====================================================\n\nread = config.tiempos.transito_acceso1\nconfig_tiempos.push({ \"parametro\": \"transito_acceso1\", \"valor\": read })\nflow.set(\"t1_ms\", read);\n\nread = config.tiempos.transito_acceso2\nconfig_tiempos.push({ \"parametro\": \"transito_acceso2\", \"valor\": read })\nflow.set(\"t2_ms\", read);\n\nread = config.tiempos.transito_acceso3\nconfig_tiempos.push({ \"parametro\": \"transito_acceso3\", \"valor\": read })\nflow.set(\"t3_ms\", read);\n\nread = config.tiempos.t_confirm_bascula\nconfig_tiempos.push({ \"parametro\": \"t_confirm_bascula\", \"valor\": read })\nflow.set(\"T_confirm_ms\", read);\n\nread = config.tiempos.t_semaforo1\nconfig_tiempos.push({ \"parametro\": \"t_semaforo1\", \"valor\": read })\nflow.set(\"g1_ms\", read);\n\nread = config.tiempos.t_semaforo2\nconfig_tiempos.push({ \"parametro\": \"t_semaforo2\", \"valor\": read })\nflow.set(\"g2_ms\", read);\n\nread = config.tiempos.t_semaforo3\nconfig_tiempos.push({ \"parametro\": \"t_semaforo3\", \"valor\": read })\nflow.set(\"g3_ms\", read);\n\n\n// Inicializa algunas variables en el sistema interno\n\nflow.set('state','IDLE');\nflow.set('B_OCC',0);\nflow.set('L1',0); flow.set('L2',0); flow.set('L3',0);\n\n// Accesos bloqueados por timeout de verde (Gi)\n// Si blocked[1] = true, el acceso 1 NO puede recibir verde hasta que L1 haga OFF.\nflow.set('blocked', { 1:false, 2:false, 3:false });\n\n\n\nflow.set(\"config_general\", config_general)\nflow.set(\"config_tiempos\", config_tiempos)\nflow.set(\"config_conex\", config_conex)\n//flow.set(\"config_camara\", config_camara)\n//flow.set(\"config_sensor\", config_sensor)\n\nreturn [{topic:'CMD/SET_SEMS', payload:{S1:'RED',S2:'RED',S3:'RED',ts:Date.now()}}, {payload: flow.get(\"mode\")}];\n\n\n//*********************************************************** FUNCIONES **************************************************/\nfunction toObject(array) {\n \n const obj = {};\n array.forEach(el => {\n obj[el.parametro] = el.valor;\n\n });\n return obj;\n}\n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3150, + "y": 880, + "wires": [ + [ + "7c6babaed4914e41" + ], + [ + "4232fc1ec89537af" + ] + ] + }, + { + "id": "53019ab29a07f8f5", + "type": "link in", + "z": "576f3807f61f5098", + "g": "6f7ca09a49c974d3", + "name": "init semaforos", + "links": [ + "6a6d080c74f2da43" + ], + "x": 2805, + "y": 940, + "wires": [ + [ + "6b89fc341280ad29" + ] + ] + }, + { + "id": "7c6babaed4914e41", + "type": "link out", + "z": "576f3807f61f5098", + "g": "6f7ca09a49c974d3", + "name": "salida config", + "mode": "link", + "links": [ + "8da927c794b40546" + ], + "x": 3285, + "y": 880, + "wires": [] + }, + { + "id": "130015e21da95201", + "type": "function", + "z": "576f3807f61f5098", + "name": "Extrae Lazos", + "func": "// Espera: msg.payload = [DI0, DI1, DI2, ... DI7]\n// Salidas: [Lazo1] [Lazo2] [Lazo3]\n\n// 1) Validaciones básicas\nif (!Array.isArray(msg.payload)) {\n node.error(\"msg.payload no es un array\", msg);\n return [null, null, null];\n}\n\nconst di = msg.payload;\n\n// 2) Leer la configuración de canal desde contexto\n// Cambia global.get por flow.get si las tienes en flow\nlet ch1 = flow.get(\"DI_Lazo1\");\nlet ch2 = flow.get(\"DI_Lazo2\");\nlet ch3 = flow.get(\"DI_Lazo3\");\n\n// Si vienen como string, convertir a número\nch1 = (ch1 !== undefined && ch1 !== null) ? Number(ch1) : ch1;\nch2 = (ch2 !== undefined && ch2 !== null) ? Number(ch2) : ch2;\nch3 = (ch3 !== undefined && ch3 !== null) ? Number(ch3) : ch3;\n\n// 3) Función helper: extraer valor de canal de forma segura\nfunction readChannel(ch) {\n if (!Number.isInteger(ch) || ch < 0 || ch >= di.length) return undefined;\n const v = di[ch]; // true/false (o a veces 0/1)\n return (v === undefined) ? undefined : (v ? 1 : 0);\n}\n\n// 4) Leer valores según mapeo\nconst v1 = readChannel(ch1);\nconst v2 = readChannel(ch2);\nconst v3 = readChannel(ch3);\n\n// 5) Construir mensajes para cada salida\n// Si no hay valor (canal mal configurado), devolvemos null para no disparar ese RBE\nconst out1 = (v1 === undefined) ? null : { payload: v1, channel: ch1, lazo: 1, _src: \"adam_di\", topic:\"lazo1\" };\nconst out2 = (v2 === undefined) ? null : { payload: v2, channel: ch2, lazo: 2, _src: \"adam_di\", topic:\"lazo2\"};\nconst out3 = (v3 === undefined) ? null : { payload: v3, channel: ch3, lazo: 3, _src: \"adam_di\", topic:\"lazo3\" };\n\n// (Opcional) copiar propiedades útiles del msg original (por si necesitas timestamp, etc.)\nfunction inherit(base, m) {\n if (!m) return null;\n // Copia todo menos payload si quieres mantener estructura\n // Aquí lo dejo simple: hereda algunas claves comunes si existen\n //if (base.topic !== undefined) m.topic = base.topic;\n if (base.timestamp !== undefined) m.timestamp = base.timestamp;\n return m;\n}\n\nreturn [\n inherit(msg, out1),\n inherit(msg, out2),\n inherit(msg, out3)\n];\n", + "outputs": 3, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 590, + "y": 460, + "wires": [ + [ + "e4a843aa2e13ce4d", + "3845edd7f91846ec" + ], + [ + "69f5087384c48122", + "176fe4510822e556" + ], + [ + "fb6d5180204e6bce", + "94f2a26231e45789" + ] + ] + }, + { + "id": "b6317c2d3d6d070c", + "type": "modbus-response", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "", + "registerShowMax": 20, + "x": 2490, + "y": 1040, + "wires": [] + }, + { + "id": "217697ee4e884321", + "type": "link in", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "modo", + "links": [ + "4232fc1ec89537af" + ], + "x": 1715, + "y": 880, + "wires": [ + [ + "06c0e45f61e0d364" + ] + ] + }, + { + "id": "4232fc1ec89537af", + "type": "link out", + "z": "576f3807f61f5098", + "g": "6f7ca09a49c974d3", + "name": "link out 5", + "mode": "link", + "links": [ + "217697ee4e884321" + ], + "x": 3285, + "y": 940, + "wires": [] + }, + { + "id": "29b8ed91354ac1e2", + "type": "link in", + "z": "576f3807f61f5098", + "g": "f4f4ca47869551e3", + "name": "peso_in", + "links": [ + "3e6f6384f2ec9846" + ], + "x": 935, + "y": 820, + "wires": [ + [ + "a849cb3f53ceb581", + "e616701e86d299ad" + ] + ] + }, + { + "id": "b0c185b2e9fb99d4", + "type": "switch", + "z": "576f3807f61f5098", + "g": "f4f4ca47869551e3", + "name": "Vial", + "property": "payload.numero_vial", + "propertyType": "msg", + "rules": [ + { + "t": "eq", + "v": "numero_vial", + "vt": "flow" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 1150, + "y": 820, + "wires": [ + [ + "29994ee556e17a85", + "0af79df0c83f9717" + ] + ] + }, + { + "id": "176fe4510822e556", + "type": "debug", + "z": "576f3807f61f5098", + "name": "debug 45", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 780, + "y": 300, + "wires": [] + }, + { + "id": "94f2a26231e45789", + "type": "debug", + "z": "576f3807f61f5098", + "name": "debug 46", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 780, + "y": 340, + "wires": [] + }, + { + "id": "0af79df0c83f9717", + "type": "debug", + "z": "576f3807f61f5098", + "name": "debug 47", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1240, + "y": 920, + "wires": [] + }, + { + "id": "a849cb3f53ceb581", + "type": "debug", + "z": "576f3807f61f5098", + "name": "debug 48", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1040, + "y": 920, + "wires": [] + }, + { + "id": "e616701e86d299ad", + "type": "json", + "z": "576f3807f61f5098", + "g": "f4f4ca47869551e3", + "name": "", + "property": "payload", + "action": "", + "pretty": false, + "x": 1030, + "y": 820, + "wires": [ + [ + "b0c185b2e9fb99d4" + ] + ] + }, + { + "id": "c79d32e49f4cf252", + "type": "debug", + "z": "576f3807f61f5098", + "name": "debug 49", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1360, + "y": 240, + "wires": [] + }, + { + "id": "110e757690de4198", + "type": "comment", + "z": "576f3807f61f5098", + "name": "Gestion de Semaforos V1.0", + "info": "Esta version 1.0 contiene las conexiones modbus con ADAM 6066 para el control de los lazos de induccion\ny de los semaforos. \nContiene una pagina de configuracion de parametros elementales de operacion del sistema de gestion \nde semaforos el cual esta en http://ip:1880/semaforos. El archivo de configuracion \"semaforos.txt\" esta \nen \"/home/trypton/node-red/projects\" al igual que los demas archivos de configuracion de los otros flujos.\nEsta version 1.0 aunque tiene todas las funcionalidades requeridas aun se encuentra en pruebas.\n", + "x": 1120, + "y": 260, + "wires": [] + } +] \ No newline at end of file diff --git a/flows_cred.json b/flows_cred.json index 9e26dfe..aa7f7c1 100644 --- a/flows_cred.json +++ b/flows_cred.json @@ -1 +1,6 @@ -{} \ No newline at end of file +{ + "1c8613764b7f685e": { + "user": "trypton", + "password": "Trypton#2022" + } +} \ No newline at end of file diff --git a/uibuilder/.config/sioMiddleware.js-template b/uibuilder/.config/sioMiddleware.js-template new file mode 100644 index 0000000..678690b --- /dev/null +++ b/uibuilder/.config/sioMiddleware.js-template @@ -0,0 +1,53 @@ +/** + * Template Socket.IO Connection Middleware for uibuilder. + * UPDATED: 2022-04-01 + * + * NOTES & WARNINGS: + * 1) This function is only called ONCE - when a new client connects. So any authentication/security processing is limited + * because you cannot use this to, for example, timeout/extend a session without further server processing of incoming messages. + * However, see the sioUse.js and sioMsgOut.js middlewares for per-msg handling. + * 2) Failing to either return or call `next()` will mean that your clients will never connect. + * 3) An error in this function will probably cause Node-RED to fail to start at all. + * 4) You have to restart Node-RED if you change this file. + * 5) To use for authentication/authorisation with Express and sio connection middleware, create a common node.js module. + * + * Allows custom processing for authentication, session management, connection validation, logging, rate limiting, etc. + * + * see also: uibRoot/.config/sioUse.js & sioMsgOut.js + * https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#websocket-implementation-hints + * + * @param {*} socket Socket.IO socket object + * @param {function} next The callback to hand off to the next middleware + */ +function sioMw(socket, next) { + // Some SIO related info that might be useful in security checks + console.log('== [sioMiddleware.js] ====================') + console.log('New client connected to Namespace') + console.log('--socket.request.connection.remoteAddress--', socket.request.connection.remoteAddress) + console.log('--socket id--', socket.id) + // Added by uibuilder when Namespace is created by uibuilder node instance - Note also that socket.nsp.log can be used to output to the Node-RED log + console.log('--socket namespace metadata (server)--', { + name: socket.nsp.name, + url: socket.nsp.url, + nodeId: socket.nsp.nodeId, + useSecurity: socket.nsp.useSecurity, + }) + //console.log('--socket handshake--', socket.handshake) + //console.log('--socket properties--', Object.keys(socket)) + // Show the client id (set by uibuilder ExpressJS middleware) + console.log('--client id handshake.auth--', socket.handshake.auth.clientId) + console.log('--client id in custom header (polling only)--', socket.handshake.headers['x-clientid']) + console.log('==========================================\n ') + + // Simplistic auth example + let auth = true + if (auth !== true) { + socket.nsp.log.error(`[uibuilder:sioMiddleware.js] - Authentication error, client disconnected - ID: ${socket.id}`) + return next (new Error(`[uibuilder:sioMiddleware.js] - Authentication error, client disconnected - ID: ${socket.id}` )) + } + + return next() + +} // Remember to end with a `next()` statement or nothing will work. + +module.exports = sioMw \ No newline at end of file diff --git a/uibuilder/.config/sioMsgOut.js-template b/uibuilder/.config/sioMsgOut.js-template new file mode 100644 index 0000000..0331fce --- /dev/null +++ b/uibuilder/.config/sioMsgOut.js-template @@ -0,0 +1,22 @@ +/** + * Template Socket.IO outbound per-msg middleware for uibuilder. Fn will be called for EVERY outbound msg from Node-RED/uibuilder to a client. + * UPDATED: 2022-04-01 + * + * NOTES & WARNINGS: + * 1) This function is called whenever any instance of uibuilder sends a msg to any client. + * 2) You have to restart Node-RED if you change this file. + * 3) You can use this to make changes to the msg before it is sent. + * + * Allows you to process outgoing data to clients. Use it to add security/user data or anything else. + * + * @param {object} msg The msg being seny by uibuilder to a client + * @param {string} url The uibuilder instance url + * @param {string} channel The socket.io channel being used + */ + function sioMsgOutMw( msg, url, channel ) { + + console.log('[uibuilder:Socket.IO:sioMsgOut.js] msg from server: ', msg, url, channel) + +} + +module.exports = sioMsgOutMw \ No newline at end of file diff --git a/uibuilder/.config/sioUse.js-template b/uibuilder/.config/sioUse.js-template new file mode 100644 index 0000000..dff1ab4 --- /dev/null +++ b/uibuilder/.config/sioUse.js-template @@ -0,0 +1,43 @@ +/** + * Template Socket.IO `use` middleware for uibuilder. Fn will be called for EVERY inbound msg from a client to Node-RED/uibuilder. + * UPDATED: 2022-04-01 + * + * NOTES & WARNINGS: + * 1) This function is called when a client sends a "packet" of data to the server. + * 2) Failing to either return or call `next()` will mean that your clients will never be able to get responses. + * 3) You can amend the incoming msg in this middleware. + * 4) An error in this function will probably cause Node-RED to fail to start at all. + * 5) You have to restart Node-RED if you change this file. + * 6) If you call `next( new Error('blah') )` The error is sent back to the client and further proessing of the incoming msg stops. + * 7) To use for authentication/authorisation with Express and sio connection middleware, create a common node.js module. + * + * Allows you to process incoming data from clients. + * + * see: https://socket.io/docs/v4/server-api/#socketusefn + * see also: uibRoot/.config/sioMiddleware.js & sioMsgOut.js + * and https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#websocket-implementation-hints + * + * @param {[string,Array]} data The channel name (strictly the event name) and args send by a client (Socket.IO calls it a "packet"). data[0] is the channel/event name, data[args][0] is the actual msg + * @param {function} next The callback to hand off to the next middleware + */ +function sioUseMw([ channel, ...args ], next) { + + const msg = args[0] + + console.log('[uibuilder:Socket.IO:sioUse.js] msg from client: ', 'Channel Name:', channel, ' Msg:', msg) + + // Simplistic error example - looking for specific property on the inbound msg + if ( msg.i_am_an_error ) { + // The error is sent back to the client and further processing of the msg stops + next(new Error('Oops! Some kind of error happened')) + return + } + + // You can amend the incoming msg + msg._test = 'added by sioUse.js middleware' + + next() + +} // Do not forget to end with a call to `next()` or clients will not be able to connect + +module.exports = sioUseMw \ No newline at end of file diff --git a/uibuilder/.config/uibMiddleware.js-template b/uibuilder/.config/uibMiddleware.js-template new file mode 100644 index 0000000..56f9632 --- /dev/null +++ b/uibuilder/.config/uibMiddleware.js-template @@ -0,0 +1,26 @@ +/** + * Template ExpressJS Middleware for uibuilder. + * UPDATED: 2022-04-01 + * + * NOTES & WARNINGS: + * 1) This function is called EVERY TIME any web call is made to the URL defined by your uib instance. + * So it should be kept short and efficient. + * 2) Failing to either return or call `next()` will cause an ExpressJS error. + * 3) An error in this function will probably cause Node-RED to fail to start at all. + * 4) You have to restart Node-RED if you change this file. + * 5) To use for authentication/authorisation with sio connection middleware, create a common node.js module. + * + * Allows custom processing for authentication, session management, custom logging, etc. + * + * @param {object} req The ExpressJS request object + * @param {object} res The ExpressJS result object + * @param {function} next The callback to hand off to the next middleware + */ + function uibMw(req,res,next) { + + console.log('[uibuilder:uibMiddleware.js] Custom ExpressJS middleware called.') + next() + +} // Do not forget to end with a call to `next()` + +module.exports = uibMw \ No newline at end of file diff --git a/uibuilder/bascula/LICENSE b/uibuilder/bascula/LICENSE new file mode 100644 index 0000000..a16c46a --- /dev/null +++ b/uibuilder/bascula/LICENSE @@ -0,0 +1,178 @@ + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/uibuilder/bascula/README.md b/uibuilder/bascula/README.md new file mode 100644 index 0000000..b5257b4 --- /dev/null +++ b/uibuilder/bascula/README.md @@ -0,0 +1,77 @@ +# uibuilder Template: Blank (Default) + +> NOTE: You can replace the contents of this README with text that describes your UI. + +This is about the simplest template you can get for uibuilder. Is is also (as of uibuilder v5+), the default template. + +It does not use any frameworks and has no other dependencies. It demonstrates that you can use uibuilder purely with HTML/JavaScript or even just HTML and still easily build a simple, dynamic, data-driven user interface with the help of Node-RED. + +All it does is load the uibuilder client library and connect to Node-RED. + +## UI + +Initially only shows an H1 heading with a sub-heading. However, it contains a `
` with the id "`more`" which is used by many of the examples in the Node-RED import library. + +In addition, the `more` div uses uibuilder's `uib-topic` special attribute which allows it to be used as a target for messages sent from Node-RED. This is a useful feature that allows you to easily update the content of the page without having to write any JavaScript code. Send a message containing `{ topic: 'more', payload: 'Hello World' }` to the `uibuilder` node and the content of the `more` div will be updated with "Hello World". Note that the payload can contain HTML. As an example, use an inject node with `msg.payload` set to use a JSONata expression like `"Hello! This is a message from Node-RED at " & $moment()`. Don't forget to set `msg.topic` to `more` so that the uibuilder client library knows where to send the message. + +> **WARNING**: Using the "more" topic completely overwrites the contents of the `more` div. + +## Folders + +* `/` - The root folder contains this file. It can be used for other things **but** it will not be served up in the Node-RED web server. +* `/src/` - the default folder that serves files as web resources. However, this can be changed to a different folder if desired. +* `/dist/` - the default folder for serving files as web resources where a build step is used. In that case, the `/src` folder is the source used by the build tool and `/dist` is the destination for the build (the "distribution" folder). +* `/routes/` - This folder can contain `.js` files defining routing middleware for uibuilder's ExpressJS web server. +* `/api/` - This folder can contain `.js` files defining REST API's specific to this uibuilder instance. +* `/types/` - Contains typescript definition files (`*.d.ts`) for the uibuilder client library. This is not used by uibuilder but can be used by your IDE to provide type checking and auto-completion for the uibuilder client library. This is useful if you are using TypeScript or JavaScript with type checking enabled. Remember to update these for new uibuilder versions. + +The above folders will all pre-exist for the built-in uibuilder templates. The folders can safely be removed if not needed but one folder must exist to serve the web resources from (this cannot be the root folder). + +The template only has files in the root and `src` folders. The `src` folder is the default used by uibuilder to serve up files to clients. + +One reserved item in the root folder however will be a `package.json` file. This will be used in the future to help with build/compile steps. You can still use it yourself, just bear in mind that a future version of uibuilder will make use it as well. If you need to have any development packages installed to build your UI, don't forget to tell `npm` to save them as development dependencies not normal dependencies. + +The `dist` folder should be used if you have a build step to convert your source code to something that browsers understand. So if you are using a build (compile) step to produce your production code, ensure that it is configured to use the `dist` folder as the output folder and that it creates at least an `index.html` file. + +You can switch between the `src` and `dist` (or other) folders using the matching setting in the Editor. See uibuilder's advanced settings tab. + +Also note that you can use **linked** folders and files in this folder structure. This can be handy if you want to maintain your code in a different folder somewhere or if your default build process needs to use sub-folders other than `src` and `dist`.(Though as of v6, you can specify any sub-folder to be served) + +## Files in this template + +* `package.json`: REQUIRED. Defines the basic structure, name, description of the project and defines any local development dependencies if any. Also works with `npm` allowing the installation of dev packages (such as build or linting tools). +* `README.md`: This file. Change this to describe your web app and provide documentation for it. +* `eslint.config.js`: A pre-configured configuration for the ESLINT tool. Helps when writing front-end code. Note that you need at least eslint v8+ installed for this to work. +* `LICENSE`: A copy of the Apache 2.0 license. Replace with a different license if needed. Always license your code. Apache 2.0 matches the licensing of uibuilder. +* `src/index.html`: REQUIRED. Contains your basic HTML and will be the file loaded and displayed in the browser when going to the uibuilder defined URL. +* `src/index.js`: Contains all of the logic for your UI. It must be linked to in the html file. Optional. +* `src/index.css`: Contains your custom CSS for styling. It must be linked to in the html file. Optional. +* `tsconfig.json`: A configuration file for TypeScript. This can be used by your IDE to provide descriptions, type checking and auto-completion for the uibuilder client library. This is useful if you are using TypeScript or JavaScript with type checking enabled. Uses the typescript definition files in the `/types` folder, remember to update these for new uibuilder versions. + +Note that only the `package.json` and `index.html` files are actually _required_. uibuilder will not function as expected without them. + +It is possible to use the index.html file simply as a link to other files but it must be present. + +The other files are all optional. However, you will need to change the index.html file accordingly if you rename or remove them. + +## Multiple HTML pages + +uibuilder will happily serve up any number of web pages from a single instance. It will also make use of sub-folders. However, each folder should have an `index.html` file so that a URL that ends with the folder name will still work without error. + +Note that each html file is a separate page and requires its own JavaScript and uibuilder library reference. When moving between pages, remember that every page is stand-alone, a new environment. You can share one `index.js` file between multiple pages if you prefer but each page will run a separate instance. + +If multiple pages are connected to the same uibuilder instance, they will all get the same broadcast messages from Node-RED. So if you want to handle different messages on different pages, remember to filter them in your front-end JavaScript in `uibuilder.onChange('msg', ....)` function. Turn on the advanced flag for including a `msg._uib` property in output if you need to differentiate between pages and/or clients in Node-RED. + +## URL endpoints + +When specifying links in your HTML, CSS and JavaScript files, you should use relative URLs. e.g. `./index.mjs` will load that file from the `src` folder or wherever else you have told uibuilder to use. + +When using uibuilder's server-side resources, you will generally use `../uibuilder/....`, for example `../uibuilder/uib-brand.min.css` as seen in the default `index.css` file. When accessing a front-end library being served by uibuilder, you can use the form `../uibuilder/vendor/....`. Use the "Full details" button in the uibuilder node to see all of the possible endpoints you may want to use. + + +## License + +This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. + +This template may be used however you like. It is provided as a test template for uibuilder and is not intended to be a full template. You are free to use it as a starting point for your own template or to use it as-is if you find it useful. + diff --git a/uibuilder/bascula/api/.keep b/uibuilder/bascula/api/.keep new file mode 100644 index 0000000..e69de29 diff --git a/uibuilder/bascula/dist/.keep b/uibuilder/bascula/dist/.keep new file mode 100644 index 0000000..e69de29 diff --git a/uibuilder/bascula/eslint.config.mjs b/uibuilder/bascula/eslint.config.mjs new file mode 100644 index 0000000..e1008e0 --- /dev/null +++ b/uibuilder/bascula/eslint.config.mjs @@ -0,0 +1,121 @@ +import { defineConfig } from 'eslint/config' +import js from '@eslint/js' +import globals from 'globals' +import jsdoc from 'eslint-plugin-jsdoc' +import stylistic from '@stylistic/eslint-plugin' +import html from 'eslint-plugin-html' + +// Shared rules +const jsdocRules = { + 'jsdoc/check-alignment': 'off', + // "jsdoc/check-indentation": ["warn", {"excludeTags":['example', 'description']}], + 'jsdoc/check-indentation': 'off', + 'jsdoc/check-param-names': 'warn', + 'jsdoc/check-tag-names': ['warn', { + definedTags: ['typicalname', 'element', 'memberOf', 'slot', 'csspart'], + }], + 'jsdoc/multiline-blocks': ['error', { + noZeroLineText: false, + }], + 'jsdoc/no-multi-asterisk': 'off', + 'jsdoc/no-undefined-types': ['error', { + definedTypes: ['JQuery', 'NodeListOf', 'ProxyHandler'], + }], + 'jsdoc/tag-lines': 'off', +} +const stylisticRules = { + '@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true, }], + '@stylistic/comma-dangle': ['error', { + arrays: 'only-multiline', + objects: 'always', + imports: 'never', + exports: 'always-multiline', + functions: 'never', + importAttributes: 'never', + dynamicImports: 'never', + }], + '@stylistic/eol-last': ['error', 'always'], + '@stylistic/indent': ['error', 4, { + SwitchCase: 1, + }], + '@stylistic/indent-binary-ops': ['error', 4], + '@stylistic/linebreak-style': ['error', 'unix'], + '@stylistic/lines-between-class-members': 'off', + '@stylistic/newline-per-chained-call': ['error', { + ignoreChainWithDepth: 2, + }], + '@stylistic/no-confusing-arrow': 'error', + '@stylistic/no-extra-semi': 'error', + '@stylistic/no-mixed-spaces-and-tabs': 'error', + '@stylistic/no-trailing-spaces': 'error', + '@stylistic/semi': ['error', 'never'], + '@stylistic/space-before-function-paren': 'off', + '@stylistic/spaced-comment': ['error', 'always', { + line: { + exceptions: ['*', '#region', '#endregion'], + }, + block: { + exceptions: ['*'], + }, + }], + '@stylistic/space-in-parens': 'off', + '@stylistic/quotes': ['error', 'single', { + avoidEscape: true, + allowTemplateLiterals: 'always', + }], +} +const generalRules = { + 'new-cap': 'error', + 'no-else-return': 'error', + 'no-empty': ['error', { + allowEmptyCatch: true, + }], + 'no-unused-vars': 'off', + 'no-useless-escape': 'off', + 'no-var': 'warn', + 'prefer-const': 'error', +} + +export default defineConfig([ + // Apply to all JavaScript files + { + files: ['**/*.js', '**/*.html'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'script', // Use script rather than ES modules + globals: { + ...globals.browser, + // ...globals.node, + UibRouter: 'readonly', + uibuilder: 'readonly', + $: 'readonly', + $$: 'readonly', + }, + }, + plugins: { + 'js': js, + 'jsdoc': jsdoc, + '@stylistic': stylistic, + 'html': html, + }, + extends: [ + js.configs.recommended, + jsdoc.configs['flat/recommended'], + stylistic.configs.recommended, + ], + rules: { + ...jsdocRules, + ...stylisticRules, + ...generalRules, + // 'no-empty': ['error', { 'allowEmptyCatch': true }], + }, + }, + + // Specific rules for configuration files + { + files: ['eslint.config.mjs', '**/*.config.js', '**/*.config.mjs'], + languageOptions: { + sourceType: 'module', // Config files can use ES modules + }, + }, +]) diff --git a/uibuilder/bascula/package.json b/uibuilder/bascula/package.json new file mode 100644 index 0000000..5a52510 --- /dev/null +++ b/uibuilder/bascula/package.json @@ -0,0 +1,35 @@ +{ + "name": "uib-blank", + "version": "2025-05-27", + "private": true, + "description": "This is about the simplest template you can get for uibuilder.", + "browser": "./src/index.js", + "scripts": { + "build": "echo \"No build process specified\"" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.27.0", + "@stylistic/eslint-plugin": "^4.2.0", + "eslint": "^9.27.0", + "eslint-plugin-html": "^8.1.3", + "eslint-plugin-jsdoc": "^50.6.17", + "globals": "^16.1.0" + }, + "keywords": ["uibuilder", "node-red", "node-red-contrib-uibuilder"], + "author": "Julian Knight (Totally Information)", + "license": "Apache-2.0", + "homepage": "https://github.com/TotallyInformation/node-red-contrib-uibuilder", + "bugs": "https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues", + "repository": { + "type": "git", + "url": "https://github.com/TotallyInformation/node-red-contrib-uibuilder.git" + }, + "browserslist": [ + "> 0.5%", + "maintained versions", + "last 2 versions", + "not dead", + "not ie > 0" + ] +} diff --git a/uibuilder/bascula/routes/.keep b/uibuilder/bascula/routes/.keep new file mode 100644 index 0000000..e69de29 diff --git a/uibuilder/bascula/src/index.css b/uibuilder/bascula/src/index.css new file mode 100644 index 0000000..dad4e43 --- /dev/null +++ b/uibuilder/bascula/src/index.css @@ -0,0 +1,140 @@ +body, html { + margin: 0; + padding: 0; + background: white; + font-family: sans-serif; + width: 100vw; + height: 100vh; + overflow: hidden; + /*cursor: none;*/ +} + +.pantalla-container { + width: 800px; + height: 460px; + margin: auto; + display: flex; + align-items: center; + justify-content: center; +} +.grid-zonas { + display: grid; + grid-template-areas: + "peso semaforo" + "mensajes datos"; + grid-template-columns: 70% 30%; + grid-template-rows: 50% 50%; + width: 100%; + height: 100%; + padding: 10px; + box-sizing: border-box; +} +.peso { + grid-area: peso; + display: flex; + align-items: center; + justify-content: center; + font-size: 80px; + font-weight: bold; + +} +.peso-valor { + font-size: 160px; + text-align: center; +} +.peso-unidad { + font-size: 28px; + margin-left: 10px; +} +.semaforo { + grid-area: semaforo; + display: flex; + align-items: center; + justify-content: center; +} +#semaforo { + width: 150px; + height: 150px; + border-radius: 50%; + background-color: gray; + box-shadow: 0 0 12px rgba(0,0,0,0.3); + +} +.mensajes { + grid-area: mensajes; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + gap: 6px; + padding-top: 10px; +} +.linea-mensaje { + font-size: 30px; + font-weight: bold; + padding: 4px 12px; + border-radius: 6px; + min-height: 28px; + width: 100%; + max-width: 500px; +} +.btn-ok { + font-size: 56px; + font-weight: bold; + padding: 15px 100px; + background-color: #28a745;/* valor por defecto */ + color: orange; + border: none; + border-radius: 10px; + display: none; +} +#mensajes-informativos { + margin-top: 10px; + display: none; + font-size: 20px; + color: #8B4513; +} +#mensajes-informativos p { + margin: 5px 0; +} +.datos { + grid-area: datos; + display: flex; + flex-direction: column; + justify-content: center; + gap: 15px; + padding-top: 10px; +} +.dato-bloque .label-mat { + font-weight: bold; + font-size: 28px; + color: #003366; + text-align: center; + +} +.dato-bloque .label-id { + font-weight: bold; + font-size: 28px; + color: #003366; + text-align: center; +} +.dato-bloque .valor { + font-size: 32px; + font-weight: bold; + color: #b30000; + text-align: center; + padding-top: 10px; + min-height: 40px; +} +/* +div { + border: 1px dashed red !important; +} +*/ + + + + + + diff --git a/uibuilder/bascula/src/index.html b/uibuilder/bascula/src/index.html new file mode 100644 index 0000000..fa31ad3 --- /dev/null +++ b/uibuilder/bascula/src/index.html @@ -0,0 +1,41 @@ + + + + + + Pantalla Totem + + + + + + + +
+
+
+ 0Kg +
+
+
+
+
+
+
+
+ +
+
+
+
MATRICULA
+
+
+
+
TARJETA ID
+
+
+
+
+
+ + \ No newline at end of file diff --git a/uibuilder/bascula/src/index.js b/uibuilder/bascula/src/index.js new file mode 100644 index 0000000..c68f634 --- /dev/null +++ b/uibuilder/bascula/src/index.js @@ -0,0 +1,70 @@ +uibuilder.start(); + +uibuilder.onChange('msg', msg => { + if (msg.hasOwnProperty("peso")) document.getElementById('peso').textContent = msg.peso; + if (msg.hasOwnProperty("id_m2")) document.getElementById('matricula').textContent = msg.id_m2 + if (msg.hasOwnProperty("id_m4")) document.getElementById('tarjetaid').textContent = msg.id_m4 + if (msg.hasOwnProperty("semaforo")) document.getElementById('semaforo').style.backgroundColor = msg.semaforo; + + + + const boton = document.getElementById('boton-ok'); + const m1 = document.getElementById('mensaje1'); + const m2 = document.getElementById('mensaje2'); + const m3 = document.getElementById('mensaje3'); + + const payload = msg.payload || {}; + + // Mensajes individuales + if (msg.hasOwnProperty("men_m1")) { + m1.textContent = msg.men_m1; + m1.style.color = msg.men_m1_color || 'black'; + m1.style.display = 'block'; + } else { + m1.style.display = 'none'; + } + + if (!(msg.button == "button3") && msg.hasOwnProperty("men_m2")) { + m2.textContent = msg.men_m2; + m2.style.color = msg.men_m2_color || 'black'; + m2.style.display = 'block'; + } else { + m2.style.display = 'none'; + } + + if (!(msg.button == "button3") && msg.hasOwnProperty("men_m3")) { + m3.textContent = msg.men_m3; + m3.style.color = msg.men_m2_color || 'black'; + m3.style.display = 'block'; + } else { + m3.style.display = 'none'; + } + if (msg.hasOwnProperty("men_m1")) { + const m1 = document.getElementById('mensaje1'); + m1.textContent = msg.men_m1; + m1.style.display = 'block'; + m1.style.color = msg.men_m1_color || 'black'; + } else { + document.getElementById('mensaje1').style.display = 'none'; + } + + // Botón + if (msg.button == "button3") { + //console.log("boton ok") + boton.style.display = 'inline-block'; + boton.textContent = msg.button3 || 'OK'; + boton.style.backgroundColor = msg.botonvalidacion_background || '#28a745'; + boton.style.color = msg.botonvalidacion_foreground || 'orange'; + } + else { + boton.style.display = 'none'; + } + +}); + +document.getElementById('boton-ok').addEventListener('click', () => { + document.getElementById('boton-ok').style.display = 'none'; + document.getElementById('mensaje1').style.display = 'none'; + + uibuilder.send({ payload: { boton: "ok" } }); +}); diff --git a/uibuilder/bascula/tsconfig.json b/uibuilder/bascula/tsconfig.json new file mode 100644 index 0000000..38f9ea4 --- /dev/null +++ b/uibuilder/bascula/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", + "baseUrl": "./src", + "typeRoots": ["./types"] + }, + "include": ["types", "src/**/*.js"] +} diff --git a/uibuilder/bascula/types/uibuilder.d.ts b/uibuilder/bascula/types/uibuilder.d.ts new file mode 100644 index 0000000..0d359af --- /dev/null +++ b/uibuilder/bascula/types/uibuilder.d.ts @@ -0,0 +1,12 @@ +/// + +/** + * Make the uibuilder instance globally available (from uibuilder.module.d.ts) + * @version 7.3.0 + */ +declare global { + // Use typeof import to reference the Uib class from the module + const uibuilder: import("./uibuilder.module").Uib; +} + +export {}; diff --git a/uibuilder/bascula/types/uibuilder.module.d.ts b/uibuilder/bascula/types/uibuilder.module.d.ts new file mode 100644 index 0000000..44eecc7 --- /dev/null +++ b/uibuilder/bascula/types/uibuilder.module.d.ts @@ -0,0 +1,647 @@ +/** + * Type definitions for uibuilder.module.js + * WCAG 2.2 AA, ESLint v9, Shift-Left security, and project conventions applied. + * @version 7.3.0 + * @author Julian Knight (Totally Information) + */ + +export type HtmlString = string + +/** Column metadata for tables */ +export interface ColumnDefinition { + index: number, + hasName: boolean, + title: string, + name?: string, + key?: string | number, + dataType?: 'string' | 'date' | 'number' | 'html', + editable?: boolean, +} + +/** Options for building HTML tables */ +export interface TableOptions { + cols?: ColumnDefinition[], + parent?: HTMLElement | string, + allowHTML?: boolean, +} + +/** Options for tblAddListener */ +export interface TableListenerOptions { + eventScope?: 'row' | 'cell', + returnType?: 'text' | 'html', + pad?: number, + send?: boolean, + logLevel?: string | number, + eventType?: string, +} + +/** Options for notification */ +export interface NotificationConfig { + title?: string, + body?: string, + return?: boolean, + [key: string]: any, +} + +/** + * Uibuilder main class + * @typicalname uibuilder + * @description The client-side Front-End JavaScript for uibuilder in HTML Module form. + * Provides a number of global objects that can be used in your own JavaScript. + * See the docs folder `./docs/uibuilder.module.md` for details of how to use this fully. + * @version 7.3.0 + * @author Julian Knight (Totally Information) + */ +export class Uib { + /** + * Static metadata for the Uibuilder client + */ + static _meta: { + version: string, + type: string, + displayName: string, + } + + /** Client ID set by uibuilder on connect */ + clientId: string + /** The collection of cookies provided by uibuilder */ + cookies: Record + /** Copy of last control msg object received from server */ + ctrlMsg: object + /** Is Socket.IO client connected to the server? */ + ioConnected: boolean + /** Is the library running from a minified version? */ + isMinified: boolean + /** Is the browser tab containing this page visible or not? */ + isVisible: boolean + /** Remember the last page (re)load/navigation type: navigate, reload, back_forward, prerender */ + lastNavType: string + /** Max msg size that can be sent over Socket.IO - updated by "client connect" msg receipt */ + maxHttpBufferSize: number + /** Last std msg received from Node-RED */ + msg: object + /** Number of messages sent to server since page load */ + msgsSent: number + /** Number of messages received from server since page load */ + msgsReceived: number + /** Number of control messages sent to server since page load */ + msgsSentCtrl: number + /** Number of control messages received from server since page load */ + msgsCtrlReceived: number + /** Is the client online or offline? */ + online: boolean + /** Last control msg object sent via uibuilder.send() */ + sentCtrlMsg: object + /** Last std msg object sent via uibuilder.send() */ + sentMsg: object + /** Placeholder to track time offset from server, see fn socket.on(ioChannels.server ...) */ + serverTimeOffset: number | null + /** Placeholder for a socket error message */ + socketError: string | null + /** Tab identifier from session storage */ + tabId: string + /** Actual name of current page (set in constructor) */ + pageName: string | null + /** Is the DOMPurify library loaded? Updated in start() */ + purify: boolean + /** Is the Markdown-IT library loaded? Updated in start() */ + markdown: boolean + /** Current URL hash. Initial set is done from start->watchHashChanges via a set to make it watched */ + urlHash: string + /** Default originator node id - empty string by default */ + originator: string + /** Optional default topic to be included in outgoing standard messages */ + topic?: string + /** Either undefined or a reference to a uib router instance. Set by uibrouter, do not set manually. */ + uibrouterinstance?: any + /** Set by uibrouter, do not set manually */ + uibrouter_CurrentRoute?: any + /** Internal: auto-send ready flag */ + autoSendReady: boolean + /** Node-RED setting (via cookie) */ + httpNodeRoot: string + /** Socket.IO namespace - unique to each uibuilder node instance */ + ioNamespace: string + /** Socket.IO path */ + ioPath: string + /** Starting delay factor for subsequent reconnect attempts */ + retryFactor: number + /** Starting retry ms period for manual socket reconnections workaround */ + retryMs: number + /** Prefix for all uib-related localStorage */ + storePrefix: string + /** Whether uibuilder client has started */ + started: boolean + /** Socket.IO connection options */ + socketOptions: object + + // --- Getters/Setters --- + logLevel: number + meta: typeof Uib._meta + + /** + * Set uibuilder properties to a new value - works on any property except _* or #* + * Also triggers any event listeners. + * @param prop Any uibuilder property who's name does not start with a _ or # + * @param val The set value of the property or a string declaring that a protected property cannot be changed + * @param store If true, the variable is also saved to the browser localStorage if possible + * @param autoload If true & store is true, on load, uib will try to restore the value from the store automatically + * @returns Input value + */ + set(prop: string, val: any, store?: boolean, autoload?: boolean): any + + /** + * Get the value of a uibuilder property + * @param prop The name of the property to get as long as it does not start with a _ or # + * @returns The current value of the property + */ + get(prop: string): any + + /** + * Write to localStorage if possible. Console error output if can't write + * Also uses this.storePrefix + * @param id localStorage var name to be used (prefixed with 'uib_') + * @param value value to write to localstore + * @param autoload If true, on load, uib will try to restore the value from the store + * @returns True if succeeded else false + */ + setStore(id: string, value: any, autoload?: boolean): boolean + + /** + * Attempt to get and re-hydrate a key value from localStorage + * @param id The key of the value to attempt to retrieve + * @returns The re-hydrated value of the key or null if key not found, undefined on error + */ + getStore(id: string): any + + /** + * Remove a given id from the uib keys in localStorage + * @param id The key to remove + */ + removeStore(id: string): void + + /** + * Returns a list of uibuilder properties (variables) that can be watched with onChange + * @returns List of uibuilder managed variables + */ + getManagedVarList(): Record + + /** + * Returns a list of currently watched variables + * @returns List of watched variable names + */ + getWatchedVars(): string[] + + /** + * Register on-change event listeners for uibuilder tracked properties + * @param prop The property of uibuilder that we want to monitor + * @param callback The function that will run when the property changes, parameter is the new value of the property after change + * @returns A reference to the callback to cancel + */ + onChange(prop: string, callback: (val: any) => void): number + + /** + * Cancel a previously registered onChange event listener + * @param prop The property name + * @param cbRef The callback reference number + */ + cancelChange(prop: string, cbRef: number): void + + /** + * Register a change callback for a specific msg.topic + * @param topic The msg.topic we want to listen for + * @param callback The function that will run when an appropriate msg is received + * @returns A reference to the callback to cancel + */ + onTopic(topic: string, callback: (msg: any) => void): number + + /** + * Cancel a previously registered onTopic event listener + * @param topic The topic name + * @param cbRef The callback reference number + */ + cancelTopic(topic: string, cbRef: number): void + + /** + * Returns a new array containing the intersection of the 2 input arrays + * @param a1 Array to check + * @param a2 Array to intersect + * @returns The intersection of the 2 arrays (may be an empty array) + */ + arrayIntersect(a1: T[], a2: T[]): T[] + + /** + * Copies a uibuilder variable to the browser clipboard + * @param varToCopy The name of the uibuilder variable to copy to the clipboard + */ + copyToClipboard(varToCopy: string): void + + /** + * Does the chosen CSS Selector currently exist? + * @param cssSelector Required. CSS Selector to examine for visibility + * @param msg Optional, default=true. If true also sends a message back to Node-RED + * @returns True if the element exists + */ + elementExists(cssSelector: string, msg?: boolean): boolean + + /** + * Format a number using the INTL standard library + * @param value Number to format + * @param decimalPlaces Number of decimal places to include + * @param intl standard locale spec, e.g. "ja-JP" or "en-GB" + * @param opts INTL library options object + * @returns formatted number + */ + formatNumber(value: number, decimalPlaces?: number, intl?: string, opts?: object): string + + /** + * Attempt to get rough size of an object + * @param obj Any serialisable object + * @returns Rough size of object in bytes or undefined + */ + getObjectSize(obj: any): number | undefined + + /** + * Returns true if a uibrouter instance is loaded, otherwise returns false + * @returns true if uibrouter instance loaded else false + */ + hasUibRouter(): boolean + + /** + * Only keep the URL Hash & ignoring query params + * @param url URL to extract the hash from + * @returns Just the route id + */ + keepHashFromUrl(url: string): string + + /** + * Custom logging function + * @param args Arguments to log + */ + log(...args: any[]): void + + /** + * Makes a null or non-object into an object. If thing is already an object. + * If not null, moves "thing" to {payload:thing} + * @param thing Thing to check + * @param property property that "thing" is moved to if not null and not an object. Default='payload' + * @returns Object + */ + makeMeAnObject(thing: any, property?: string): object + + /** + * Navigate to a new page or a new route (hash) + * @param url URL to navigate to. Can be absolute or relative (to current page) or just a hash for a route change + * @returns The new window.location string + */ + navigate(url: string): Location + + /** + * Convert a string attribute into a variable/constant reference + * Used to resolve data sources in attributes + * @param path The string path to resolve, must be relative to the `window` global scope + * @returns The resolved data source or null + */ + resolveDataSource(path: string): any + + /** + * Fast but accurate number rounding + * @param num The number to be rounded + * @param decimalPlaces Number of DP's to round to + * @returns Rounded number + */ + round(num: number, decimalPlaces: number): number + + /** + * Set the default originator. Set to '' to ignore. Used with uib-sender. + * @param originator A Node-RED node ID to return the message to + */ + setOriginator(originator?: string): void + + /** + * HTTP Ping/Keep-alive - makes a call back to uibuilder's ExpressJS server and receives a 204 response + * Can be used to keep sessions alive. + * @param ms Repeat interval in ms + */ + setPing(ms?: number): void + + /** + * Convert JSON to Syntax Highlighted HTML + * @param json A JSON/JavaScript Object + * @returns Object reformatted as highlighted HTML + */ + syntaxHighlight(json: object): HtmlString + + /** + * Returns true/false or a default value for truthy/falsy and other values + * @param val The value to test + * @param deflt Default value to use if the value is not truthy/falsy + * @returns The truth! Or the default + */ + truthy(val: any, deflt: any): boolean | any + + /** + * Joins all arguments as a URL string + * @param paths URL fragments + * @returns Joined URL string + */ + urlJoin(...paths: string[]): string + + /** + * Turn on/off/toggle sending URL hash changes back to Node-RED + * @param toggle Optional on/off/etc + * @returns True if we will send a msg to Node-RED on a hash change + */ + watchUrlHash(toggle?: any): boolean + + /** + * DEPRECATED FOR NOW - wasn't working properly. + * Is the chosen CSS Selector currently visible to the user? NB: Only finds the FIRST element of the selection. + * @returns False + */ + elementIsVisible(): false + + // --- UI handlers --- + /** + * Simplistic jQuery-like document CSS query selector, returns an HTML Element. + * If the selected element is a