diff --git a/flows.json b/flows.json index ae08685..d988d5d 100644 --- a/flows.json +++ b/flows.json @@ -373,18 +373,26 @@ "89f64c532d3d78e7", "e944fa11afc97160", "f4c6152e8ed7f5a6", - "a42d3e0da2d9dbda", "7b7da2e1e640ca0a", "1387a80aca617b59", "fdddf9cbc2fa6622", "9eda24ef14d3892c", "867c5654842a1210", - "d144432c1ab94daf" + "d144432c1ab94daf", + "573edc4212227ac9", + "7348a0042f799a64", + "3637f6cd8bf662ae", + "d7c0cf92f7146c98", + "bdfee81a7eb304fd", + "47112b3cace36ea6", + "498afa999a0e9dd7", + "68b16275b7089e9b", + "ef53c5d39ed1b135" ], - "x": 1994, + "x": 1984, "y": 1059, - "w": 2172, - "h": 488 + "w": 2362, + "h": 522 }, { "id": "6ec963a99751b141", @@ -404,7 +412,7 @@ "cb22a118da74ca79" ], "x": 1988, - "y": 1553, + "y": 1613, "w": 2044, "h": 614 }, @@ -430,39 +438,11 @@ "57fd039f5dadf4fb", "8558335b01c7e441" ], - "x": 1994, - "y": 2279, + "x": 1974, + "y": 2479, "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", @@ -486,7 +466,6 @@ "b4caaec341e453e0", "d19507c7bbe4bb32", "e2fca1c2499902fa", - "6a2b1526103aa3b0", "26f04e07495817eb", "d5ce4cb6b3abd807", "49bed5a59768fbef", @@ -499,7 +478,8 @@ "6455539b9fac2063", "7ff54b99b5b933b2", "08bf0c9897d6e815", - "65b220fc46d55c96" + "65b220fc46d55c96", + "a5ccb375313029ce" ], "x": 2004, "y": 459, @@ -555,25 +535,27 @@ "182712df6587926f", "de78dc5fb92df251", "6703fb916aef8f4b", - "b264dc44d4344d72", "4e1f0ff54eef4e7b", "2dc5c2942966b097", "3d7afdefd9a5d353", "138317044e960cfa", "4f30b9755ed381bb", - "66630f04db34f483", - "2d857220b76dda21", "8a7201a76e53897f", - "1b2d9af6c1e13e5f", "5c4679ed20761208", "c61c445897350722", "9fa72afe7d97828e", - "dc3fe6bd2494cab0" + "dc3fe6bd2494cab0", + "53f4bfb1ec29f1e1", + "45be747f248c1a59", + "4adc1ea54b29ee78", + "e0afc0a180416993", + "45b20bb1509256b3", + "dd447b7f9321fedc" ], - "x": 614, - "y": 1319, - "w": 1052, - "h": 442 + "x": 628, + "y": 1299, + "w": 1038, + "h": 582 }, { "id": "a57600b79a898710", @@ -598,12 +580,14 @@ "65bfea0283dfab05", "a1c2fb59c438c1c5", "2e4cf9271d9a78d8", - "dd4decb988977915" + "dd4decb988977915", + "f2d77a74edf9958e", + "beaff73dc2b3d86d" ], "x": 614, - "y": 1779, - "w": 1082, - "h": 242 + "y": 1939, + "w": 1102, + "h": 262 }, { "id": "6fd6ce44bb173af7", @@ -628,7 +612,7 @@ "e22e7f0f1cea9cb5" ], "x": 614, - "y": 2179, + "y": 2419, "w": 792, "h": 242 }, @@ -653,7 +637,7 @@ "1ca5e534fa4962de" ], "x": 614, - "y": 2479, + "y": 2739, "w": 432, "h": 222 }, @@ -690,8 +674,8 @@ "d7d43c051dffd484", "e5d5709dcfce538e" ], - "x": 4114, - "y": 1619, + "x": 4204, + "y": 1899, "w": 372, "h": 202 }, @@ -772,12 +756,13 @@ "0e30fb1a5ee58746", "67e977418231666d", "798cfaee0987cdca", - "b017ceea4285eb76" + "b017ceea4285eb76", + "833d2a4708575d0e" ], "x": 1634, - "y": 299, - "w": 602, - "h": 189.5 + "y": 259, + "w": 652, + "h": 229.5 }, { "id": "e51291e8f6dc7282", @@ -794,12 +779,14 @@ "nodes": [ "606dc6f9453f5e0d", "891cfa9c8d6e464d", - "15131de35b7d0425" + "15131de35b7d0425", + "7988495e94435bb4", + "4011f30e8a41b199" ], - "x": 1644, - "y": 539, - "w": 462, - "h": 82 + "x": 1634, + "y": 519, + "w": 492, + "h": 182 }, { "id": "fdde9120f66c6c06", @@ -819,7 +806,7 @@ "2dc673e9f13e0f0b" ], "x": 1644, - "y": 679, + "y": 719, "w": 442, "h": 82 }, @@ -841,17 +828,21 @@ "2f0ca87e8afc0d73", "06c0e45f61e0d364", "49a1dfc2d343bdbb", - "4878484f33b6ea38", "d8112ca72e0a342d", "b6317c2d3d6d070c", "dd43a77de63f2443", "217697ee4e884321", - "967aada409fe59f3" + "967aada409fe59f3", + "f2e46b18a6973969", + "eb17917cdc2ad86e", + "2daec3c01c1f7a1e", + "85025902ef5f0cde", + "5ecf6d948f96ea9a" ], "x": 1638, - "y": 839, - "w": 968, - "h": 448 + "y": 813, + "w": 1238, + "h": 428 }, { "id": "4a6a83419193ac56", @@ -908,8 +899,8 @@ "d6c429592c6cfdab", "764c5454d21fc52b" ], - "x": 2684, - "y": 299, + "x": 3084, + "y": 779, "w": 1012, "h": 482 }, @@ -933,11 +924,168 @@ "7c6babaed4914e41", "4232fc1ec89537af" ], - "x": 2694, - "y": 839, + "x": 3154, + "y": 539, "w": 632, "h": 142 }, + { + "id": "e6cc59abb0016d36", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "REVISION UBICACION INTERNA", + "style": { + "stroke": "#92d04f", + "fill": "#92d04f", + "fill-opacity": "0.14", + "label": true, + "color": "#000000" + }, + "nodes": [ + "22c0ec71d86183eb", + "d3197fdbc8ead3c3", + "2153eeb22a8551a0", + "c7d75c1b61b0d370" + ], + "x": 1984, + "y": 2319, + "w": 662, + "h": 82 + }, + { + "id": "110ca6b0bb9f248a", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "FUNCIONALIDADES EN ESTE FLOW\\n POR GRUPOS", + "style": { + "stroke": "#ff3f3f", + "fill-opacity": "0.16", + "label": true, + "label-position": "n", + "color": "#000000", + "fill": "#ff3f3f" + }, + "nodes": [ + "b3b8ba19a1803cfd", + "6f0cd359cf151c60", + "0ad183c1c32db0d0", + "212c50537ac465f0", + "051399d75c3148ce", + "f848d959035c5b4e", + "53b75027e49e6050", + "7281c7346d01c4e3", + "a44f7d1f3334d404", + "03b6b47b9544437e" + ], + "x": 234, + "y": 983, + "w": 312, + "h": 790 + }, + { + "id": "3a42dfd538259345", + "type": "group", + "z": "576f3807f61f5098", + "name": "CONTROL MANUAL DE SEMÁFOROS CON UIBUILDER", + "style": { + "label": true, + "stroke": "#0070c0", + "fill": "#7fb7df", + "fill-opacity": "0.32", + "color": "#000000" + }, + "nodes": [ + "3d15de7453ed0c33", + "1eb64e619b7e9a05", + "731bbc2ad85b5397", + "fab37c863ac68f06", + "d24eab5d07c0e51d", + "57655daf51fbf286" + ], + "x": 1894, + "y": 1299, + "w": 932, + "h": 142 + }, + { + "id": "fc56c475842bc648", + "type": "group", + "z": "744bdf3a0e08d754", + "name": "ACTUALIZACION DE TABLAS LOCALES", + "style": { + "label": true, + "color": "#000000", + "label-position": "n", + "stroke": "#ff0000", + "fill": "#c8e7a7", + "fill-opacity": "0.16" + }, + "nodes": [ + "ce6670be92cd4852", + "477599045e6aebb5", + "74ab401f1064a600", + "2f0a0951674cb0f7", + "057182f23d107ea9", + "f2da7ffbe437010d", + "de2470b64fc3a3ba", + "a7f0444a0f26b7e4", + "95a5ae68ae30005f", + "6a1e4d0c93dd137c", + "1226068e0f3be997", + "50f01b1f2ed222c9", + "c0351940eb077ea3", + "dabbaae991aa5540", + "1188e42874ce799f", + "70d8ef5cb93723c1", + "1259840b70ad958f", + "473c44f182c41108", + "301d1ad243d7df4c", + "9263d310331ec72f", + "e2e2d5e11894a5ca", + "ae0e0b8787f929be", + "b6dc38f78b9f146c", + "d29750f0c8e69c5f", + "5f68071edd29bae4", + "766671d6ac2d3fef", + "284196caeb4787bf", + "b3d236466b39e10b", + "343b7f167c6ee9a8", + "175be4afd40e4b72" + ], + "x": 1948, + "y": 2793, + "w": 1738, + "h": 368 + }, + { + "id": "9b68013ae7e6f87d", + "type": "group", + "z": "576f3807f61f5098", + "name": "AJUSTES LOCALES", + "style": { + "stroke": "#0070c0", + "stroke-opacity": "0.45", + "fill": "#7fb7df", + "fill-opacity": "0.29", + "label": true, + "color": "#000000" + }, + "nodes": [ + "fb90705e73d8ee4a", + "e91fc83a620b3648", + "f7711ab022abbe49", + "45a87cfad42d825f", + "77cf26031855da2d", + "ab9eeeb391287001", + "f28f2c7106d2590f", + "d7abaec22c1a4d68", + "94af1c83dc635ea7" + ], + "x": 874, + "y": 1019, + "w": 372, + "h": 322 + }, { "id": "fdddf9cbc2fa6622", "type": "group", @@ -961,8 +1109,8 @@ "0a4223f208ebf170", "fc3b9b3c65b9e540" ], - "x": 2994, - "y": 1219, + "x": 3294, + "y": 1239, "w": 552, "h": 302 }, @@ -993,11 +1141,14 @@ "e0c3a3c33ed7aaca", "7aee3c3ea10fd9a5", "aa870f8d73e4f504", - "1dae889e8697bf15" + "1dae889e8697bf15", + "8722ca0bd371a5f2", + "18fa8d0658cc9ba1", + "3804a5fcd6ce7f3e" ], "x": 2014, - "y": 1579, - "w": 1712, + "y": 1639, + "w": 1812, "h": 322 }, { @@ -1027,7 +1178,7 @@ "ceba75f09293b8f4" ], "x": 2014, - "y": 1939, + "y": 1999, "w": 972, "h": 202 }, @@ -1060,7 +1211,7 @@ "dfdc2a0fceee62b1" ], "x": 2994, - "y": 1939, + "y": 1999, "w": 1012, "h": 202 }, @@ -1114,10 +1265,60 @@ "18202cf1d5012bd6" ], "x": 1664, - "y": 979, + "y": 839, "w": 192, "h": 282 }, + { + "id": "45b20bb1509256b3", + "type": "group", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "TEST", + "style": { + "stroke": "#0070c0", + "fill": "#0070c0", + "fill-opacity": "0.1", + "label": true + }, + "nodes": [ + "b264dc44d4344d72", + "66630f04db34f483", + "2d857220b76dda21", + "1b2d9af6c1e13e5f", + "bbe015cc5fb9128c", + "52693c4aeb043be3" + ], + "x": 654, + "y": 1419, + "w": 232, + "h": 282 + }, + { + "id": "b6dc38f78b9f146c", + "type": "group", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "TEST", + "style": { + "stroke": "#0070c0", + "fill": "#7fb7df", + "fill-opacity": "0.2", + "label": true, + "label-position": "n", + "color": "#000000" + }, + "nodes": [ + "615724f93d501817", + "0f7ac5ca216ea759", + "c26fd52fb4cdd1ea", + "adc10412b8e600cf" + ], + "x": 1974, + "y": 2819, + "w": 292, + "h": 182 + }, { "id": "f928d3e1f28c1f5b", "type": "serial-port", @@ -1660,7 +1861,7 @@ "writeType": "str", "maxRetries": "", "retryDelay": "", - "indefiniteRetries": true, + "indefiniteRetries": false, "topic": "basculaTCP3", "name": "BASCULATCP3", "debug": "info", @@ -2039,7 +2240,7 @@ "writeType": "str", "maxRetries": "5", "retryDelay": "3000", - "indefiniteRetries": true, + "indefiniteRetries": false, "topic": "basculaTCP1", "name": "BASCULATCP1", "debug": "info", @@ -2455,7 +2656,7 @@ "writeType": "str", "maxRetries": "5", "retryDelay": "2000", - "indefiniteRetries": true, + "indefiniteRetries": false, "topic": "basculaTCP2", "name": "BASCULATCP2", "debug": "error", @@ -2851,9 +3052,9 @@ "newline": "", "write": "", "writeType": "str", - "maxRetries": "", - "retryDelay": "", - "indefiniteRetries": true, + "maxRetries": "5", + "retryDelay": "3000", + "indefiniteRetries": false, "topic": "basculaTCP4", "name": "BASCULATCP4", "debug": "error", @@ -4129,10 +4330,14 @@ "4526132d69e6c210", "7f282cc6ba963043", "ce3029f2688a0aba", - "ed04082db0d29b5b" + "ed04082db0d29b5b", + "c7d75c1b61b0d370", + "e0afc0a180416993", + "3804a5fcd6ce7f3e", + "284196caeb4787bf" ], - "x": 4155, - "y": 1720, + "x": 4245, + "y": 2000, "wires": [ [ "d7d43c051dffd484", @@ -4154,8 +4359,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 4350, - "y": 1660, + "x": 4440, + "y": 1940, "wires": [] }, { @@ -4168,8 +4373,8 @@ "links": [ "1b496d68c6bda310" ], - "x": 4305, - "y": 1780, + "x": 4395, + "y": 2060, "wires": [] }, { @@ -4185,8 +4390,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3020, - "y": 2440, + "x": 3000, + "y": 2640, "wires": [] }, { @@ -4210,8 +4415,8 @@ "topic": "SELECT * FROM movimientos WHERE matricula = '6421FPG' AND tipo = 'inicial'", "payload": "1", "payloadType": "num", - "x": 650, - "y": 2060, + "x": 550, + "y": 2160, "wires": [ [ "cc229c10e3f4e423" @@ -4239,8 +4444,8 @@ "topic": "", "payload": "0420MMR", "payloadType": "str", - "x": 3320, - "y": 2420, + "x": 3860, + "y": 2980, "wires": [ [ "9dd64e7e2d2a77c9" @@ -4260,8 +4465,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3940, - "y": 2340, + "x": 4480, + "y": 2900, "wires": [] }, { @@ -4277,8 +4482,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 4200, - "y": 2400, + "x": 4740, + "y": 2960, "wires": [] }, { @@ -4302,8 +4507,8 @@ "topic": "", "payload": "1334DEF", "payloadType": "str", - "x": 3320, - "y": 2500, + "x": 3860, + "y": 3060, "wires": [ [ "9dd64e7e2d2a77c9" @@ -4315,7 +4520,7 @@ "type": "debug", "z": "744bdf3a0e08d754", "name": "debug 35", - "active": true, + "active": false, "tosidebar": true, "console": false, "tostatus": false, @@ -4323,105 +4528,18 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 1800, - "y": 1820, + "x": 1700, + "y": 1980, "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, + "x": 4280, + "y": 2980, "wires": [ [ "8586621b0d788c6c", @@ -4441,8 +4559,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 3520, - "y": 2420, + "x": 4060, + "y": 2980, "wires": [ [ "381ba7abb9ab0be7" @@ -4461,8 +4579,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 3970, - "y": 2420, + "x": 4510, + "y": 2980, "wires": [ [ "6c42dc5e8e906084" @@ -4483,8 +4601,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3020, - "y": 2340, + "x": 3000, + "y": 2540, "wires": [] }, { @@ -4505,11 +4623,11 @@ "crontab": "", "once": false, "onceDelay": 0.1, - "topic": "Select * from Matriculas_Identificadores where matricula = \"6421FPG\";", + "topic": "Select * from MatriculasIdentificadores where matricula = \"6421FPG\";", "payload": "1", "payloadType": "num", - "x": 2150, - "y": 2480, + "x": 2130, + "y": 2680, "wires": [ [ "8558335b01c7e441" @@ -4534,11 +4652,11 @@ "crontab": "", "once": false, "onceDelay": 0.1, - "topic": "INSERT INTO Matriculas_Identificadores (matricula, id) VALUES (\"6423FPG\", \"546DA2A4-B07B-482D-B98F-B4EB0FFABCAE\") ;", + "topic": "INSERT INTO MatriculasIdentificadores (matricula, id) VALUES (\"6423FPG\", \"546DA2A4-B07B-482D-B98F-B4EB0FFABCAE\") ;", "payload": "1", "payloadType": "num", - "x": 2150, - "y": 2520, + "x": 2130, + "y": 2720, "wires": [ [ "8558335b01c7e441" @@ -4566,8 +4684,8 @@ "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, + "x": 2290, + "y": 2720, "wires": [ [ "8558335b01c7e441" @@ -4578,9 +4696,9 @@ "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, + "name": "Version 1.51", + "info": "*Version 1.51* 29/0/2026\n- En esta version hemos incluido la sincronizacion de movimientos cerrados no sincronizados en el mommento de producirse. Estos movimientos quedan registrados\nen la BD y marcados como no sincronizados, es decir no ha sido subidos al servidor central o mejor dicho este supervisor \nno ha recibido respuesta \"OK\" luego de haber enviado el movimiento. Dado que para componer la trama json del movimiento se necesita leer todas las imagenes asociadas al movimiento, las cuales pueden estar presentes o no (depende de la configuracion al momento de producir el movimiento) Hemos realizado un nuevo nodo\nque prepara todos los nombres de las imagenes que se deben leer y otro nodo que confirma que las imagenes necesarias para este movimiento han sido ya leidas y se encuentran almacenadas en memoria. En ese momento podemos armar la trama json con todos sus elementos y entonces enviarla nuevamente por http via un API. Una vez concluido el envio, procedemos al realizar la misma operacion con el proximo movimiento a sincronizar. Cada vez que enviamos una trama esperamos respuesta del servidor y en caso afirmativo se procede a marcar el movimiento como ya sincronizado.\n\n*Version 1.5* 25/0/2026\n- En esta version se agrega la gestion de la imagen de contexto tanto durante el movimiento inicial como durante el movimiento final\nPara ello se han introducido modificaciones en el nodo \"Comandos via Websockets\" a nivel del case de movimiento inicial. Luego tambien\nse han introducido cambios en el nodo \"movimiento cerrado\" para incluir el nombre y las imagenes de contexto en los envios al Servidor.\nAdicionalmente se han hecho cambios en las tablas de \"movimientos\" y \"movimientos cerrados\" para incluir los campos de los nombres de dichas imagenes\nEn el RPi se ha hecho una nueva carpeta (/home/trypton/.nodered/contexto) para incluir las imagenes de contexto que deben permanecer almacenadas.\n\n*Version 1.4*\n- En esta version hemos implementado la resincronizacion de movimientos cerrados que se encuentran dentro de la ta tabla local\n\"movimientos_cerrados\" pero que por alguna razon no se han podido sincronizar en el momento que se intento hacerlo.\nPara ello, cada vez que el sistema cierra una pesada y envia el movimiento, aprovecha para revisar la tabla de movimientos_cerrados\nde manera que si encuentra algun registro no sincronizado, lo trata de sincronizar en ese momento.\n\n*Version 1.3*\n- Esta version incluye una nueva forma de actualizacion de las tablas en BD local a peticion del Servidor. \nEl grupo que se encarga de esto se llama \"ACTUALIZACION DE TABLAS LOCALES\" y contiene todos los nodos que se encargan\nde realizar esta funcionalidad. Basicamente hay dos formas de actualizar:\n - Se actualizan las cuatro tablas en la misma operacion\n - Se actualiza una sola tabla (normalmente UbicacionesInternas)\nLa razon de esto es porque en general cualquier cambio en una tabla afecta a las demas tablas. El caso de UbicacionesInternas\nes un caso particular ya que un cambio en el campo de descripcion que realmente es lo que puede cambiar a menudo, no afecta\na ninguna otra tabla.\nEste nuevo metodo de actualizacion se basa en solicitar todas las tablas (4) insertarlas en la BD local como tablas temporales\ny una vez las tablas escritas sin errores reportados entonces se realiza un cambio de nombre de tablas de manera a colocar \nlas temporales como activas y las activas como temporales. De esta forma minimizamos la ventana de tiempo (ms) donde la BD pudiera\nquedar sin tablas sincronizadas al 100%\n\nVersion 1.2\nCuando el Supervisor recibe un comando de actualizar las tablas, una vez actualizadas las tablas en la BD local\nel Supervisor envia una trama de acknoledge al cliente solicitante de la actualizacion. Este aviso se envia wia websocket\na traves de la misma conexión donde se recibio el comando. El formato del envío es el siguiente:\n{\nsender: \"Supervisor\",\ndevice: “Urgesbas”, (el nombre del dispositivo que solicito el update)\nresult: true, (en caso de error este elemento iría en false)\nhora: hora de la actualización,\nIdCentroProveedor: GUID,\nrows: [array con nombre de tabla y registros insertados]\n}\n\nHemos agregado la opcion de recibir comandos via websocket para varias funcionalidades que a continuacion se mencionan:\n1.Actualizar las tablas de la base de datos (Matriculas, Identificadores, Identificadores_Matriculas, Ubicaciones_Internas)\nEl comando es: \n{sender: \"Urgesbas\", \ndevice: \"supervisor\", \naccion: \"actualizar\", \ntabla: \"Identificadores\", // Aqui va el nombre de la tabla que se desee actualizar\nIdCentroProveedor: \"gui\"\n}\n2. Gestionar manualmente los semaforos que maneja el supervisor\nEl comando para pasar a modo MANUAL / AUTO:\n{sender: \"Urgesbas\", \ndevice: \"supervisor\", \naccion: \"semaforos\", \ncomando: \"MANUAL\", // En caso de ponerlo en automatico enviamos \"AUTO\"\nIdCentroProveedor: \"gui\"\n}\nEl comando para encender un semaforo en verde (La bascula debe estar desocupada):\n{sender: \"Urgesbas\", \ndevice: \"supervisor\", \naccion: \"semaforos\", \ncomando: \"ON/OFF\",\ncontrol_semaforo: \"0, 1, 0\", (El orden de los semaforos es S1, S2, S3) el valor 0 = Rojo, El valor 1 = Verde)\nIdCentroProveedor: \"gui\"\n}\n3. Gestionar ciertas funcionalidades de los Totems (semaforo On / Off, Abrir Barrera, Hacer reboot del totem, Iniciar Configuracion)\nForzar el semaforo en Verde:\n{ sender: \"Urgesbas\",\n device: \"totem\",\n accion: \"semaforo_on\",\n numero_vial: 2,\n totem_id: \"totem_1\",\n IdCentroProveedor: \"gui\"\n}\nForzar el semaforo en Rojo:\n{ sender: \"Urgesbas\",\n device: \"totem\",\n accion: \"semaforo_off\",\n numero_vial: 2,\n totem_id: \"totem_1\",\n IdCentroProveedor: \"gui\"\n}\nAbrir la Barrera del Totem:\n{ sender: \"Urgesbas\",\n device: \"totem\",\n accion: \"abrirbarrera\",\n numero_vial: 2,\n totem_id: \"totem_1\",\n IdCentroProveedor: \"gui\"\n}\nHacer un reboot del Totem:\n{ sender: \"Urgesbas\",\n device: \"totem\",\n accion: \"reboot\",\n numero_vial: 2,\n totem_id: \"totem_1\",\n IdCentroProveedor: \"gui\"\n}\nVersion 1.1. Se agrega una nueva funcionalidad en el totem para avisar al conductor acerca de a que ubicacion interna debe dirigirse y por \nconsiguiente agregamos aqui en el Supervisor un grupo nuevo (\"REVISION UBICACION INTERNA\") con dos nodos que se encarga de buscar la ubicacion a partir del codigo del \nresiduo recibido del totem. Adicionalmente se agrgó unas lineas de codigo en el nodo \"Comandos via Websocket\" que mediante el comando \"check_ubicacion\" y el \ncodigo del residuo enviados por el totem, se activa la busqueda en BD\n\nVersion 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": 1130, "y": 880, "wires": [] }, @@ -4719,7 +4837,7 @@ "links": [ "a58ecf0f246aff6a" ], - "x": 2055, + "x": 2025, "y": 1160, "wires": [ [ @@ -4733,7 +4851,7 @@ "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", + "func": "setTimeout(function () {\n //msg.topic = \"SELECT * FROM movimientos_cerrados WHERE sincro = 0 ORDER BY ts_cierre ASC\";\n msg.topic = `SELECT\n uuid AS IdMovimiento,\n matricula,\n nombre_imagen_i,\n nombre_imagen_f,\n nombre_firma,\n nombre_contexto_i,\n nombre_contexto_f,\n horaentrada_i,\n horasalida_i,\n horaentrada_f,\n horasalida_f,\n pesoentrada AS pesoEntrada,\n pesosalida AS pesoSalida,\n rfid_i,\n rfid_f,\n qr_i,\n qr_f,\n idseleccionado AS IdCentroServicio,\n vial,\n totem,\n ts_cierre\nFROM movimientos_cerrados\nWHERE sincro = 0\nORDER 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, @@ -4770,7 +4888,7 @@ "y": 1160, "wires": [ [ - "b1c81347b429091a" + "573edc4212227ac9" ] ] }, @@ -4780,14 +4898,14 @@ "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}", + "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 && msg.payload.Resultado) {\n msg.topic = `UPDATE movimientos_cerrados SET sincro = 1 WHERE uuid = '${actual.IdMovimiento}'`;\n msg.payload = {Resultado: msg.payload.Resultado};\n return [msg, null];\n } else {\n node.warn(`Error al enviar UUID ${actual.IdMovimiento}, código HTTP: ${msg.statusCode}, Resultado:${msg.payload.Resultado}`);\n return [null, { payload: 1 }];\n }\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}\n*/", "outputs": 2, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 3490, + "x": 3630, "y": 1160, "wires": [ [ @@ -4812,12 +4930,12 @@ "initialize": "", "finalize": "", "libs": [], - "x": 3930, + "x": 4090, "y": 1160, "wires": [ [ "2b9158681d5eb09b", - "f4c6152e8ed7f5a6" + "ef53c5d39ed1b135" ] ] }, @@ -4835,7 +4953,7 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3680, + "x": 3820, "y": 1100, "wires": [] }, @@ -4871,7 +4989,7 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 4060, + "x": 4240, "y": 1100, "wires": [] }, @@ -4879,6 +4997,7 @@ "id": "d4a053997921cfc0", "type": "file in", "z": "744bdf3a0e08d754", + "d": true, "g": "68b92de7bc4c82f6", "name": "lee imagen_i", "filename": "filename", @@ -4900,9 +5019,10 @@ "id": "89f64c532d3d78e7", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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", + "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 (flow.get(\"files_2_rx\") == 2 && flow.get(\"imagen_i_rx_flag\") && flow.get(\"imagen_f_rx_flag\")){ // Hemos recibido las dos imagenes de matricula\n let reg2send = flow.get(\"pendientes\")[flow.get(\"indiceActual\")]\n reg2send.imagen_entrada = flow.get(\"imagen_i\")\n reg2send.IdCentroProveedor = flow.get(\"IdCentroProveedor\")\n if (flow.get(\"movimientosxapi\")){\n node.send([{ payload: reg2send, url: flow.get(\"url_API\") + `api/Totem/InsertarMovimientos?token=${flow.get(\"tokenapi\")}`, requestTimeout : 5000}, null, null])\n }\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 */\n}\nelse if (flow.get(\"files_2_rx\") == 3 && flow.get(\"imagen_i_rx_flag\") && 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 reg2send.IdCentroProveedor = flow.get(\"IdCentroProveedor\")\n if (flow.get(\"movimientosxapi\")) {\n node.send([{ payload: reg2send, url: flow.get(\"url_API\") + `api/Totem/InsertarMovimientos?token=${flow.get(\"tokenapi\")}`, requestTimeout: 10000 }, null, null])\n }\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 */\n}\nreturn\n\n\n\n\n\n\n\n", "outputs": 3, "timeout": 0, "noerr": 0, @@ -4913,7 +5033,6 @@ "y": 1160, "wires": [ [ - "a42d3e0da2d9dbda", "f41c4b5c61a58ea2" ], [ @@ -4929,6 +5048,7 @@ "id": "e944fa11afc97160", "type": "file in", "z": "744bdf3a0e08d754", + "d": true, "g": "68b92de7bc4c82f6", "name": "lee firma", "filename": "filename", @@ -4939,7 +5059,7 @@ "encoding": "none", "allProps": false, "x": 2880, - "y": 1240, + "y": 1220, "wires": [ [ "89f64c532d3d78e7" @@ -4950,10 +5070,11 @@ "id": "f4c6152e8ed7f5a6", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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, + "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 = {} // mensaje de firma\nlet msg_f = {} //mensaje de movimiento final\nlet msg_contexto_i = {} //mensaje de movimiento inicial\nlet msg_contexto_f = {} //mensaje de movimiento final\nflow.set(\"imagen_i_rx_flag\", false)\nflow.set(\"imagen_f_rx_flag\", false)\nflow.set(\"firma_rx_flag\", false)\nflow.set(\"contexto_i_rx_flag\", false)\nflow.set(\"contexto_f_rx_flag\", false)\n\nif (pendientes.length > 0) {\n let imagename_i = pendientes[indiceActual].nombre_imagen_i\n let imagename_f = pendientes[indiceActual].nombre_imagen_f\n let nombre_contexto_i = pendientes[indiceActual].nombre_contexto_i\n let nombre_contexto_f = pendientes[indiceActual].nombre_contexto_f\n \n // Procesamos imagen de movimiento inicial\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_i\"\n // Procesamos imagen de movimiento final\n if(imagename_f != \"Sin Matricula\"){\n msg_f.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_f.filename = flow.get(\"path_images\") + imagename_f + \".jpg\"\n }\n msg_f.tipo = \"imagen_f\"\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 flow.set(\"files_2_rx\", 3) // marca cantidad de imagenes a recibir en el proximo nodo\n node.send([msg, msg_f, msg2])\n }\n else{\n flow.set(\"files_2_rx\", 2) // marca cantidad de imagenes a recibir en el proximo nodo\n node.send([msg, msg_f, null])\n }\n \n if (nombre_contexto_i != \"\"){\n msg_contexto_i.filename = flow.get(\"path_contexto\") + nombre_contexto_i\n msg_contexto_i.tipo = nombre_contexto_i\n node.send([null, null, null, msg_contexto_i, null])\n \n }\n if (nombre_contexto_f != \"\"){\n msg_contexto_f.filename = flow.get(\"path_contexto\") + nombre_contexto_f\n msg_contexto_f.tipo = nombre_contexto_f\n node.send([null, null, null, null, msg_contexto_f])\n }\n \n \n return null\n}\nreturn null; \n\n\n\n", + "outputs": 5, "timeout": 0, "noerr": 0, "initialize": "", @@ -4970,27 +5091,15 @@ ], [ "e944fa11afc97160" + ], + [ + "7348a0042f799a64" + ], + [ + "3637f6cd8bf662ae" ] ] }, - { - "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", @@ -5027,15 +5136,16 @@ "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, + "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) y el Resultado es \"true\" se procede a marcar en la BD el bit de sincronizacion a 1\n para este registro 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 o error (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 (MQTT fue de momento abandopnado en el desarrollo)\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": 2190, + "y": 1540, "wires": [] }, { "id": "ca758e5b6e2d880e", "type": "mqtt in", "z": "744bdf3a0e08d754", + "d": true, "g": "fdddf9cbc2fa6622", "name": "ack", "topic": "totemautonomo/movimientos/movimiento_ok", @@ -5046,8 +5156,8 @@ "rap": true, "rh": 0, "inputs": 0, - "x": 3130, - "y": 1440, + "x": 3430, + "y": 1460, "wires": [ [ "a62dedb280979227", @@ -5059,6 +5169,7 @@ "id": "4a2f600ba9ca7a15", "type": "mqtt out", "z": "744bdf3a0e08d754", + "d": true, "g": "fdddf9cbc2fa6622", "name": "send data", "topic": "totemautonomo/movimientos/movimiento", @@ -5070,14 +5181,15 @@ "correl": "", "expiry": "", "broker": "3966bb218232f6b3", - "x": 3280, - "y": 1260, + "x": 3580, + "y": 1280, "wires": [] }, { "id": "a62dedb280979227", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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 ;", @@ -5087,8 +5199,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 3280, - "y": 1420, + "x": 3580, + "y": 1440, "wires": [ [ "b1c81347b429091a", @@ -5100,6 +5212,7 @@ "id": "d5ced8b11f124c67", "type": "debug", "z": "744bdf3a0e08d754", + "d": true, "g": "fdddf9cbc2fa6622", "name": "tx mqtt", "active": true, @@ -5110,14 +5223,15 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3270, - "y": 1320, + "x": 3570, + "y": 1340, "wires": [] }, { "id": "6d9ec47623128a2d", "type": "debug", "z": "744bdf3a0e08d754", + "d": true, "g": "fdddf9cbc2fa6622", "name": "rx ack", "active": true, @@ -5128,14 +5242,15 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3270, - "y": 1480, + "x": 3570, + "y": 1500, "wires": [] }, { "id": "1d3c5c1f2e5be4b0", "type": "trigger", "z": "744bdf3a0e08d754", + "d": true, "g": "fdddf9cbc2fa6622", "name": "timeout", "op1": "", @@ -5150,8 +5265,8 @@ "bytopic": "all", "topic": "topic", "outputs": 1, - "x": 3280, - "y": 1360, + "x": 3580, + "y": 1380, "wires": [ [ "0a4223f208ebf170", @@ -5163,6 +5278,7 @@ "id": "0a4223f208ebf170", "type": "change", "z": "744bdf3a0e08d754", + "d": true, "g": "fdddf9cbc2fa6622", "name": "init uuid", "rules": [ @@ -5179,8 +5295,8 @@ "from": "", "to": "", "reg": false, - "x": 3460, - "y": 1360, + "x": 3760, + "y": 1380, "wires": [ [] ] @@ -5189,17 +5305,19 @@ "id": "fc3b9b3c65b9e540", "type": "comment", "z": "744bdf3a0e08d754", + "d": true, "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, + "x": 3380, + "y": 1400, "wires": [] }, { "id": "9eda24ef14d3892c", "type": "file in", "z": "744bdf3a0e08d754", + "d": true, "g": "68b92de7bc4c82f6", "name": "lee imagen_f", "filename": "filename", @@ -5224,7 +5342,7 @@ "g": "68b92de7bc4c82f6", "mydb": "1c8613764b7f685e", "name": "Actualiza tabla", - "x": 3720, + "x": 3880, "y": 1140, "wires": [ [ @@ -5239,12 +5357,12 @@ "g": "68b92de7bc4c82f6", "mydb": "1c8613764b7f685e", "name": "Actualiza tabla", - "x": 2420, + "x": 2400, "y": 1160, "wires": [ [ - "f4c6152e8ed7f5a6", - "3c3867c2a2185f63" + "3c3867c2a2185f63", + "d7c0cf92f7146c98" ] ] }, @@ -5252,6 +5370,7 @@ "id": "d683e9cccedc561f", "type": "mqtt in", "z": "744bdf3a0e08d754", + "d": true, "g": "f0815d4d5c1e13e0", "name": "Contratos Update", "topic": "totemautonomo/contratos/update", @@ -5263,7 +5382,7 @@ "rh": 0, "inputs": 0, "x": 2120, - "y": 1700, + "y": 1760, "wires": [ [ "36b9bf6834913595" @@ -5274,6 +5393,7 @@ "id": "0c144d19f803250f", "type": "switch", "z": "744bdf3a0e08d754", + "d": true, "g": "f0815d4d5c1e13e0", "name": "Tabla destino", "property": "payload.tabla", @@ -5291,7 +5411,7 @@ }, { "t": "eq", - "v": "Matriculas_Identificadores", + "v": "MatriculasIdentificadores", "vt": "str" }, { @@ -5304,7 +5424,7 @@ "repair": false, "outputs": 4, "x": 2450, - "y": 1700, + "y": 1760, "wires": [ [ "ac9d1ea0c4c9dfbe" @@ -5325,9 +5445,10 @@ "id": "105c677a40b6461b", "type": "debug", "z": "744bdf3a0e08d754", + "d": true, "g": "f0815d4d5c1e13e0", "name": "✅ Éxito actualización", - "active": false, + "active": true, "tosidebar": true, "console": false, "tostatus": true, @@ -5336,13 +5457,14 @@ "statusVal": "✓ OK", "statusType": "str", "x": 3580, - "y": 1640, + "y": 1700, "wires": [] }, { "id": "f0f98e25ebd44ac3", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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;", @@ -5353,7 +5475,7 @@ "finalize": "", "libs": [], "x": 2840, - "y": 1860, + "y": 1920, "wires": [ [] ] @@ -5362,6 +5484,7 @@ "id": "67e7ede89ac08b87", "type": "mqtt in", "z": "744bdf3a0e08d754", + "d": true, "g": "f0815d4d5c1e13e0", "name": "Delete a record", "topic": "totemautonomo/contratos/delete", @@ -5373,7 +5496,7 @@ "rh": 0, "inputs": 0, "x": 2180, - "y": 1860, + "y": 1920, "wires": [ [ "f0f98e25ebd44ac3" @@ -5384,13 +5507,14 @@ "id": "36b9bf6834913595", "type": "json", "z": "744bdf3a0e08d754", + "d": true, "g": "f0815d4d5c1e13e0", "name": "", "property": "payload", "action": "", "pretty": false, "x": 2290, - "y": 1700, + "y": 1760, "wires": [ [ "0c144d19f803250f" @@ -5401,13 +5525,12 @@ "id": "c7a7b3e8c093ae1e", "type": "link in", "z": "744bdf3a0e08d754", + "d": true, "g": "f0815d4d5c1e13e0", "name": "data_tables", - "links": [ - "24855820a3fadbb8" - ], + "links": [], "x": 2315, - "y": 1760, + "y": 1820, "wires": [ [ "0c144d19f803250f" @@ -5418,6 +5541,7 @@ "id": "afd34cb6f61a6475", "type": "debug", "z": "744bdf3a0e08d754", + "d": true, "g": "f0815d4d5c1e13e0", "name": "debug 37", "active": true, @@ -5429,16 +5553,17 @@ "statusVal": "", "statusType": "auto", "x": 3080, - "y": 1620, + "y": 1680, "wires": [] }, { "id": "ac9d1ea0c4c9dfbe", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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", + "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\n\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 )\nflow.set(\"tabla_update\", \"Matriculas\") // indicamos la tabla que vamos a actualizar para utilizarlo en el nodo de feeback\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\n//la linea ${valuesArray.length ? lo que hace es evitar que se haga un INSERT con filas que no existen\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, @@ -5446,7 +5571,7 @@ "finalize": "", "libs": [], "x": 2810, - "y": 1660, + "y": 1720, "wires": [ [ "afd34cb6f61a6475", @@ -5458,9 +5583,10 @@ "id": "b9b6a469329290a5", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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", + "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\n\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(\"identificadores_counter\", valuesArray.length);\nflow.set(\"tabla_update\", \"Identificadores\") // indicamos la tabla que vamos a actualizar para utilizarlo en el nodo de feeback\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, @@ -5468,7 +5594,7 @@ "finalize": "", "libs": [], "x": 2820, - "y": 1700, + "y": 1760, "wires": [ [ "afd34cb6f61a6475", @@ -5480,9 +5606,10 @@ "id": "76a88549a38a751d", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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", + "name": "MatriculasIdentificadores", + "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\n\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(\"matriculasIdentificadores_counter\", valuesArray.length )\nflow.set(\"tabla_update\", \"MatriculasIdentificadores\") // indicamos la tabla que vamos a actualizar para utilizarlo en el nodo de feeback\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 MatriculasIdentificadores;\n${valuesArray.length ? `\nINSERT INTO MatriculasIdentificadores (matricula, id)\nVALUES ${valuesArray.join(\",\")};\n` : \"\"}\nCOMMIT;\n`;\nmsg.payload = \"\"\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5490,7 +5617,7 @@ "finalize": "", "libs": [], "x": 2850, - "y": 1740, + "y": 1800, "wires": [ [ "afd34cb6f61a6475", @@ -5502,6 +5629,7 @@ "id": "e0c3a3c33ed7aaca", "type": "debug", "z": "744bdf3a0e08d754", + "d": true, "g": "f0815d4d5c1e13e0", "name": "debug 36", "active": false, @@ -5513,21 +5641,23 @@ "statusVal": "", "statusType": "auto", "x": 2620, - "y": 1620, + "y": 1680, "wires": [] }, { "id": "7aee3c3ea10fd9a5", "type": "mysql", "z": "744bdf3a0e08d754", + "d": true, "g": "f0815d4d5c1e13e0", "mydb": "1c8613764b7f685e", "name": "Actualiza tablas", "x": 3260, - "y": 1680, + "y": 1740, "wires": [ [ - "105c677a40b6461b" + "105c677a40b6461b", + "8722ca0bd371a5f2" ] ] }, @@ -5535,13 +5665,12 @@ "id": "aa870f8d73e4f504", "type": "link in", "z": "744bdf3a0e08d754", + "d": true, "g": "f0815d4d5c1e13e0", "name": "delete_items_in", - "links": [ - "6a2b1526103aa3b0" - ], + "links": [], "x": 3135, - "y": 1820, + "y": 1880, "wires": [ [ "7aee3c3ea10fd9a5" @@ -5571,7 +5700,7 @@ "payload": "0236MWK", "payloadType": "str", "x": 2120, - "y": 2040, + "y": 2100, "wires": [ [ "b76ed973b1299bb9" @@ -5588,7 +5717,7 @@ "6703fb916aef8f4b" ], "x": 2345, - "y": 2080, + "y": 2140, "wires": [ [ "9a65971441f7579f", @@ -5607,7 +5736,7 @@ "cb6c28fe05c25229" ], "x": 2915, - "y": 2020, + "y": 2080, "wires": [] }, { @@ -5618,7 +5747,7 @@ "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, + "y": 2160, "wires": [] }, { @@ -5636,7 +5765,7 @@ "statusVal": "", "statusType": "auto", "x": 2720, - "y": 2000, + "y": 2060, "wires": [] }, { @@ -5654,7 +5783,7 @@ "statusVal": "", "statusType": "auto", "x": 2500, - "y": 2100, + "y": 2160, "wires": [] }, { @@ -5672,7 +5801,7 @@ "statusVal": "", "statusType": "auto", "x": 2860, - "y": 2100, + "y": 2160, "wires": [] }, { @@ -5681,7 +5810,7 @@ "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", + "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 MatriculasIdentificadores 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 MatriculasIdentificadores 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, @@ -5689,7 +5818,7 @@ "finalize": "", "libs": [], "x": 2300, - "y": 2040, + "y": 2100, "wires": [ [ "ceba75f09293b8f4" @@ -5710,7 +5839,7 @@ "finalize": "", "libs": [], "x": 2750, - "y": 2040, + "y": 2100, "wires": [ [ "22f142c6e6d19334", @@ -5737,7 +5866,7 @@ "statusVal": "", "statusType": "auto", "x": 2880, - "y": 1980, + "y": 2040, "wires": [] }, { @@ -5748,7 +5877,7 @@ "mydb": "1c8613764b7f685e", "name": "check matricula", "x": 2540, - "y": 2040, + "y": 2100, "wires": [ [ "a16351341e1a09da", @@ -5762,7 +5891,7 @@ "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*/", + "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 MatriculasIdentificadores 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 MatriculasIdentificadores 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, @@ -5770,7 +5899,7 @@ "finalize": "", "libs": [], "x": 3270, - "y": 2020, + "y": 2080, "wires": [ [ "4f214eb96e82b18b", @@ -5801,7 +5930,7 @@ "payload": "4265HGS", "payloadType": "str", "x": 3100, - "y": 2060, + "y": 2120, "wires": [ [ "dcd65882403b8019" @@ -5822,7 +5951,7 @@ "finalize": "", "libs": [], "x": 3710, - "y": 2020, + "y": 2080, "wires": [ [ "3605e72173f34f32", @@ -5849,7 +5978,7 @@ "statusVal": "", "statusType": "auto", "x": 3900, - "y": 1980, + "y": 2040, "wires": [] }, { @@ -5863,7 +5992,7 @@ "cb6c28fe05c25229" ], "x": 3895, - "y": 2020, + "y": 2080, "wires": [] }, { @@ -5874,7 +6003,7 @@ "name": "Generacion de Lista", "links": [], "x": 3145, - "y": 2100, + "y": 2160, "wires": [ [ "dcd65882403b8019" @@ -5889,7 +6018,7 @@ "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, + "y": 2160, "wires": [] }, { @@ -5903,7 +6032,7 @@ "cb6c28fe05c25229" ], "x": 3895, - "y": 2060, + "y": 2120, "wires": [] }, { @@ -5921,7 +6050,7 @@ "statusVal": "", "statusType": "auto", "x": 3900, - "y": 2100, + "y": 2160, "wires": [] }, { @@ -5939,7 +6068,7 @@ "statusVal": "", "statusType": "auto", "x": 3680, - "y": 1980, + "y": 2040, "wires": [] }, { @@ -5950,7 +6079,7 @@ "mydb": "1c8613764b7f685e", "name": "get menu", "x": 3460, - "y": 2020, + "y": 2080, "wires": [ [ "eb466c2601840e18", @@ -5973,7 +6102,7 @@ "statusVal": "", "statusType": "auto", "x": 3420, - "y": 1980, + "y": 2040, "wires": [] }, { @@ -5982,15 +6111,15 @@ "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", + "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 MatriculasIdentificadores mi\n ON TRIM(mi.id) = TRIM(input.id)\nLEFT JOIN Identificadores i\n ON TRIM(i.id) = TRIM(input.id)\nLIMIT 1;`;\n}\n\nelse if (msg.tipo == \"qr\") {\n let qr = msg.payload\n msg.topic = `\nSELECT\n CASE WHEN i.id IS NOT NULL THEN TRUE ELSE FALSE END AS autorizado,\n i.descripcion AS descripcion,\n i.nivel AS nivel\nFROM (SELECT '${qr}' AS id) AS input\nLEFT JOIN Identificadores i\n ON TRIM(i.id) = TRIM(input.id)\nLIMIT 1;`;\n}\n\n\n\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 MatriculasIdentificadores mi\n ON TRIM(mi.id) = TRIM(input.id)\nLEFT JOIN Identificadores i\n ON TRIM(i.id) = TRIM(input.id)\nLIMIT 1;`;\n}\n*/\nreturn msg;\n\n\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 2240, - "y": 2360, + "x": 2220, + "y": 2560, "wires": [ [ "57fd039f5dadf4fb", @@ -6019,10 +6148,10 @@ "once": false, "onceDelay": 0.1, "topic": "", - "payload": "546DA2A4-B07B-482D-B98F-B4EB0FFABCAE", + "payload": "546DA2A4-C456-482D-B98F-B4EB0FFABB0E", "payloadType": "str", - "x": 2090, - "y": 2360, + "x": 2070, + "y": 2560, "wires": [ [ "9b47806ebf017d2c" @@ -6038,8 +6167,8 @@ "links": [ "4e1f0ff54eef4e7b" ], - "x": 2095, - "y": 2320, + "x": 2075, + "y": 2520, "wires": [ [ "9b47806ebf017d2c" @@ -6059,8 +6188,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 2750, - "y": 2360, + "x": 2730, + "y": 2560, "wires": [ [ "9bd381438eec96eb", @@ -6079,8 +6208,8 @@ "links": [ "cb6c28fe05c25229" ], - "x": 2895, - "y": 2320, + "x": 2875, + "y": 2520, "wires": [] }, { @@ -6106,8 +6235,8 @@ "topic": "", "payload": "1983696218", "payloadType": "str", - "x": 2090, - "y": 2400, + "x": 2070, + "y": 2600, "wires": [ [ "9b47806ebf017d2c" @@ -6128,8 +6257,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 2380, - "y": 2420, + "x": 2360, + "y": 2620, "wires": [] }, { @@ -6139,8 +6268,8 @@ "g": "f968efec7b96ec13", "mydb": "1c8613764b7f685e", "name": "check QR / Tarjeta", - "x": 2530, - "y": 2360, + "x": 2510, + "y": 2560, "wires": [ [ "ea115b9b38cdaea1", @@ -6148,191 +6277,6 @@ ] ] }, - { - "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", @@ -6350,8 +6294,8 @@ "y": 560, "wires": [ [ - "6455539b9fac2063", - "08bf0c9897d6e815" + "08bf0c9897d6e815", + "6455539b9fac2063" ], [ "e36bb58515b89053", @@ -6480,7 +6424,7 @@ "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", + "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.payload.IdMovimiento}, 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, @@ -6490,7 +6434,9 @@ "x": 2690, "y": 540, "wires": [ - [], + [ + "a58ecf0f246aff6a" + ], [ "732068c66d8e0a25", "10affb7590fa95f1" @@ -6515,7 +6461,7 @@ "wires": [ [ "1bdaeab49913082e", - "6a2b1526103aa3b0" + "a5ccb375313029ce" ], [ "8778197111d2ab0d", @@ -6532,7 +6478,7 @@ "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;", + "func": "const dias = Number(flow.get(\"dias_en_BD\")) || 3\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, @@ -6578,20 +6524,6 @@ ] ] }, - { - "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", @@ -6690,6 +6622,7 @@ "id": "32e555ac8f46a0f1", "type": "mqtt in", "z": "744bdf3a0e08d754", + "d": true, "g": "e716da8ff5c458c9", "name": "ack", "topic": "totemautonomo/movimientos/movimiento_ok", @@ -6713,6 +6646,7 @@ "id": "e36bb58515b89053", "type": "mqtt out", "z": "744bdf3a0e08d754", + "d": true, "g": "e716da8ff5c458c9", "name": "send data", "topic": "totemautonomo/movimientos/movimiento", @@ -6732,6 +6666,7 @@ "id": "6b1ae2b4cdb257fb", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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 ;", @@ -6754,6 +6689,7 @@ "id": "7ef59a8ac4d8a7f3", "type": "debug", "z": "744bdf3a0e08d754", + "d": true, "g": "e716da8ff5c458c9", "name": "tx mqtt-1", "active": true, @@ -6772,6 +6708,7 @@ "id": "aba034491220fa28", "type": "debug", "z": "744bdf3a0e08d754", + "d": true, "g": "e716da8ff5c458c9", "name": "rx ack-1", "active": true, @@ -6790,6 +6727,7 @@ "id": "ac94ab2b132fab52", "type": "trigger", "z": "744bdf3a0e08d754", + "d": true, "g": "e716da8ff5c458c9", "name": "timeout", "op1": "", @@ -6818,6 +6756,7 @@ "id": "774b2334aae0188c", "type": "change", "z": "744bdf3a0e08d754", + "d": true, "g": "e716da8ff5c458c9", "name": "init uuid", "rules": [ @@ -6844,6 +6783,7 @@ "id": "9a5e603ea67d2991", "type": "debug", "z": "744bdf3a0e08d754", + "d": true, "g": "e716da8ff5c458c9", "name": "timeout", "active": true, @@ -6862,6 +6802,7 @@ "id": "9606dbbee4d79399", "type": "comment", "z": "744bdf3a0e08d754", + "d": true, "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.", @@ -6899,7 +6840,7 @@ "initialize": "", "finalize": "", "libs": [], - "x": 2990, + "x": 3030, "y": 500, "wires": [ [], @@ -7229,7 +7170,7 @@ "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", + "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//---------------------------------------------------------------------------------------------\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//-------------------------------------------------------\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 -------------------------\n/*\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);\n*/\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 -------------------------------------------------------------------------\n/*\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\nread = config.conexion.path_contexto;\nconfig_conex.push({ \"parametro\": \"path_contexto\", \"valor\": read })\nflow.set(\"path_contexto\", 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----------------------------------------------------------\n/*\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-----------------------------------------------------------------------\n/*\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, @@ -7272,7 +7213,7 @@ "z": "744bdf3a0e08d754", "g": "8aa65df7a757f075", "name": "debug 2", - "active": false, + "active": true, "tosidebar": true, "console": false, "tostatus": false, @@ -7281,7 +7222,7 @@ "statusVal": "", "statusType": "auto", "x": 1080, - "y": 1360, + "y": 1340, "wires": [] }, { @@ -7294,7 +7235,7 @@ "action": "", "pretty": false, "x": 830, - "y": 1440, + "y": 1380, "wires": [ [ "73752ba2ae415953", @@ -7308,15 +7249,15 @@ "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, + "func": "if (msg.payload.device == \"supervisor\") {\n //incluimos en el mensaje (fuera del payload) estos elementos \"numero_vial\" y \"totem_id\"para utilizarlos al momento de responder al totem\n //Recordemos que en este caso es OBLIGATORIO conservar el mensaje entrante con todos los elementos incluyendo el \"msg._session\"\n //Esto es debido a que el Supervisor atiende a multiples clientes simultanemaente y tenemos que asegurar que no se mezclen las respuestas\n //ya que eso ocasionaria un mal funcionamiento en el sistema.\n msg.numero_vial = msg.payload.numero_vial\n msg.totem_id = msg.payload.totem_id\n msg.sender = msg.payload.sender\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 MatriculasIdentificadores. 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 MatriculasIdentificadores 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 MatriculasIdentificadores 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 nombre_contexto = new_register.nombre_contexto\n let imagen_contexto = new_register.imagen_contexto\n \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 de firma en disco local\n node.send([null, null, null, null, null, msg_firma, null])\n }\n if(nombre_contexto != \"\"){\n let msg_contexto = { payload: imagen_contexto, filename: flow.get(\"path_contexto\") + nombre_contexto } // guardamos archivo con imagen de contexto en disco local\n node.send([null, null, null, null, msg_contexto, null, null, null, null, null, null, null])\n }\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, nombre_contexto_i) \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}', '${new_register.nombre_contexto}')`;\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.nombre_contexto_i = new_register.nombre_contexto\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 msg_inicial.payload.imagen_contexto_i = new_register.imagen_contexto || \"\"\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 case \"check_ubicacion\":\n msg.topic = `SELECT * FROM UbicacionesInternas WHERE id = '${msg.payload.id_residuo}' `\n msg.payload = \"\"\n node.send([null, null, null, null, null, null, null, null, msg])\n break;\n \n // ********************* COMANDO PARA ACTUALIZACION DE TABLAS DE LA BD ********************************************\n case \"actualizar\":\n flow.set(\"numero_vial_sender\", msg.numero_vial)\n flow.set(\"totem_id_sender\", msg.totem_id)\n flow.set(\"urgesbas_session\", msg._session) // guardamos sesion del comando\n msg.payload.tables = msg.payload.tabla // cambiamos nombre para ser compatibles con el grupo de actualizacion de tablas\n delete msg.payload.tabla\n if (msg.payload.hasOwnProperty(\"tables\") && msg.payload.IdCentroProveedor === flow.get(\"IdCentroProveedor\")){\n node.send([null, null, null, null, null, null, null, null, null, msg, null, null])\n }\n else{\n msg.payload.sender = \"Supervisor\"\n msg.payload.IdCentroProveedor = flow.get(\"IdCentroProveedor\")\n msg.payload.result = false\n msg.payload.error = \"NOMBRE DE TABLA NO DEFINIDo\"\n node.warn(\"ERROR: TABLA NO DEFINIDA\")\n node.send([null, null, null, null, null, null, null, null, null, null, null, msg])\n }\n\n break;\n // ************************COMANDO PARA LA GESTION DE LOS SEMAFOROS QUE CONTROLA EL SUPERVISOR ************************************************\n case \"semaforos\":\n if (msg.payload.IdCentroProveedor === flow.get(\"IdCentroProveedor\")){\n //node.warn(flow.get(\"IdCentroProveedor\"))\n node.send([null, null, null, null, null, null, null, null, null, null, msg, null])\n }\n break;\n }\n \n\n}\n\n// ************************ GESTION DE COMANDOS DE UN TOTEM CONECTADO AL SUPERVISOR ***********************************************************************\nelse if (msg.payload.device === \"totem\" && msg.payload.IdCentroProveedor === flow.get(\"IdCentroProveedor\")){\n let msg_totem = {}\n switch (msg.payload.accion) {\n case \"abrirbarrera\":\n msg_totem.payload = {totem_id: msg.payload.totem_id, numero_vial: msg.payload.numero_vial, abrirbarrera: true}\n node.send([null, null, null, null, null, null, null, null, null, null, null, msg_totem])\n break;\n case \"semaforo_on\":\n msg_totem.payload = { totem_id: msg.payload.totem_id, numero_vial: msg.payload.numero_vial, semaforoabierto: true }\n node.send([null, null, null, null, null, null, null, null, null, null, null, msg_totem])\n break;\n case \"semaforo_off\":\n //node.warn(\"semaforo\")\n msg_totem.payload = { totem_id: msg.payload.totem_id, numero_vial: msg.payload.numero_vial, semaforoabierto: false }\n node.send([null, null, null, null, null, null, null, null, null, null, null, msg_totem])\n break;\n case \"init_config\":\n msg_totem.payload = { totem_id: msg.payload.totem_id, numero_vial: msg.payload.numero_vial, init_config: true }\n node.send([null, null, null, null, null, null, null, null, null, null, null, msg_totem])\n break;\n case \"reboot\":\n msg_totem.payload = { totem_id: msg.payload.totem_id, numero_vial: msg.payload.numero_vial, reboot_flag: true }\n node.send([null, null, null, null, null, null, null, null, null, null, null, msg_totem])\n break;\n }\n}\nreturn;", + "outputs": 12, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1090, - "y": 1460, + "y": 1520, "wires": [ [ "6703fb916aef8f4b" @@ -7337,11 +7278,24 @@ "9fa72afe7d97828e" ], [ - "5c4679ed20761208" + "5c4679ed20761208", + "11a1e6cca1c726b6" ], [ "138317044e960cfa", "dc3fe6bd2494cab0" + ], + [ + "53f4bfb1ec29f1e1" + ], + [ + "45be747f248c1a59" + ], + [ + "4adc1ea54b29ee78" + ], + [ + "e0afc0a180416993" ] ] }, @@ -7356,15 +7310,15 @@ "a1ac4bf3b8c14b21" ], "x": 1315, - "y": 1400, + "y": 1340, "wires": [] }, { "id": "b264dc44d4344d72", "type": "inject", "z": "744bdf3a0e08d754", - "g": "8aa65df7a757f075", - "name": "", + "g": "45b20bb1509256b3", + "name": "check 6234FLH", "props": [ { "p": "payload" @@ -7381,8 +7335,8 @@ "topic": "", "payload": "{\"device\":\"supervisor\",\"totem_id\":\"totem_103\",\"accion\":\"check\",\"matricula\":\"6234FLH\"}", "payloadType": "json", - "x": 830, - "y": 1500, + "x": 780, + "y": 1540, "wires": [ [ "de78dc5fb92df251" @@ -7400,7 +7354,7 @@ "8e56735bbb37a598" ], "x": 1315, - "y": 1440, + "y": 1380, "wires": [] }, { @@ -7414,7 +7368,7 @@ "c674aac5bef7594a" ], "x": 1315, - "y": 1480, + "y": 1420, "wires": [] }, { @@ -7430,7 +7384,7 @@ "overwriteFile": "true", "encoding": "none", "x": 1410, - "y": 1560, + "y": 1500, "wires": [ [] ] @@ -7448,8 +7402,8 @@ "complete": "false", "statusVal": "", "statusType": "auto", - "x": 1400, - "y": 1680, + "x": 1380, + "y": 1620, "wires": [] }, { @@ -7467,15 +7421,15 @@ "statusVal": "", "statusType": "auto", "x": 1560, - "y": 1720, + "y": 1660, "wires": [] }, { "id": "66630f04db34f483", "type": "inject", "z": "744bdf3a0e08d754", - "g": "8aa65df7a757f075", - "name": "salida", + "g": "45b20bb1509256b3", + "name": "movimiento final", "props": [ { "p": "payload" @@ -7492,7 +7446,7 @@ "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, + "x": 780, "y": 1620, "wires": [ [ @@ -7504,8 +7458,8 @@ "id": "2d857220b76dda21", "type": "inject", "z": "744bdf3a0e08d754", - "g": "8aa65df7a757f075", - "name": "entrada", + "g": "45b20bb1509256b3", + "name": "movimiento inicial", "props": [ { "p": "payload" @@ -7522,7 +7476,7 @@ "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, + "x": 780, "y": 1660, "wires": [ [ @@ -7541,15 +7495,15 @@ "0ba07aef43c4a226" ], "x": 1315, - "y": 1520, + "y": 1460, "wires": [] }, { "id": "1b2d9af6c1e13e5f", "type": "inject", "z": "744bdf3a0e08d754", - "g": "8aa65df7a757f075", - "name": "1234ABC", + "g": "45b20bb1509256b3", + "name": "check 1234ABC", "props": [ { "p": "payload" @@ -7566,8 +7520,8 @@ "topic": "", "payload": "{\"device\":\"supervisor\",\"totem_id\":\"totem_103\",\"accion\":\"check\",\"matricula\":\"1234ABC\"}", "payloadType": "json", - "x": 840, - "y": 1540, + "x": 780, + "y": 1580, "wires": [ [ "de78dc5fb92df251" @@ -7582,7 +7536,7 @@ "mydb": "1c8613764b7f685e", "name": "guarda movimiento", "x": 1410, - "y": 1640, + "y": 1580, "wires": [ [] ] @@ -7597,7 +7551,7 @@ "2dc5c2942966b097" ], "x": 655, - "y": 1880, + "y": 2040, "wires": [ [ "cc229c10e3f4e423" @@ -7619,7 +7573,7 @@ "statusVal": "", "statusType": "auto", "x": 920, - "y": 1820, + "y": 1980, "wires": [] }, { @@ -7628,15 +7582,15 @@ "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, + "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_contexto_i = movinicial.nombre_contexto_i\n msg.payload.nombre_contexto_f = movfinal.nombre_contexto\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_contexto_i = movinicial.imagen_contexto_i || \"\"\n msg.payload.imagen_contexto_f = movfinal.imagen_contexto || \"\"\n //msg.payload.imagen_firma = movinicial.firma || \"\"\n flow.set(\"movimiento_cerrado_temp\", msg.payload)\n \n //***** Guardamos imagen del vehiculo del movimiento final \n let msg_imagen_f = { payload: movfinal.imagen, filename: flow.get(\"path_images\") + movfinal.nombre_imagen } // guardamos archivo con imagen del vehiculo del movimiento final en disco local\n node.send([null, null, null, msg_imagen_f])\n \n //***** Leemos imagen del vehiculo del movimiento inicial\n let msg_imagen_i = { topic: \"imagen_matricula\", filename: flow.get(\"path_images\") + movinicial.nombre_imagen }// leemos archivo con imagen del vehiculo del movimiento inicial del disco local\n flow.set(\"imagen_matricula_rx\", false)\n node.send([msg_imagen_i, null, null])\n \n //***** Guardamos imagen de contexto del movimiento final\n if (msg.payload.nombre_contexto_f != \"\"){\n let msg_contexto_f = { payload: movfinal.imagen_contexto, filename: flow.get(\"path_contexto\") + movfinal.nombre_contexto } // guardamos archivo con imagen de contexto del vehiculo del movimiento final en disco local\n node.send([null, null, null, msg_contexto_f])\n }\n \n // Inicializamos flags de control de lectura de imagenes de contexto. Se utilizan en el proximo nodo para confirmar que tenemos todas las imagenes\n flow.set(\"imagen_contexto_i_rx\", true)\n \n //***** Leemos imagen de contexto del movimiento inicial\n if(msg.payload.nombre_contexto_i != \"\"){\n let msg_contexto_i = { topic: \"imagen_contexto_i\", filename: flow.get(\"path_contexto\") + movinicial.nombre_contexto_i }// leemos archivo con imagen del vehiculo del movimiento inicial del disco local\n flow.set(\"imagen_contexto_i_rx\", false)\n node.send([null, null, null, null, msg_contexto_i])\n }\n \n //***** Leemos imagen de la firma en el movimiento inicial\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 }// leemos archivo con imagen de la firma en movimiento inicial del disco local\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]) // ejecutamos la lectura del archivo\n }\n else{\n flow.set(\"imagen_firma_rx\", true) // simulamos como que recibimos la imagen de firma. Esto es para validar en el proximo nodo\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, nombre_contexto_i, nombre_contexto_f\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 '${esc(movinicial.nombre_contexto_i)}',\n '${esc(movfinal.nombre_contexto)}');`;\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": 5, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1010, - "y": 1880, + "y": 2040, "wires": [ [ "a1c2fb59c438c1c5" @@ -7646,6 +7600,12 @@ ], [ "65bfea0283dfab05" + ], + [ + "f2d77a74edf9958e" + ], + [ + "beaff73dc2b3d86d" ] ] }, @@ -7659,8 +7619,8 @@ "links": [ "c858b37cb0eeba4f" ], - "x": 1655, - "y": 1820, + "x": 1675, + "y": 2040, "wires": [] }, { @@ -7673,7 +7633,7 @@ "8a7201a76e53897f" ], "x": 855, - "y": 1980, + "y": 2140, "wires": [ [ "417e71d002f16583" @@ -7688,7 +7648,7 @@ "mydb": "1c8613764b7f685e", "name": "analisis", "x": 780, - "y": 1880, + "y": 2040, "wires": [ [ "405f569c2efbf22d", @@ -7704,7 +7664,7 @@ "mydb": "1c8613764b7f685e", "name": "guarda movimiento_c", "x": 1260, - "y": 1980, + "y": 2060, "wires": [ [] ] @@ -7713,9 +7673,10 @@ "id": "81f447b69bd2c603", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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;", + "func": "let mat = \"0131HLN\"\nmsg.topic = `\nWITH\nseeds AS (\n SELECT mi.id AS id_root\n FROM MatriculasIdentificadores 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, @@ -7723,7 +7684,7 @@ "finalize": "", "libs": [], "x": 960, - "y": 2220, + "y": 2460, "wires": [ [ "e22e7f0f1cea9cb5" @@ -7734,6 +7695,7 @@ "id": "d2082aabd7021a2e", "type": "inject", "z": "744bdf3a0e08d754", + "d": true, "g": "6fd6ce44bb173af7", "name": "", "props": [ @@ -7753,7 +7715,7 @@ "payload": "", "payloadType": "date", "x": 730, - "y": 2220, + "y": 2460, "wires": [ [ "81f447b69bd2c603" @@ -7764,6 +7726,7 @@ "id": "1d44274163bba745", "type": "debug", "z": "744bdf3a0e08d754", + "d": true, "g": "6fd6ce44bb173af7", "name": "debug 34", "active": true, @@ -7775,16 +7738,17 @@ "statusVal": "", "statusType": "auto", "x": 1300, - "y": 2220, + "y": 2460, "wires": [] }, { "id": "17b55fb17b52609b", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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;", + "func": "let mat = \"0131HLN\"\nmsg.topic = `\nSELECT mi.id AS id_root\nFROM MatriculasIdentificadores mi\nWHERE UPPER(TRIM(mi.matricula)) = UPPER(TRIM('${mat}')) ;\n`;\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -7792,7 +7756,7 @@ "finalize": "", "libs": [], "x": 970, - "y": 2300, + "y": 2540, "wires": [ [ "e22e7f0f1cea9cb5" @@ -7803,6 +7767,7 @@ "id": "f769af64083450ed", "type": "inject", "z": "744bdf3a0e08d754", + "d": true, "g": "6fd6ce44bb173af7", "name": "", "props": [ @@ -7822,7 +7787,7 @@ "payload": "", "payloadType": "date", "x": 730, - "y": 2300, + "y": 2540, "wires": [ [ "17b55fb17b52609b" @@ -7833,6 +7798,7 @@ "id": "a550322a7e6cafd2", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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;", @@ -7843,7 +7809,7 @@ "finalize": "", "libs": [], "x": 960, - "y": 2380, + "y": 2620, "wires": [ [ "e22e7f0f1cea9cb5" @@ -7854,6 +7820,7 @@ "id": "9e4bc4c1c4148e95", "type": "inject", "z": "744bdf3a0e08d754", + "d": true, "g": "6fd6ce44bb173af7", "name": "id", "props": [ @@ -7873,7 +7840,7 @@ "payload": "D3220E78-2338-4738-B0D2-499D0223EFFF", "payloadType": "str", "x": 730, - "y": 2380, + "y": 2620, "wires": [ [ "a550322a7e6cafd2" @@ -7884,11 +7851,12 @@ "id": "e22e7f0f1cea9cb5", "type": "mysql", "z": "744bdf3a0e08d754", + "d": true, "g": "6fd6ce44bb173af7", "mydb": "1c8613764b7f685e", "name": "analisis", "x": 1140, - "y": 2220, + "y": 2460, "wires": [ [ "1d44274163bba745" @@ -7908,7 +7876,7 @@ "oldrc": false, "name": "Screen ON/OFF", "x": 940, - "y": 2600, + "y": 2860, "wires": [ [], [], @@ -7938,7 +7906,7 @@ "payload": "echo 1 | sudo tee /sys/class/backlight/0-0045/bl_power", "payloadType": "str", "x": 720, - "y": 2580, + "y": 2840, "wires": [ [ "6a066066436f56ef" @@ -7968,7 +7936,7 @@ "payload": "echo 0 | sudo tee /sys/class/backlight/0-0045/bl_power", "payloadType": "str", "x": 720, - "y": 2540, + "y": 2800, "wires": [ [ "6a066066436f56ef" @@ -7998,7 +7966,7 @@ "payload": "sudo shutdown -r now", "payloadType": "str", "x": 710, - "y": 2620, + "y": 2880, "wires": [ [ "6a066066436f56ef" @@ -8028,7 +7996,7 @@ "payload": "sudo poweroff", "payloadType": "str", "x": 720, - "y": 2660, + "y": 2920, "wires": [ [ "6a066066436f56ef" @@ -8045,7 +8013,7 @@ "e4f5cd90a27363a0" ], "x": 815, - "y": 2520, + "y": 2780, "wires": [ [ "6a066066436f56ef" @@ -8061,8 +8029,8 @@ "links": [ "63dfdbc95bd896d0" ], - "x": 655, - "y": 1440, + "x": 685, + "y": 1380, "wires": [ [ "182712df6587926f" @@ -8115,7 +8083,7 @@ "encoding": "none", "allProps": false, "x": 1250, - "y": 1860, + "y": 1980, "wires": [ [ "dd4decb988977915" @@ -8136,7 +8104,7 @@ "encoding": "none", "allProps": false, "x": 1230, - "y": 1900, + "y": 2020, "wires": [ [ "dd4decb988977915" @@ -8149,7 +8117,7 @@ "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 ;", + "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 (msg.topic == \"imagen_contexto_i\"){\n flow.set(\"imagen_contexto_i_rx\", true)\n flow.set(\"imagen_leida_contexto_i\", msg.payload)\n}\n\nif (flow.get(\"imagen_matricula_rx\") && flow.get(\"imagen_firma_rx\") && flow.get(\"imagen_contexto_i_rx\")){\n \n //Actualizacion de los elementos del movimiento que vamos a enviar (inicial + final)\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 if(msg.payload.nombre_contexto_i != \"\"){\n msg.payload.imagen_contexto_i = flow.get(\"imagen_leida_contexto_i\")\n }\n node.send (msg)\n}\nreturn ;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -8157,7 +8125,7 @@ "finalize": "", "libs": [], "x": 1510, - "y": 1880, + "y": 2040, "wires": [ [ "44c64f7eb7bdf346", @@ -8178,7 +8146,7 @@ "overwriteFile": "true", "encoding": "none", "x": 1400, - "y": 1600, + "y": 1540, "wires": [ [] ] @@ -8199,7 +8167,7 @@ "authType": "", "senderr": false, "x": 1390, - "y": 1720, + "y": 1660, "wires": [ [ "4f30b9755ed381bb" @@ -8210,9 +8178,10 @@ "id": "1dae889e8697bf15", "type": "function", "z": "744bdf3a0e08d754", + "d": true, "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", + "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\n\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 )\nflow.set(\"tabla_update\", \"UbicacionesInternas\") // indicamos la tabla que vamos a actualizar para utilizarlo en el nodo de feeback\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, @@ -8220,7 +8189,7 @@ "finalize": "", "libs": [], "x": 2840, - "y": 1780, + "y": 1840, "wires": [ [ "7aee3c3ea10fd9a5", @@ -8229,31 +8198,912 @@ ] }, { - "id": "eca4e2e077d64f16", + "id": "b6bf6045b97bcc3a", + "type": "debug", + "z": "744bdf3a0e08d754", + "name": "debug 45", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2640, + "y": 2280, + "wires": [] + }, + { + "id": "5dcbef935a14195e", + "type": "debug", + "z": "744bdf3a0e08d754", + "name": "debug 46", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2380, + "y": 2280, + "wires": [] + }, + { + "id": "53f4bfb1ec29f1e1", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "to ubicacionesinternas", + "mode": "link", + "links": [ + "d3197fdbc8ead3c3" + ], + "x": 1325, + "y": 1720, + "wires": [] + }, + { + "id": "22c0ec71d86183eb", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "e6cc59abb0016d36", + "mydb": "1c8613764b7f685e", + "name": "check Ubicaciones", + "x": 2170, + "y": 2360, + "wires": [ + [ + "2153eeb22a8551a0", + "5dcbef935a14195e" + ] + ] + }, + { + "id": "d3197fdbc8ead3c3", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "e6cc59abb0016d36", + "name": "get ubicaciones internas", + "links": [ + "53f4bfb1ec29f1e1" + ], + "x": 2025, + "y": 2360, + "wires": [ + [ + "22c0ec71d86183eb" + ] + ] + }, + { + "id": "2153eeb22a8551a0", "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", + "g": "e6cc59abb0016d36", + "name": "respuesta UI", + "func": "let record = msg.payload\nmsg.payload = {numero_vial: msg.numero_vial, totem_id: msg.totem_id}\n// Si encontramos un sitio donde dirigirse\nif (record.length > 0 ){\n msg.payload.ubicacion_interna = true\n msg.payload.descripcion = record[0].descripcion\n}\n// Si NO se encuentra sitio donde dirigirse\nelse {\n msg.payload.ubicacion_interna = false\n msg.payload.descripcion = \"No hay Ubicacion Interna\"\n}\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 2420, - "y": 2720, + "x": 2430, + "y": 2360, "wires": [ [ - "e87018791b56046f" + "c7d75c1b61b0d370", + "b6bf6045b97bcc3a" ] ] }, { - "id": "cc2d5ea437442925", + "id": "c7d75c1b61b0d370", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "e6cc59abb0016d36", + "name": "salida ubicaciones", + "mode": "link", + "links": [ + "cb6c28fe05c25229" + ], + "x": 2605, + "y": 2360, + "wires": [] + }, + { + "id": "b3b8ba19a1803cfd", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "110ca6b0bb9f248a", + "name": "Aplica Configuracion", + "info": "", + "x": 350, + "y": 1040, + "wires": [] + }, + { + "id": "6f0cd359cf151c60", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "110ca6b0bb9f248a", + "name": "Recepcion de \\n Movimientos y Comandos \\n Guarda Movimientos \\n Envia Mov. Inicial al Servidor", + "info": "", + "x": 380, + "y": 1120, + "wires": [] + }, + { + "id": "0ad183c1c32db0d0", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "110ca6b0bb9f248a", + "name": "Generacion de \\n Movimientos Cerrados", + "info": "", + "x": 360, + "y": 1220, + "wires": [] + }, + { + "id": "212c50537ac465f0", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "110ca6b0bb9f248a", + "name": "Envia Movimientos al Servidor \\n Actualiza BD diariamente \\n Sirve datos de movimientos (API)", + "info": "", + "x": 390, + "y": 1300, + "wires": [] + }, + { + "id": "051399d75c3148ce", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "110ca6b0bb9f248a", + "name": "Sincroniza Movimientos \\n Anteriores", + "info": "", + "x": 360, + "y": 1400, + "wires": [] + }, + { + "id": "f848d959035c5b4e", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "110ca6b0bb9f248a", + "name": "Actualizacion de Tablas locales", + "info": "", + "x": 390, + "y": 1460, + "wires": [] + }, + { + "id": "53b75027e49e6050", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "110ca6b0bb9f248a", + "name": "Revisa si Matricula Existe \\n Genera Menus de Residuos", + "info": "", + "x": 380, + "y": 1520, + "wires": [] + }, + { + "id": "7281c7346d01c4e3", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "110ca6b0bb9f248a", + "name": "Revisa Ubicacion Interna", + "info": "", + "x": 370, + "y": 1580, + "wires": [] + }, + { + "id": "a44f7d1f3334d404", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "110ca6b0bb9f248a", + "name": "Revisa si QR o Tarjeta \\n existen en BD", + "info": "", + "x": 360, + "y": 1640, + "wires": [] + }, + { + "id": "45be747f248c1a59", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "tablas", + "mode": "link", + "links": [ + "ae0e0b8787f929be" + ], + "x": 1325, + "y": 1760, + "wires": [] + }, + { + "id": "4adc1ea54b29ee78", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "semaforos", + "mode": "link", + "links": [ + "eb17917cdc2ad86e" + ], + "x": 1325, + "y": 1800, + "wires": [] + }, + { + "id": "e0afc0a180416993", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "websocket out", + "mode": "link", + "links": [ + "cb6c28fe05c25229" + ], + "x": 1325, + "y": 1840, + "wires": [] + }, + { + "id": "03b6b47b9544437e", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "110ca6b0bb9f248a", + "name": "Sirve una pagina para configurar\\n los parametros del sistema", + "info": "", + "x": 390, + "y": 1720, + "wires": [] + }, + { + "id": "8722ca0bd371a5f2", + "type": "function", + "z": "744bdf3a0e08d754", + "d": true, + "g": "f0815d4d5c1e13e0", + "name": "Update feedback", + "func": "const r = msg.payload; // array de resultados (multi-statements)\n\n// START(0), DELETE(1), INSERT(2 si existe), COMMIT(último)\nconst deleted = r?.[1]?.affectedRows ?? 0;\n\n// Si no hubo INSERT (valuesArray vacío), el array suele ser más corto\nconst inserted = (r.length >= 4) ? (r?.[2]?.affectedRows ?? 0) : 0;\nlet msg_feedback = {}\nmsg_feedback._session = flow.get(\"urgesbas_session\"); // muy importante para responder al mismo cliente\nconst now = new Date();\nconst stamp = now.toLocaleString(\"es-ES\", { timeZone: \"Europe/Madrid\" });\nmsg_feedback.payload = {\n sender: \"Supervisor\",\n device: flow.get(\"device_sender\"),\n result: true,\n tabla: flow.get(\"table_update\") || \"desconocida\",\n hora: stamp,\n IdCentroProveedor: flow.get(\"IdCentroProveedor\"),\n borrados: deleted,\n insertados: inserted\n \n};\n\nreturn msg_feedback;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3470, + "y": 1840, + "wires": [ + [ + "18fa8d0658cc9ba1", + "3804a5fcd6ce7f3e" + ] + ] + }, + { + "id": "18fa8d0658cc9ba1", + "type": "debug", + "z": "744bdf3a0e08d754", + "d": true, + "g": "f0815d4d5c1e13e0", + "name": "update feedback", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3690, + "y": 1800, + "wires": [] + }, + { + "id": "3804a5fcd6ce7f3e", + "type": "link out", + "z": "744bdf3a0e08d754", + "d": true, + "g": "f0815d4d5c1e13e0", + "name": "feedback", + "mode": "link", + "links": [ + "cb6c28fe05c25229" + ], + "x": 3655, + "y": 1880, + "wires": [] + }, + { + "id": "ce6670be92cd4852", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "Prepara Plan", + "func": "\n// Build Prepare Plan (4 tablas)\n// Evita dos sync simultáneas\n// Construimos un array con las 4 tablas a actualizar cada elemento del array es: (sync_id, tabla, phase) phase indica si estamos en mod opreparacion o en modo commit para cambiar las tablas en BD\n//sync_id es un id que generamos para asociar a toda la operacion de sincronizacion de manera a evitar confuciones de datos\nif (flow.get(\"sync_busy\")) { // aqui evitamos hacer una sincronizacion si no hemos terminado la actual\n node.warn(\"SYNC ya en curso. Ignorando nuevo START.\");\n return null;\n}\nflow.set(\"sync_busy\", true);\n\n// Sync ID\nconst sync_id = (Date.now().toString(36) + \"-\" + Math.random().toString(16).slice(2)).toUpperCase();\nflow.set(\"sync_id\", sync_id);\n\nlet tables\nif (msg.payload.tables === \"todas\"){\n tables = [\"UbicacionesInternas\", \"Identificadores\", \"Matriculas\", \"MatriculasIdentificadores\"]\n}\nelse{\n tables = [msg.payload.tables]\n flow.set(\"tables\", tables)\n}\nmsg.expected = tables.length;\nflow.set(\"expected_tables\", msg.expected)\n/*\ntables = Array.isArray(msg.tables) && msg.tables.length\n ? msg.tables\n : [\"UbicacionesInternas\", \"Identificadores\", \"Matriculas\", \"MatriculasIdentificadores\"];\n\n\n// Orden correcto de la actualización de las tablas\ntables = [\n \"UbicacionesInternas\",\n \"Identificadores\",\n \"Matriculas\",\n \"MatriculasIdentificadores\"\n];\n*/\n\n// Plan de PREPARE (cada item será un msg despues del nodo split o division)\nmsg.payload = tables.map(t => ({\n sync_id,\n table: t,\n phase: \"prepare\"\n}));\n\n// limpia marcas anteriores\nfor (const t of tables) {\n flow.set(`${t}_prepared_ts`, null);\n flow.set(`${t}_committed_ts`, null);\n}\n\nmsg.sync_id = sync_id;\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2430, + "y": 2860, + "wires": [ + [ + "74ab401f1064a600" + ] + ] + }, + { + "id": "477599045e6aebb5", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "Decide commit", + "func": "\n// Decide Commit\n// Espera: msg.payload = [ {table, ok, ...}, ... ] de las 4 tablas\n\nconst results = msg.payload;\nconst sync_id = flow.get(\"sync_id\");\nconst expected = Number(flow.get(\"expected_tables\") || 4);\n\n// Validación básica (N tablas)\nif (!Array.isArray(results) || results.length !== expected) {\n node.warn(`No llegaron ${expected} resultados de PREPARE. Llegaron ${Array.isArray(results) ? results.length : \"null\"}. Abortando.`);\n flow.set(\"sync_busy\", false);\n return null;\n}\n\n\nconst failed = results.filter(r => !r || r.ok !== true);\nif (failed.length) {\n node.warn(\"PREPARE falló en: \" + failed.map(f => f.table || \"?\").join(\", \"));\n // aquí podrías enviar feedback al cliente con details\n flow.set(\"sync_busy\", false);\n msg.payload = { sync_id, ok: false, phase: \"prepare\", failed };\n return msg; // o return null si no quieres continuar\n}\n\n// Si todo OK: construye plan de COMMIT\nlet tables\nif (flow.get(\"expected_tables\") === 4){\ntables = [\n \"UbicacionesInternas\",\n \"Identificadores\",\n \"Matriculas\",\n \"MatriculasIdentificadores\"\n];\n}\nelse if (flow.get(\"expected_tables\") === 1) {\n tables = flow.get(\"tables\"); \n}\nmsg.payload = tables.map(t => ({\n sync_id,\n table: t,\n phase: \"commit\"\n}));\n\nmsg.sync_id = sync_id;\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2720, + "y": 3080, + "wires": [ + [ + "1259840b70ad958f" + ] + ] + }, + { + "id": "74ab401f1064a600", + "type": "split", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "", + "splt": "\\n", + "spltType": "str", + "arraySplt": 1, + "arraySpltType": "len", + "stream": false, + "addname": "", + "property": "payload", + "x": 2590, + "y": 2860, + "wires": [ + [ + "a7f0444a0f26b7e4" + ] + ] + }, + { + "id": "2f0a0951674cb0f7", + "type": "join", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "join prepare", + "mode": "custom", + "build": "array", + "property": "payload", + "propertyType": "msg", + "key": "topic", + "joiner": "\\n", + "joinerType": "str", + "useparts": false, + "accumulate": false, + "timeout": "", + "count": "4", + "reduceRight": false, + "reduceExp": "", + "reduceInit": "", + "reduceInitType": "", + "reduceFixup": "", + "x": 2540, + "y": 3080, + "wires": [ + [ + "477599045e6aebb5" + ] + ] + }, + { + "id": "057182f23d107ea9", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "request", + "func": "// Cada solicitud esta separada de un delay configurado en el nodo anterior para evitar colisiones en los certificados del https\nconst table = msg.payload.table\nmsg.table = table\nmsg.url = `api/Totem/Seleccionar${table}Totem?token=${flow.get(\"tokenapi\")}&idCentroProveedor=${flow.get(\"IdCentroProveedor\")}`\nif (table === \"UbicacionesInternas\"){\n msg.url = `api/Totem/Seleccionar${table}?token=${flow.get(\"tokenapi\")}&idCentroProveedor=${flow.get(\"IdCentroProveedor\")}`\n}\nmsg.url = flow.get(\"url_API\") + msg.url\nmsg.mode = msg.payload.phase\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2900, + "y": 2860, + "wires": [ + [ + "f2da7ffbe437010d" + ] + ] + }, + { + "id": "f2da7ffbe437010d", + "type": "https-node", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "https request", + "method": "GET", + "ret": "txt", + "paytoqs": "ignore", + "url": "", + "tls": "", + "persist": false, + "proxy": "", + "authType": "", + "senderr": false, + "x": 3070, + "y": 2860, + "wires": [ + [ + "343b7f167c6ee9a8" + ] + ] + }, + { + "id": "de2470b64fc3a3ba", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "Generico Prepare/Commit", + "func": "let datos = msg.payload; // aqui llega el array con los datos de la tabla\nconst table = msg.table;\nconst mode = (msg.mode || \"prepare\").toString().toLowerCase(); // <-- captura ANTES\n\nconst sync_id = msg.sync_id;\n\n// Normaliza datos si vienen como string \nif (typeof datos === \"string\") {\n try { datos = JSON.parse(datos); }\n catch (err) {\n node.error(\"Error al parsear datos: \" + err.message);\n return null;\n }\n}\n\n// ====================== CONFIGURACION POR CADA TABLA ======================\nconst TABLES = {\n \"Matriculas\": {\n cols: [\"matricula\", \"tara\", \"pma\"],\n row: (o, esc) => {\n const m = esc((o.matricula || \"\").trim().toUpperCase());\n const tara = Number(o.tara) || 0;\n const pma = Number(o.pma) || 0;\n return `('${m}', ${tara}, ${pma})`;\n }\n },\n \"Identificadores\": {\n cols: [\"id\", \"id_padre\", \"nivel\", \"descripcion\"],\n row: (o, esc) => {\n const id = esc((o.id || \"\").trim());\n const id_padre = (o.id_padre === null || o.id_padre === undefined || o.id_padre === \"\")\n ? \"NULL\"\n : `'${esc(String(o.id_padre).trim())}'`;\n const nivel = (o.nivel === null || o.nivel === undefined) ? \"NULL\" : (Number(o.nivel) || 0);\n const desc = (o.descripcion === null || o.descripcion === undefined)\n ? \"NULL\"\n : `'${esc(String(o.descripcion).trim())}'`;\n return `('${id}', ${id_padre}, ${nivel}, ${desc})`;\n }\n },\n \"UbicacionesInternas\": {\n cols: [\"id\", \"descripcion\"],\n row: (o, esc) => {\n const id = esc((o.id || \"\").trim());\n const desc = (o.descripcion === null || o.descripcion === undefined)\n ? \"NULL\"\n : `'${esc(String(o.descripcion).trim())}'`;\n return `('${id}', ${desc})`;\n }\n },\n \"MatriculasIdentificadores\": {\n cols: [\"matricula\", \"id\"],\n row: (o, esc) => {\n const m = esc((o.matricula || \"\").trim().toUpperCase());\n const id = esc((o.id || \"\").trim());\n return `('${m}', '${id}')`;\n }\n }\n};\n\n// ====================== VALIDACIONES ======================\nif (!TABLES[table]) {\n node.warn(`Tabla no permitida o no configurada: ${table}`);\n return null;\n}\n\nconst esc = s => (s ?? \"\").toString().replace(/'/g, \"''\");\nconst cfg = TABLES[table];\nconst cols = cfg.cols.join(\", \");\nconst stg = `${table}_new`;\nconst batchSize = Number(msg.batchSize) || 800;\n\n// ====================== MODO COMMIT ====================== Esta es la fase final una vez que hemos recibido los datos de las tablas y los hemos colocado en tablas temporales dentro de la BD\nif (mode === \"commit\") {\n msg.topic = `\nRENAME TABLE\n ${table} TO ${table}_old,\n ${stg} TO ${table},\n ${table}_old TO ${stg};\n`;\n msg.payload = \"\";\n msg.mode = \"commit\"; // <-- importante: conservarlo\n msg.sync_id = sync_id; // <-- si lo usas\n\n flow.set(\"tabla_update\", table);\n flow.set(`${table}_committed_ts`, new Date().toISOString());\n return msg;\n}\n\n// ====================== MODO PREPARE ====================== Esta fase es la que usamos para recibir los datos de las tablas y colocarlos en las tablas temporales de la BD\nif (!Array.isArray(datos)) {\n node.warn(\"datos no es un array (prepare).\");\n return null;\n}\n\nconst rows = datos.map(o => cfg.row(o, esc));\n\nlet inserts = \"\";\nfor (let i = 0; i < rows.length; i += batchSize) {\n const chunk = rows.slice(i, i + batchSize);\n if (!chunk.length) continue;\n\n if (table === \"Matriculas\") {\n // Si llega una matrícula duplicada en el mismo batch, MySQL actualiza (último gana)\n inserts += `\nINSERT INTO ${stg} (${cols}) VALUES ${chunk.join(\",\")}\nON DUPLICATE KEY UPDATE\n tara = VALUES(tara),\n pma = VALUES(pma);\n`;\n } else {\n inserts += `\nINSERT INTO ${stg} (${cols}) VALUES ${chunk.join(\",\")};\n`;\n }\n}\n\nmsg.topic = `\nCREATE TABLE IF NOT EXISTS ${stg} LIKE ${table};\nTRUNCATE TABLE ${stg};\n${inserts}\n`;\n\nmsg.payload = \"\";\nmsg.mode = \"prepare\"; // <-- importante para los nodos siguientes\nmsg.sync_id = sync_id;\n\nflow.set(\"tabla_update\", table);\nflow.set(`${table}_size`, rows.length);\nflow.set(`${table}_batches`, Math.ceil(rows.length / batchSize));\nflow.set(`${table}_prepared_ts`, new Date().toISOString());\n\nreturn msg;\n\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2510, + "y": 2960, + "wires": [ + [ + "95a5ae68ae30005f" + ] + ] + }, + { + "id": "a7f0444a0f26b7e4", + "type": "delay", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "", + "pauseType": "delay", + "timeout": "1", + "timeoutUnits": "seconds", + "rate": "1", + "nbRateUnits": "1", + "rateUnits": "second", + "randomFirst": "1", + "randomLast": "5", + "randomUnits": "seconds", + "drop": false, + "allowrate": false, + "outputs": 1, + "x": 2740, + "y": 2860, + "wires": [ + [ + "057182f23d107ea9" + ] + ] + }, + { + "id": "95a5ae68ae30005f", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "mydb": "1c8613764b7f685e", + "name": "Actualiza tablas 2", + "x": 2750, + "y": 2960, + "wires": [ + [ + "6a1e4d0c93dd137c" + ] + ] + }, + { + "id": "6a1e4d0c93dd137c", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "DB OK", + "func": "\nconst table = msg.table || \"UNKNOWN\";\nconst mode = (msg.mode || \"prepare\").toLowerCase();\n\nmsg.payload = {\n sync_id: msg.sync_id || flow.get(\"sync_id\"),\n table,\n ok: true,\n phase: mode,\n rows: flow.get(`${table}_size`) ?? null,\n batches: flow.get(`${table}_batches`) ?? null\n};\nmsg.topic = \"\" // lo borramos para que el mensaje no lleve el query, que ya no es necesario\n\nif (mode === \"prepare\"){\n node.send([msg, null])\n}\nelse if (mode === \"commit\"){\n node.send([null, msg])\n}\nreturn;", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2940, + "y": 2960, + "wires": [ + [ + "dabbaae991aa5540" + ], + [ + "e2e2d5e11894a5ca" + ] + ] + }, + { + "id": "1226068e0f3be997", + "type": "catch", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "Check Actualiza 2", + "scope": [ + "95a5ae68ae30005f" + ], + "uncaught": false, + "x": 2070, + "y": 3080, + "wires": [ + [ + "50f01b1f2ed222c9" + ] + ] + }, + { + "id": "50f01b1f2ed222c9", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "Check for error", + "func": "const table = msg.table || msg._sync?.table || \"UNKNOWN\";\nconst mode = (msg.mode || msg._sync?.mode || \"prepare\").toLowerCase();\n\nmsg.payload = {\n sync_id: msg.sync_id || flow.get(\"sync_id\"),\n table,\n ok: false,\n phase: mode,\n error: msg.error?.message || \"mysql error\"\n};\n\n// NO borres msg.table aquí; lo necesitas para debug\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2280, + "y": 3080, + "wires": [ + [ + "2f0a0951674cb0f7" + ] + ] + }, + { + "id": "c0351940eb077ea3", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "debug 55", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3580, + "y": 3000, + "wires": [] + }, + { + "id": "dabbaae991aa5540", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "prepare ok", + "mode": "link", + "links": [ + "1188e42874ce799f" + ], + "x": 3075, + "y": 2920, + "wires": [] + }, + { + "id": "1188e42874ce799f", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "joint", + "links": [ + "dabbaae991aa5540" + ], + "x": 2185, + "y": 3040, + "wires": [ + [ + "301d1ad243d7df4c" + ] + ] + }, + { + "id": "70d8ef5cb93723c1", + "type": "join", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "join commit", + "mode": "custom", + "build": "array", + "property": "payload", + "propertyType": "msg", + "key": "topic", + "joiner": "\\n", + "joinerType": "str", + "useparts": false, + "accumulate": false, + "timeout": "", + "count": "4", + "reduceRight": false, + "reduceExp": "", + "reduceInit": "", + "reduceInitType": "", + "reduceFixup": "", + "x": 3230, + "y": 2960, + "wires": [ + [ + "5f68071edd29bae4" + ] + ] + }, + { + "id": "1259840b70ad958f", + "type": "split", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "", + "splt": "\\n", + "spltType": "str", + "arraySplt": 1, + "arraySpltType": "len", + "stream": false, + "addname": "", + "property": "payload", + "x": 2870, + "y": 3080, + "wires": [ + [ + "473c44f182c41108" + ] + ] + }, + { + "id": "473c44f182c41108", + "type": "change", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "change", + "rules": [ + { + "t": "set", + "p": "table", + "pt": "msg", + "to": "payload.table", + "tot": "msg" + }, + { + "t": "set", + "p": "mode", + "pt": "msg", + "to": "payload.phase", + "tot": "msg" + }, + { + "t": "set", + "p": "sync_id", + "pt": "msg", + "to": "payload.sync_id", + "tot": "msg" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 3000, + "y": 3080, + "wires": [ + [ + "175be4afd40e4b72" + ] + ] + }, + { + "id": "301d1ad243d7df4c", + "type": "switch", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "sw", + "property": "expected_tables", + "propertyType": "flow", + "rules": [ + { + "t": "eq", + "v": "4", + "vt": "num" + }, + { + "t": "eq", + "v": "1", + "vt": "num" + } + ], + "checkall": "true", + "repair": false, + "outputs": 2, + "x": 2310, + "y": 3040, + "wires": [ + [ + "2f0a0951674cb0f7" + ], + [ + "9263d310331ec72f" + ] + ] + }, + { + "id": "9263d310331ec72f", + "type": "join", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "join prepare", + "mode": "custom", + "build": "array", + "property": "payload", + "propertyType": "msg", + "key": "topic", + "joiner": "\\n", + "joinerType": "str", + "useparts": false, + "accumulate": false, + "timeout": "", + "count": "1", + "reduceRight": false, + "reduceExp": "", + "reduceInit": "", + "reduceInitType": "", + "reduceFixup": "", + "x": 2540, + "y": 3120, + "wires": [ + [ + "477599045e6aebb5" + ] + ] + }, + { + "id": "e2e2d5e11894a5ca", + "type": "switch", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "switch", + "property": "expected_tables", + "propertyType": "flow", + "rules": [ + { + "t": "eq", + "v": "4", + "vt": "num" + }, + { + "t": "eq", + "v": "1", + "vt": "num" + } + ], + "checkall": "true", + "repair": false, + "outputs": 2, + "x": 3090, + "y": 2980, + "wires": [ + [ + "70d8ef5cb93723c1" + ], + [ + "766671d6ac2d3fef" + ] + ] + }, + { + "id": "ae0e0b8787f929be", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "ACTUALIZAR TABLAS", + "links": [ + "45be747f248c1a59" + ], + "x": 2305, + "y": 2840, + "wires": [ + [ + "ce6670be92cd4852" + ] + ] + }, + { + "id": "615724f93d501817", "type": "inject", "z": "744bdf3a0e08d754", - "name": "UbicacionesInternas", + "g": "b6dc38f78b9f146c", + "name": "Ubicaciones", + "props": [ + { + "p": "payload.tables", + "v": "UbicacionesInternas", + "vt": "str" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "x": 2150, + "y": 2920, + "wires": [ + [ + "ce6670be92cd4852" + ] + ] + }, + { + "id": "0f7ac5ca216ea759", + "type": "change", + "z": "744bdf3a0e08d754", + "g": "b6dc38f78b9f146c", + "name": "sync", + "rules": [ + { + "t": "set", + "p": "sync_busy", + "pt": "flow", + "to": "false", + "tot": "bool" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 2190, + "y": 2860, + "wires": [ + [] + ] + }, + { + "id": "c26fd52fb4cdd1ea", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "b6dc38f78b9f146c", + "name": "", "props": [ { "p": "payload" @@ -8268,16 +9118,514 @@ "once": false, "onceDelay": 0.1, "topic": "", - "payload": "UbicacionesInternas", - "payloadType": "str", - "x": 1850, - "y": 2800, + "payload": "1", + "payloadType": "num", + "x": 2070, + "y": 2860, "wires": [ [ - "c95d9408ac6b1595" + "0f7ac5ca216ea759" ] ] }, + { + "id": "adc10412b8e600cf", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "b6dc38f78b9f146c", + "name": "todas", + "props": [ + { + "p": "payload.tables", + "v": "todas", + "vt": "str" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "x": 2170, + "y": 2960, + "wires": [ + [ + "ce6670be92cd4852" + ] + ] + }, + { + "id": "d29750f0c8e69c5f", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "debug 57", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3580, + "y": 2940, + "wires": [] + }, + { + "id": "5f68071edd29bae4", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "Unlock", + "func": "let results = msg.payload;\nconst sync_id = flow.get(\"sync_id\");\n\n// Normaliza a array\nif (!Array.isArray(results)) {\n results = (results && typeof results === \"object\") ? [results] : [];\n}\n\n// libera lock\nflow.set(\"sync_busy\", false);\n\n// expected robusto (usa tu nombre expected_tables)\nconst expectedFlow = Number(flow.get(\"expected_tables\"));\nconst expected = (Number.isFinite(expectedFlow) && expectedFlow > 0) ? expectedFlow : results.length;\n\nconst gotCount = results.length;\nconst okAll = gotCount > 0 && results.every(r => r && r.ok === true);\nconst okExpectedCount = (gotCount === expected);\n\n// Construir rows por tabla\nconst rowsByTable = {};\nfor (const r of results) {\n const t = r.table || \"UNKNOWN\";\n // prioridad: si viene en el result -> si no, lo tomamos de flow (prepare)\n const n = (r.rows !== null && r.rows !== undefined)\n ? r.rows\n : (flow.get(`${t}_size`) ?? null);\n\n if (t !== \"UNKNOWN\") rowsByTable[t] = n;\n}\n\n// (Opcional) resumen por tabla con ok/error\nconst tables = {};\nfor (const r of results) {\n const t = r.table || \"UNKNOWN\";\n tables[t] = {\n ok: r.ok === true,\n error: r.error || null\n };\n}\n// Armamos el mensaje de feedback\nconst now = new Date()\nconst nowdate = now.toLocaleString('es-ES', { hour12: false });\nlet msg_confirm = {payload:{}}\nmsg_confirm._session = flow.get(\"urgesbas_session\")\nmsg_confirm.payload = {\n sender: \"Supervisor\",\n device: \"Urgesbas\",\n IdCentroProveedor: flow.get(\"IdCentroProveedor\"),\n result: okAll && okExpectedCount,\n //expected_tables: expected,\n //got_tables: gotCount,\n time: nowdate,\n rows: rowsByTable,\n ...(okAll && okExpectedCount ? {} : { error: msg.error }) // <-- solo si okAll && okExpectedCount=false\n \n};\nlet statusText = `result:${okAll && okExpectedCount ? '🟢' : '🔴'} Tablas:${gotCount}`\n\nnode.status({\n fill: \"grey\", \n shape: \"dot\", \n text: statusText\n});\nreturn ([msg, msg_confirm]);\n\n", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3390, + "y": 2980, + "wires": [ + [ + "d29750f0c8e69c5f" + ], + [ + "c0351940eb077ea3", + "284196caeb4787bf" + ] + ] + }, + { + "id": "766671d6ac2d3fef", + "type": "join", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "join commit", + "mode": "custom", + "build": "array", + "property": "payload", + "propertyType": "msg", + "key": "topic", + "joiner": "\\n", + "joinerType": "str", + "useparts": false, + "accumulate": false, + "timeout": "", + "count": "1", + "reduceRight": false, + "reduceExp": "", + "reduceInit": "", + "reduceInitType": "", + "reduceFixup": "", + "x": 3230, + "y": 3000, + "wires": [ + [ + "5f68071edd29bae4" + ] + ] + }, + { + "id": "284196caeb4787bf", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "Salida Actualizacion Tablas", + "mode": "link", + "links": [ + "cb6c28fe05c25229" + ], + "x": 3535, + "y": 3060, + "wires": [] + }, + { + "id": "b3d236466b39e10b", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "generico actualizacion", + "links": [ + "343b7f167c6ee9a8", + "175be4afd40e4b72" + ], + "x": 2335, + "y": 2960, + "wires": [ + [ + "de2470b64fc3a3ba" + ] + ] + }, + { + "id": "343b7f167c6ee9a8", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "salida https", + "mode": "link", + "links": [ + "b3d236466b39e10b" + ], + "x": 3205, + "y": 2860, + "wires": [] + }, + { + "id": "175be4afd40e4b72", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "fc56c475842bc648", + "name": "salida decide commit", + "mode": "link", + "links": [ + "b3d236466b39e10b" + ], + "x": 3135, + "y": 3080, + "wires": [] + }, + { + "id": "bbe015cc5fb9128c", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "45b20bb1509256b3", + "name": "Actualizar 1", + "props": [ + { + "p": "payload" + }, + { + "p": "_session", + "v": "{\"name\": \"no hay datos\"}", + "vt": "json" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{\"sender\":\"Urgesbas\",\"device\":\"supervisor\",\"accion\":\"actualizar\",\"tabla\":\"UbicacionesInternas\",\"IdCentroProveedor\":\"A1ADAC83-AEFA-4E1F-8944-56722AC43F4F\",\"numero_vial\":2,\"totem_id\":\"totem_1\"}", + "payloadType": "json", + "x": 790, + "y": 1460, + "wires": [ + [ + "de78dc5fb92df251" + ] + ] + }, + { + "id": "52693c4aeb043be3", + "type": "inject", + "z": "744bdf3a0e08d754", + "g": "45b20bb1509256b3", + "name": "Actualizar 4", + "props": [ + { + "p": "payload" + }, + { + "p": "_session", + "v": "{\"name\": \"no hay datos\"}", + "vt": "json" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{\"sender\":\"Urgesbas\",\"device\":\"supervisor\",\"accion\":\"actualizar\",\"tabla\":\"todas\",\"IdCentroProveedor\":\"A1ADAC83-AEFA-4E1F-8944-56722AC43F4F\",\"numero_vial\":2,\"totem_id\":\"totem_1\"}", + "payloadType": "json", + "x": 790, + "y": 1500, + "wires": [ + [ + "de78dc5fb92df251" + ] + ] + }, + { + "id": "dd447b7f9321fedc", + "type": "comment", + "z": "744bdf3a0e08d754", + "g": "8aa65df7a757f075", + "name": "REVISAR", + "info": "En la recepcion del cierre final nos falta guardar la imagen de la matricula y de la firma en el sistema de archivos. ", + "x": 1080, + "y": 1640, + "wires": [] + }, + { + "id": "f2d77a74edf9958e", + "type": "file", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "name": "guarda imagenes", + "filename": "filename", + "filenameType": "msg", + "appendNewline": false, + "createDir": true, + "overwriteFile": "true", + "encoding": "none", + "x": 1250, + "y": 2120, + "wires": [ + [] + ] + }, + { + "id": "583af6c1434c1d45", + "type": "debug", + "z": "744bdf3a0e08d754", + "name": "debug 52", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 3640, + "y": 1040, + "wires": [] + }, + { + "id": "573edc4212227ac9", + "type": "json", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "", + "property": "payload", + "action": "", + "pretty": false, + "x": 3450, + "y": 1160, + "wires": [ + [ + "b1c81347b429091a", + "583af6c1434c1d45" + ] + ] + }, + { + "id": "a5ccb375313029ce", + "type": "mysql", + "z": "744bdf3a0e08d754", + "g": "75dd57faa07f33b9", + "mydb": "1c8613764b7f685e", + "name": "Actualiza tablas", + "x": 3360, + "y": 580, + "wires": [ + [] + ] + }, + { + "id": "beaff73dc2b3d86d", + "type": "file in", + "z": "744bdf3a0e08d754", + "g": "a57600b79a898710", + "name": "imagen contexto_i", + "filename": "filename", + "filenameType": "msg", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 1250, + "y": 2160, + "wires": [ + [ + "dd4decb988977915" + ] + ] + }, + { + "id": "11a1e6cca1c726b6", + "type": "debug", + "z": "744bdf3a0e08d754", + "name": "debug 53", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1740, + "y": 1600, + "wires": [] + }, + { + "id": "7348a0042f799a64", + "type": "file in", + "z": "744bdf3a0e08d754", + "d": true, + "g": "68b92de7bc4c82f6", + "name": "lee contexto_i", + "filename": "filename", + "filenameType": "msg", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 2880, + "y": 1260, + "wires": [ + [ + "89f64c532d3d78e7" + ] + ] + }, + { + "id": "3637f6cd8bf662ae", + "type": "file in", + "z": "744bdf3a0e08d754", + "d": true, + "g": "68b92de7bc4c82f6", + "name": "lee contexto_f", + "filename": "filename", + "filenameType": "msg", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 2880, + "y": 1300, + "wires": [ + [ + "89f64c532d3d78e7" + ] + ] + }, + { + "id": "d7c0cf92f7146c98", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "extrae nombre\\n de imagenes", + "func": "\n// Aqui analizamos los movimientos pendientes y tomamos el movimiento correspondiente para sincronizarlo\nif (msg.inicio){\n flow.set(\"pendientes\", msg.payload); // Si es la primera solicitud de sincronizacion guardamos el array en memoria flow, sino vamos directo a las proximas lineas\n}\n//indiceActual se refiere al indice que llevamos del array que hemos recibimos del nodo anterior mysql con todos los movimientos que necesitan sincronizacion\nlet indiceActual = flow.get(\"indiceActual\")\nlet pendientes = flow.get(\"pendientes\")\n\n\n\n// ===== Function 1: preparar lecturas de imágenes =====\n\nconst mov = pendientes[indiceActual]; // registro del movimiento cerrado\nconst uuid = mov.IdMovimiento;\n\n// Ajusta estas rutas base a tu instalación real\nconst BASE_IMG = flow.get(\"path_images\");\nconst BASE_CTX = flow.get(\"path_contexto\");\nconst BASE_FIRMA = flow.get(\"path_firmas\")\n\n// Helper. Averigua si una variable contiene texto\nfunction tieneTexto(v) {\n return v !== undefined && v !== null && String(v).trim() !== \"\";\n}\n\n// Construimos las tareas de lectura. Es un array donde estan las imagenes que debemos incluir en el movimiento json a enviar\nlet tasks = [];\n\n// ---- imagen inicial ----\nif (tieneTexto(mov.nombre_imagen_i)) {\n tasks.push({\n tipo: \"imagen_i\",\n filename: BASE_IMG + mov.nombre_imagen_i\n });\n}\n\n// ---- imagen final ----\nif (tieneTexto(mov.nombre_imagen_f)) {\n tasks.push({\n tipo: \"imagen_f\",\n filename: BASE_IMG + mov.nombre_imagen_f\n });\n}\n\n// ---- firma ----\nif (tieneTexto(mov.nombre_firma) && mov.nombre_firma !== \"No Habilitada\") {\n tasks.push({\n tipo: \"firma\",\n filename: BASE_FIRMA + mov.nombre_firma\n });\n}\n\n// ---- contexto inicial ----\nif (tieneTexto(mov.nombre_contexto_i)) {\n tasks.push({\n tipo: \"contexto_i\",\n filename: BASE_CTX + mov.nombre_contexto_i\n });\n}\n\n// ---- contexto final ----\nif (tieneTexto(mov.nombre_contexto_f)) {\n tasks.push({\n tipo: \"contexto_f\",\n filename: BASE_CTX + mov.nombre_contexto_f\n });\n}\n\n// Aqui ya tenemos un array con los nombres de archivos que debemos buscar e incluir en el movimiento\nconst expectedCount = tasks.length;\n\n// Si por alguna razón no hay ninguna imagen que leer, (es decir longitud del array = 0)\n// sacamos ya el movimiento listo sin pasar por file in.\n\nif (expectedCount === 0) {\n msg.payload = {\n ...mov,\n imagen_i: null,\n imagen_f: null,\n firma: null,\n contexto_i: null,\n contexto_f: null\n };\n msg._ready_to_send = true;\n return [msg, null]; // salida 2: directo a enviar\n}\n\n// Dado que tenemos un nodo de lectura de archivos, generamos un mensaje por cada tarea, es decir una secuencia de mensajes\nconst msgs = tasks.map(t => {\n return {\n payload: t.filename, // redundante\n filename: t.filename, // aqui es donde el nodo de lectura espera recibir el nombre del archivo\n tipoImagen: t.tipo,\n uuid: uuid,\n expectedCount: expectedCount,\n\n // guardamos también el movimiento original para reconstruir luego\n movimiento: mov\n };\n});\n\n// salida 1: hacia lectura de archivos\n// salida 2: bypass si no había imágenes\nreturn [null, msgs];", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2620, + "y": 1400, + "wires": [ + [], + [ + "bdfee81a7eb304fd" + ] + ] + }, + { + "id": "bdfee81a7eb304fd", + "type": "file in", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "lectura de archivo", + "filename": "filename", + "filenameType": "msg", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "allProps": false, + "x": 2870, + "y": 1400, + "wires": [ + [ + "47112b3cace36ea6" + ] + ] + }, + { + "id": "47112b3cace36ea6", + "type": "function", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "file in", + "func": "\n// ===== Function 2: recoger archivos leídos y armar JSON final =====\n\nconst uuid = msg.uuid;\nconst tipo = msg.tipoImagen;\nconst expected = msg.expectedCount;\nconst mov = msg.movimiento;\n\n// Estructura global de acumulación\nlet pend = flow.get(\"pendientes_resync\") || {};\n\n// Si no existe la entrada para este movimiento, la creamos\nif (!pend[uuid]) {\n pend[uuid] = {\n expected: expected,\n received: 0,\n movimiento: mov,\n imagenes: {}\n };\n}\n\n// Convertimos Buffer a base64\n/*\nlet b64 = null;\nif (Buffer.isBuffer(msg.payload)) {\n b64 = msg.payload.toString(\"base64\");\n} else if (typeof msg.payload === \"string\") {\n // por si algún nodo devolviera string\n b64 = Buffer.from(msg.payload).toString(\"base64\");\n} else {\n node.warn(`UUID ${uuid} tipo ${tipo}: payload de archivo no reconocido`);\n b64 = null;\n}\n*/\n\n// Guardamos la imagen\npend[uuid].imagenes[tipo] = msg.payload; // la imagen viene en Base64\npend[uuid].received += 1;\n\n// Persistimos, guardando todo lo que hemos recibido hasta el momento\nflow.set(\"pendientes_resync\", pend); \n\n// ¿Ya llegaron todas las imagenes necesarias?\nif (pend[uuid].received >= pend[uuid].expected) {\n const item = pend[uuid];\n const m = item.movimiento;\n const imgs = item.imagenes;\n\n // Armamos payload final\n \n msg.payload = {\n IdMovimiento: m.IdMovimiento,\n matricula: m.matricula,\n nombre_imagen_i: m.nombre_imagen_i,\n nombre_imagen_f: m.nombre_imagen_f,\n nombre_contexto_i: m.nombre_contexto_i,\n nombre_contexto_f: m.nombre_contexto_f,\n nombre_firma: m.nombre_firma,\n horaentrada_i: m.horaentrada_i,\n horasalida_i: m.horasalida_i,\n horaentrada_f: m.horaentrada_f,\n horasalida_f: m.horasalida_f,\n pesoEntrada: m.pesoEntrada,\n pesoSalida: m.pesoSalida,\n rfid_i: m.rfid_i,\n rfid_f: m.rfid_f,\n qr_i: m.qr_i,\n qr_f: m.qr_f,\n IdCentroServicio: m.IdCentroServicio,\n IdCentroProveedor: flow.get(\"IdCentroProveedor\"),\n vial: m.vial,\n totem: m.totem,\n ts_cierre: m.ts_cierre,\n\n // imágenes en base64\n imagen_entrada: imgs.imagen_i || null,\n imagen_salida: imgs.imagen_f || null,\n imagen_firma: imgs.firma || null,\n imagen_contexto_i: imgs.contexto_i || null,\n imagen_contexto_f: imgs.contexto_f || null\n };\n \n // limpieza para no dejar basura en memoria\n delete pend[uuid];\n flow.set(\"pendientes_resync\", pend);\n if (flow.get(\"movimientosxapi\")){\n msg.url = flow.get(\"url_API\") + `api/Totem/InsertarMovimientos?token=${flow.get(\"tokenapi\")}`\n msg.requestTimeout = 5000\n \n }\n return msg;\n}\n\n// Aún faltan archivos\nreturn null;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3050, + "y": 1400, + "wires": [ + [ + "498afa999a0e9dd7", + "f41c4b5c61a58ea2" + ] + ] + }, + { + "id": "498afa999a0e9dd7", + "type": "debug", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "debug 54", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 3200, + "y": 1480, + "wires": [] + }, + { + "id": "68b16275b7089e9b", + "type": "link in", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "sincro loop", + "links": [ + "ef53c5d39ed1b135" + ], + "x": 2435, + "y": 1400, + "wires": [ + [ + "d7c0cf92f7146c98" + ] + ] + }, + { + "id": "ef53c5d39ed1b135", + "type": "link out", + "z": "744bdf3a0e08d754", + "g": "68b92de7bc4c82f6", + "name": "sincro loop-2", + "mode": "link", + "links": [ + "68b16275b7089e9b" + ], + "x": 4255, + "y": 1180, + "wires": [] + }, { "id": "a4b79d82d1b9e587", "type": "inject", @@ -8301,7 +9649,7 @@ "payload": "MANUAL", "payloadType": "str", "x": 1760, - "y": 1020, + "y": 880, "wires": [ [ "06c0e45f61e0d364" @@ -8331,7 +9679,7 @@ "payload": "AUTO", "payloadType": "str", "x": 1760, - "y": 1060, + "y": 920, "wires": [ [ "06c0e45f61e0d364" @@ -8361,7 +9709,7 @@ "payload": "{\"S1\":\"GREEN\", \"S2\":\"RED\", \"S3\":\"RED\"}", "payloadType": "json", "x": 1770, - "y": 1100, + "y": 960, "wires": [ [ "49a1dfc2d343bdbb" @@ -8391,7 +9739,7 @@ "payload": "{\"S1\":\"RED\", \"S2\":\"GREEN\", \"S3\":\"RED\"}", "payloadType": "json", "x": 1770, - "y": 1140, + "y": 1000, "wires": [ [ "49a1dfc2d343bdbb" @@ -8421,7 +9769,7 @@ "payload": "{\"S1\":\"RED\", \"S2\":\"RED\", \"S3\":\"GREEN\"}", "payloadType": "json", "x": 1770, - "y": 1180, + "y": 1040, "wires": [ [ "49a1dfc2d343bdbb" @@ -8451,7 +9799,7 @@ "payload": "{\"S1\":\"RED\", \"S2\":\"RED\", \"S3\":\"RED\"}", "payloadType": "json", "x": 1770, - "y": 1220, + "y": 1080, "wires": [ [ "49a1dfc2d343bdbb" @@ -8546,7 +9894,7 @@ "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;", + "func": "const v = Number(msg.payload) ? 1 : 0;\nconst now = Date.now();\n\n// Guardamos el estado \"crudo\" por si quieres verlo\nflow.set('L1_raw', v);\n\n// Timer pendiente de confirmar OFF\nlet offTimer = flow.get('L1_off_timer') || null;\n\nif (v === 1) {\n // ON inmediato: cancelar cualquier OFF pendiente\n if (offTimer) {\n clearTimeout(offTimer);\n flow.set('L1_off_timer', null);\n }\n\n // Estado estable: ocupado\n flow.set('L1', 1);\n\n msg.topic = 'EV/L1_ON';\n msg.payload = { src: 'L1', value: 1, ts: now };\n\n node.status({ fill: \"red\", shape: \"dot\", text: \"OCUPADO\" });\n return msg;\n}\n\n// v === 0 -> NO mandar OFF aún; confirmar tras 800 ms\nif (!offTimer) {\n offTimer = setTimeout(() => {\n // Si tras 800 ms sigue OFF, confirmamos liberación\n if (!(flow.get('L1_raw') ? 1 : 0)) {\n flow.set('L1', 0);\n\n node.send({\n topic: 'EV/L1_OFF',\n payload: { src: 'L1', value: 0, ts: Date.now(), debounced_ms: 800 }\n });\n\n node.status({ fill: \"green\", shape: \"dot\", text: \"LIBRE\" });\n }\n flow.set('L1_off_timer', null);\n }, 800);\n\n flow.set('L1_off_timer', offTimer);\n}\n\nnode.status({ fill: \"yellow\", shape: \"dot\", text: \"OFF detectado (confirmando 800ms)\" });\nreturn null;", "outputs": 1, "timeout": "", "noerr": 0, @@ -8648,7 +9996,7 @@ "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;", + "func": "const v = Number(msg.payload) ? 1 : 0;\nconst now = Date.now();\n\n// Guardamos el estado \"crudo\" por si quieres verlo\nflow.set('L2_raw', v);\n\n// Timer pendiente de confirmar OFF\nlet offTimer = flow.get('L2_off_timer') || null;\n\nif (v === 1) {\n // ON inmediato: cancelar cualquier OFF pendiente\n if (offTimer) {\n clearTimeout(offTimer);\n flow.set('L2_off_timer', null);\n }\n\n // Estado estable: ocupado\n flow.set('L2', 1);\n\n msg.topic = 'EV/L2_ON';\n msg.payload = { src: 'L2', value: 1, ts: now };\n\n node.status({ fill: \"red\", shape: \"dot\", text: \"OCUPADO\" });\n return msg;\n}\n\n// v === 0 -> NO mandar OFF aún; confirmar tras 800 ms\nif (!offTimer) {\n offTimer = setTimeout(() => {\n // Si tras 800 ms sigue OFF, confirmamos liberación\n if (!(flow.get('L2_raw') ? 1 : 0)) {\n flow.set('L2', 0);\n\n node.send({\n topic: 'EV/L2_OFF',\n payload: { src: 'L2', value: 0, ts: Date.now(), debounced_ms: 800 }\n });\n\n node.status({ fill: \"green\", shape: \"dot\", text: \"LIBRE\" });\n }\n flow.set('L2_off_timer', null);\n }, 800);\n\n flow.set('L2_off_timer', offTimer);\n}\n\nnode.status({ fill: \"yellow\", shape: \"dot\", text: \"OFF detectado (confirmando 800ms)\" });\nreturn null;", "outputs": 1, "timeout": "", "noerr": 0, @@ -8750,7 +10098,7 @@ "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;", + "func": "const v = Number(msg.payload) ? 1 : 0;\nconst now = Date.now();\n\n// Guardamos el estado \"crudo\" por si quieres verlo\nflow.set('L3_raw', v);\n\n// Timer pendiente de confirmar OFF\nlet offTimer = flow.get('L3_off_timer') || null;\n\nif (v === 1) {\n // ON inmediato: cancelar cualquier OFF pendiente\n if (offTimer) {\n clearTimeout(offTimer);\n flow.set('L3_off_timer', null);\n }\n\n // Estado estable: ocupado\n flow.set('L3', 1);\n\n msg.topic = 'EV/L3_ON';\n msg.payload = { src: 'L3', value: 1, ts: now };\n\n node.status({ fill: \"red\", shape: \"dot\", text: \"OCUPADO\" });\n return msg;\n}\n\n// v === 0 -> NO mandar OFF aún; confirmar tras 800 ms\nif (!offTimer) {\n offTimer = setTimeout(() => {\n // Si tras 800 ms sigue OFF, confirmamos liberación\n if (!(flow.get('L3_raw') ? 1 : 0)) {\n flow.set('L3', 0);\n\n node.send({\n topic: 'EV/L3_OFF',\n payload: { src: 'L3', value: 0, ts: Date.now(), debounced_ms: 800 }\n });\n\n node.status({ fill: \"green\", shape: \"dot\", text: \"LIBRE\" });\n }\n flow.set('L3_off_timer', null);\n }, 800);\n\n flow.set('L3_off_timer', offTimer);\n}\n\nnode.status({ fill: \"yellow\", shape: \"dot\", text: \"OFF detectado (confirmando 800ms)\" });\nreturn null;", "outputs": 1, "timeout": "", "noerr": 0, @@ -8773,8 +10121,9 @@ "name": "EV BUS (desde LAZOS)", "mode": "link", "links": [ + "d95551b3e1091e71", "e7754eeae28a6256", - "d95551b3e1091e71" + "fab37c863ac68f06" ], "x": 1525, "y": 460, @@ -8862,7 +10211,8 @@ "mode": "link", "links": [ "e7754eeae28a6256", - "d95551b3e1091e71" + "d95551b3e1091e71", + "fab37c863ac68f06" ], "x": 1535, "y": 740, @@ -8892,7 +10242,7 @@ "payload": "1", "payloadType": "num", "x": 1730, - "y": 340, + "y": 300, "wires": [ [ "c9d524190cbe7f96" @@ -8914,7 +10264,7 @@ "finalize": "", "libs": [], "x": 1950, - "y": 340, + "y": 300, "wires": [ [] ] @@ -8929,13 +10279,16 @@ "c4159a812594fc62", "74ea72530de212a4", "15131de35b7d0425", - "2dc673e9f13e0f0b" + "2dc673e9f13e0f0b", + "77cf26031855da2d", + "5ecf6d948f96ea9a" ], "x": 1715, "y": 440, "wires": [ [ - "0e30fb1a5ee58746" + "0e30fb1a5ee58746", + "833d2a4708575d0e" ] ] }, @@ -8943,9 +10296,10 @@ "id": "0e30fb1a5ee58746", "type": "function", "z": "576f3807f61f5098", + "d": true, "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", + "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// FSM Dispatcher (SIMPLIFICADO)\n// =============================\nconst topic = msg.topic || '';\nconst now = Date.now();\n\n// ---------- Debug ON/OFF ----------\nconst DEBUG = flow.get(\"debug\");\nconst dbg = (s) => { if (DEBUG) node.warn(s); };\n\n// ---------- Context helpers ----------\nfunction setState(st) { flow.set('state', st); }\nfunction getState() { return flow.get('state') ?? 'IDLE'; }\n\n// ---------- Blocked management ----------\nlet blocked = flow.get('blocked') ?? { 1:false, 2:false, 3:false };\n\n// Si un lazo pasa a OFF, desbloqueamos ese acceso (resetea fallo por timeout de verde)\nif (topic === 'EV/L1_OFF') blocked[1] = false;\nif (topic === 'EV/L2_OFF') blocked[2] = false;\nif (topic === 'EV/L3_OFF') blocked[3] = false;\nflow.set('blocked', blocked);\n\n// ---------- Demand snapshot ----------\nfunction getDemands() {\n const b = flow.get('blocked') ?? { 1:false,2:false,3:false };\n return {\n 1: (flow.get('L1') ? 1 : 0) && !b[1] ? 1 : 0,\n 2: (flow.get('L2') ? 1 : 0) && !b[2] ? 1 : 0,\n 3: (flow.get('L3') ? 1 : 0) && !b[3] ? 1 : 0\n };\n}\nfunction anyDemand(d) { return (d[1] || d[2] || d[3]) ? 1 : 0; }\n\n// ---------- Command builders ----------\nfunction cmdSetSems(s1, s2, s3) {\n flow.set('S1', s1); flow.set('S2', s2); flow.set('S3', s3);\n return { topic:'CMD/SET_SEMS', payload:{ S1:s1, S2:s2, S3:s3, ts: now } };\n}\nfunction cmdTimer(tpc, payload = {}) {\n return { topic: tpc, payload: { ...payload, ts: now } };\n}\nfunction cmdSched(tpc, payload = {}) {\n return { topic: tpc, payload: { ...payload, ts: now } };\n}\nfunction requestPickIfPossible(outSchedArr) {\n const B = flow.get('B_OCC') ? 1 : 0;\n const d = getDemands();\n if (!B && anyDemand(d)) {\n dbg(`[PICK_REQ] st=${getState()} B=${B} demands=${JSON.stringify(d)} order=${JSON.stringify(flow.get('order'))}`);\n outSchedArr.push(cmdSched('CMD/PICK_NEXT', { demands: d }));\n }\n}\n\n// ---------- Outputs ----------\nlet outSems = null;\nlet outTimers = [];\nlet outSched = [];\n\n// ---------- Read current state + B_OCC ----------\nlet state = getState();\nlet B_OCC = flow.get('B_OCC') ? 1 : 0;\ndbg(`[FSM] IN topic=${topic} state=${state} B=${B_OCC}`);\n\n// ---------- Keep B_OCC updated from events ----------\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// =========================================================\n// EVENTO GLOBAL DE SEGURIDAD (SIMPLE Y LIMPIO)\n// Si se ocupa báscula: OCC SIEMPRE (y cancelamos CLR si estaba)\n// =========================================================\nif (topic === 'EV/B_OCC_ON') {\n // Si estabas en CLEAR, para el timer de despeje para que no dispare tarde\n outTimers.push(cmdTimer('CMD/STOP_CLR'));\n outSems = cmdSetSems('RED', 'RED', 'RED');\n setState('OCC');\n updateStatus();\n return [outSems, outTimers.length ? outTimers : null, null];\n}\n\n// =========================================================\n// ESTADO: OCC\n// - B_OCC=1: todo rojo\n// - EV/B_OCC_OFF: ir a CLEAR y arrancar CLR (delay de despeje)\n// =========================================================\nif (state === 'OCC') {\n if (topic === 'EV/B_OCC_OFF') {\n outSems = cmdSetSems('RED', 'RED', 'RED');\n\n const ms = flow.get('clr_ms') ?? 10000;\n outTimers.push(cmdTimer('CMD/START_CLR', { ms }));\n\n setState('CLEAR');\n updateStatus();\n return [outSems, outTimers.length ? outTimers : null, null];\n }\n\n // En OCC ignoramos lo demás\n outSems = cmdSetSems('RED', 'RED', 'RED');\n updateStatus();\n return [outSems, null, null];\n}\n\n// =========================================================\n// ESTADO: CLEAR\n// - Báscula libre pero esperamos despeje\n// - EV/CLR_TIMEOUT: pasar a IDLE y pedir pick\n// - EV/B_OCC_ON ya se manejó globalmente arriba\n// =========================================================\nif (state === 'CLEAR') {\n if (topic === 'EV/CLR_TIMEOUT') {\n setState('IDLE');\n requestPickIfPossible(outSched);\n updateStatus();\n return [null, null, outSched.length ? outSched : null];\n }\n // Durante CLEAR no hacemos arbitraje\n updateStatus();\n return [null, null, null];\n}\n\n// =========================================================\n// ESTADO: IDLE\n// - si llega EV/*: pedir pick si hay demanda\n// - si llega RSP/PICK: poner verde y arrancar Gi, pasar a DANDO_i\n// =========================================================\nif (state === 'IDLE') {\n if (!B_OCC) {\n if (topic === 'RSP/PICK' && msg.payload?.i) {\n dbg(`[PICK_RSP] st=${getState()} payload=${JSON.stringify(msg.payload)} demands_now=${JSON.stringify(getDemands())} order=${JSON.stringify(flow.get('order'))}`);\n const i = Number(msg.payload.i);\n\n outSems = cmdSetSems(\n i === 1 ? 'GREEN' : 'RED',\n i === 2 ? 'GREEN' : 'RED',\n i === 3 ? 'GREEN' : 'RED'\n );\n\n outTimers.push(cmdTimer(`CMD/START_G${i}`));\n setState(`DANDO_${i}`);\n } else if (topic.startsWith('EV/')) {\n requestPickIfPossible(outSched);\n }\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// - EV/Li_OFF: cerrar verde, STOP_Gi, START_Ti, CMD/SERVED, pasar a TRANS_i\n// - EV/Gi_TIMEOUT: bloqueo, STOP_Gi, rojo, volver a IDLE y re-arbitrar\n// =========================================================\nif (state.startsWith('DANDO_')) {\n const i = Number(state.split('_')[1]);\n\n if (topic === `EV/L${i}_OFF`) {\n outSems = cmdSetSems('RED','RED','RED');\n outTimers.push(cmdTimer(`CMD/STOP_G${i}`));\n outTimers.push(cmdTimer(`CMD/START_T${i}`));\n outSched.push(cmdSched('CMD/SERVED', { i }));\n setState(`TRANS_${i}`);\n updateStatus();\n return [outSems, outTimers, outSched];\n }\n\n if (topic === `EV/G${i}_TIMEOUT`) {\n outSems = cmdSetSems('RED','RED','RED');\n outTimers.push(cmdTimer(`CMD/STOP_G${i}`));\n\n // Bloqueo hasta que Li haga OFF\n let b = flow.get('blocked') ?? {1:false,2:false,3:false};\n b[i] = true;\n flow.set('blocked', b);\n\n setState('IDLE');\n requestPickIfPossible(outSched);\n updateStatus();\n return [outSems, outTimers.length ? outTimers : null, outSched.length ? outSched : null];\n }\n\n // En DANDO ignoramos otros eventos\n return [null, null, null];\n}\n\n// =========================================================\n// ESTADO: TRANS_i\n// - EV/Ti_TIMEOUT: volver a IDLE y re-arbitrar (si B_OCC sigue libre)\n// - EV/B_OCC_ON ya se maneja globalmente\n// =========================================================\nif (state.startsWith('TRANS_')) {\n const i = Number(state.split('_')[1]);\n\n if (topic === `EV/T${i}_TIMEOUT`) {\n if (!B_OCC) {\n setState('IDLE');\n requestPickIfPossible(outSched);\n updateStatus();\n return [null, null, outSched.length ? outSched : null];\n }\n }\n\n updateStatus();\n return [null, null, null];\n}\n\n// Fallback\nupdateStatus();\nreturn [null, null, null];\n\n\n// =========================\n// Status helper\n// =========================\nfunction updateStatus() {\n const st = flow.get('state') ?? 'IDLE';\n const B = flow.get('B_OCC') ? 1 : 0;\n\n const L1 = flow.get('L1') ? 1 : 0;\n const L2 = flow.get('L2') ? 1 : 0;\n const L3 = flow.get('L3') ? 1 : 0;\n\n const b = flow.get('blocked') ?? {1:false,2:false,3:false};\n const BLK = `${b[1]?1:0}${b[2]?1:0}${b[3]?1:0}`;\n\n const S1 = flow.get('S1') ?? 'RED';\n const S2 = flow.get('S2') ?? 'RED';\n const S3 = flow.get('S3') ?? 'RED';\n\n const dot = (c) => (c === 'GREEN' ? '🟢' : '🟥');\n\n const text =\n ` st:${st} B:${B} L:${L1}${L2}${L3} BLK:${BLK} S:${dot(S1)}${dot(S2)}${dot(S3)}`;\n\n node.status({ fill: B ? 'red' : 'green', shape: 'dot', text });\n}", "outputs": 3, "timeout": "", "noerr": 0, @@ -8976,8 +10330,8 @@ "links": [ "606dc6f9453f5e0d" ], - "x": 2195, - "y": 440, + "x": 2245, + "y": 380, "wires": [] }, { @@ -8990,8 +10344,8 @@ "links": [ "00ce5615a6247bec" ], - "x": 2195, - "y": 400, + "x": 2245, + "y": 340, "wires": [] }, { @@ -9004,8 +10358,8 @@ "links": [ "8da927c794b40546" ], - "x": 2195, - "y": 360, + "x": 2245, + "y": 300, "wires": [] }, { @@ -9017,11 +10371,13 @@ "links": [ "67e977418231666d" ], - "x": 1685, - "y": 580, + "x": 1675, + "y": 560, "wires": [ [ - "891cfa9c8d6e464d" + "891cfa9c8d6e464d", + "7988495e94435bb4", + "4011f30e8a41b199" ] ] }, @@ -9029,17 +10385,18 @@ "id": "891cfa9c8d6e464d", "type": "function", "z": "576f3807f61f5098", + "d": true, "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 context.set('idx', idx);\n context.set('cur', cur);\n context.set('served', served);\n context.set('competitive', competitive);\n};\n\nfunction pickNextActiveFrom(startIdx, d) {\n for (let k = 0; k < 3; k++) {\n const j = (startIdx + k) % 3;\n const i = order[j];\n if (d[i]) return { i, j };\n }\n return null;\n}\n\nif (t === 'CMD/SERVED') {\n const i = Number(msg.payload?.i);\n\n // Solo cuenta si estamos en competencia y el servido coincide con el carril actual\n if (competitive && cur === i) {\n served++;\n\n // Si agotó cuota, avanzamos idx al siguiente carril en order\n if (served >= (quota[i] ?? 1)) {\n idx = (idx + 1) % 3;\n cur = null;\n served = 0;\n }\n }\n\n save();\n return null;\n}\n\nif (t !== 'CMD/PICK_NEXT') return null;\n\nconst d = msg.payload?.demands || {\n 1: flow.get('L1') ? 1 : 0,\n 2: flow.get('L2') ? 1 : 0,\n 3: flow.get('L3') ? 1 : 0,\n};\n\nconst act = [1,2,3].filter(i => d[i]);\nconst compNow = act.length > 1;\n\n// Si cambia el modo (de no competitivo a competitivo), arrancamos desde el más prioritario\nif (!competitive && compNow) {\n idx = 0; // order[0] es el más prioritario (p.ej. 3)\n cur = null;\n served = 0;\n}\n\ncompetitive = compNow;\n\nif (!act.length) {\n cur = null; served = 0;\n save();\n return null;\n}\n\nif (!competitive) {\n // Solo uno esperando: dar paso directo, sin lotes\n cur = null; served = 0;\n save();\n return { topic:'RSP/PICK', payload:{ i: act[0] } };\n}\n\n// 1) Mantener el lote si aún tiene demanda y no agotó cuota\nif (cur && d[cur] && served < (quota[cur] ?? 1)) {\n save();\n return { topic:'RSP/PICK', payload:{ i: cur, served, quota: quota[cur] ?? 1 } };\n}\n\n// 2) Elegir siguiente carril activo empezando desde idx (rotación justa)\nconst pick = pickNextActiveFrom(idx, d);\nif (!pick) {\n // (no debería pasar si act.length>0)\n save();\n return { topic:'RSP/PICK', payload:{ i: act[0], fallback:true } };\n}\n\ncur = pick.i;\nidx = pick.j; // idx apunta al carril elegido\nserved = 0;\n\nsave();\nnode.warn(`[SCHED] pick=${cur} idx=${idx} served=${served}/${quota[cur] ?? 1} d=${JSON.stringify(d)}`);\nreturn { topic:'RSP/PICK', payload:{ i: cur, served, quota: quota[cur] ?? 1, wrr:true } };", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 1875, - "y": 580, + "x": 1835, + "y": 560, "wires": [ [ "15131de35b7d0425" @@ -9057,8 +10414,8 @@ "e7754eeae28a6256", "d95551b3e1091e71" ], - "x": 2065, - "y": 580, + "x": 2085, + "y": 560, "wires": [] }, { @@ -9071,7 +10428,7 @@ "798cfaee0987cdca" ], "x": 1685, - "y": 720, + "y": 760, "wires": [ [ "e4543a6e7a7f0ddb" @@ -9084,7 +10441,7 @@ "z": "576f3807f61f5098", "g": "fdde9120f66c6c06", "name": "Timer Manager", - "func": "/*\n Nodo Function: Timer Manager (T_i y G_i)\n\n OBJETIVO\n --------\n Gestionar temporizadores por software para 3 accesos:\n - T1, T2, T3 : timers de TRANSITO (tiempo t_i desde lazo hasta báscula)\n - G1, G2, G3 : timers de VERDE MÁXIMO (seguridad: camión no salió del lazo)\n\n Este nodo recibe comandos para arrancar/parar timers:\n - \"CMD/START_T1\", \"CMD/STOP_T1\"\n - \"CMD/START_T2\", \"CMD/STOP_T2\"\n - \"CMD/START_T3\", \"CMD/STOP_T3\"\n - \"CMD/START_G1\", \"CMD/STOP_G1\"\n - \"CMD/START_G2\", \"CMD/STOP_G2\"\n - \"CMD/START_G3\", \"CMD/STOP_G3\"\n\n Y emite eventos cuando un timer expira:\n - \"EV/T1_TIMEOUT\", \"EV/T2_TIMEOUT\", \"EV/T3_TIMEOUT\"\n - \"EV/G1_TIMEOUT\", \"EV/G2_TIMEOUT\", \"EV/G3_TIMEOUT\"\n\n SOBRE \"??\" (Nullish Coalescing)\n -------------------------------\n La expresión:\n a ?? b\n significa: \"usa a si NO es null ni undefined; si lo es, usa b\".\n\n Es ideal para parámetros configurables porque:\n - si el usuario NO ha puesto un valor (undefined), usas un valor por defecto\n - si el usuario lo pone a 0 (cero), se respeta (0 es válido)\n (A diferencia de \"||\", que reemplazaría 0 por el default.)\n\n VARIABLES (dónde se guardan)\n ----------------------------\n - flow.t1_ms, flow.t2_ms, flow.t3_ms : duración ms de T1..T3\n - flow.g1_ms, flow.g2_ms, flow.g3_ms : duración ms de G1..G3\n\n - context.timers : objeto que guarda los TimeoutID activos por key:\n {\n \"T1\": ,\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", + "func": "/*\n Nodo Function: Timer Manager (T_i y G_i)\n\n OBJETIVO\n --------\n Gestionar temporizadores por software para 3 accesos:\n - T1, T2, T3 : timers de TRANSITO (tiempo t_i desde lazo hasta báscula)\n - G1, G2, G3 : timers de VERDE MÁXIMO (seguridad: camión no salió del lazo)\n\n Este nodo recibe comandos para arrancar/parar timers:\n - \"CMD/START_T1\", \"CMD/STOP_T1\"\n - \"CMD/START_T2\", \"CMD/STOP_T2\"\n - \"CMD/START_T3\", \"CMD/STOP_T3\"\n - \"CMD/START_G1\", \"CMD/STOP_G1\"\n - \"CMD/START_G2\", \"CMD/STOP_G2\"\n - \"CMD/START_G3\", \"CMD/STOP_G3\"\n\n Y emite eventos cuando un timer expira:\n - \"EV/T1_TIMEOUT\", \"EV/T2_TIMEOUT\", \"EV/T3_TIMEOUT\"\n - \"EV/G1_TIMEOUT\", \"EV/G2_TIMEOUT\", \"EV/G3_TIMEOUT\"\n\n SOBRE \"??\" (Nullish Coalescing)\n -------------------------------\n La expresión:\n a ?? b\n significa: \"usa a si NO es null ni undefined; si lo es, usa b\".\n\n Es ideal para parámetros configurables porque:\n - si el usuario NO ha puesto un valor (undefined), usas un valor por defecto\n - si el usuario lo pone a 0 (cero), se respeta (0 es válido)\n (A diferencia de \"||\", que reemplazaría 0 por el default.)\n\n VARIABLES (dónde se guardan)\n ----------------------------\n - flow.t1_ms, flow.t2_ms, flow.t3_ms : duración ms de T1..T3\n - flow.g1_ms, flow.g2_ms, flow.g3_ms : duración ms de G1..G3\n\n - context.timers : objeto que guarda los TimeoutID activos por key:\n {\n \"T1\": ,\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// ---------- Debug ON/OFF ----------\nconst DEBUG = flow.get(\"debug\");\nconst dbg = (s) => { if (DEBUG) node.warn(s); };\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 CLR: flow.get(\"clr_ms\") ?? 10000\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*/\nlet m = topic.match(/^CMD\\/(START|STOP)_([TG])(\\d)$/);\nlet action, key;\n\nif (m) {\n action = m[1];\n key = m[2] + m[3]; // T1..T3 o G1..G3\n} else {\n m = topic.match(/^CMD\\/(START|STOP)_(CLR)$/);\n if (!m) return null;\n\n action = m[1];\n key = m[2]; // \"CLR\"\n}\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 //node.warn(`[TIMER] TIMEOUT ${k}`);\n dbg(`[TIMER] TIMEOUT = ${k} `);\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 = Number(msg.payload?.ms) > 0 ? Number(msg.payload.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 dbg(`[TIMER] Timer ${key} no arrancado: duración inválida (${ms})`);\n return null;\n }\n //node.warn(`[TIMER] START ${key} ms=${ms}`);\n dbg(`[TIMER] START = ${key} ms = ${ms}`);\n armTimer(key, ms);\n context.set(\"timers\", timers);\n return null;\n}\n\nreturn null;\n", "outputs": 1, "timeout": "", "noerr": 0, @@ -9092,7 +10449,7 @@ "finalize": "", "libs": [], "x": 1880, - "y": 720, + "y": 760, "wires": [ [ "2dc673e9f13e0f0b" @@ -9107,11 +10464,12 @@ "name": "EV BUS (desde TIMERS)", "mode": "link", "links": [ + "d95551b3e1091e71", "e7754eeae28a6256", - "d95551b3e1091e71" + "fab37c863ac68f06" ], "x": 2045, - "y": 720, + "y": 760, "wires": [] }, { @@ -9122,10 +10480,11 @@ "name": "CMD IN (SEMS)", "links": [ "b017ceea4285eb76", - "7c6babaed4914e41" + "7c6babaed4914e41", + "57655daf51fbf286" ], - "x": 1715, - "y": 920, + "x": 1945, + "y": 960, "wires": [ [ "49a1dfc2d343bdbb" @@ -9145,8 +10504,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 2140, - "y": 940, + "x": 2480, + "y": 1020, "wires": [ [ "e6e4d33e9470fbca", @@ -9161,12 +10520,12 @@ "z": "576f3807f61f5098", "g": "e928e581c722fe25", "name": "DBG Semaforos", - "active": true, + "active": false, "tosidebar": true, "complete": "true", "targetType": "full", - "x": 2370, - "y": 940, + "x": 2710, + "y": 900, "wires": [] }, { @@ -9182,8 +10541,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 2340, - "y": 880, + "x": 2680, + "y": 1020, "wires": [ [] ] @@ -9194,16 +10553,19 @@ "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, + "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});\nif (m === 'AUTO'){\n flow.set(\"S1\", \"RED\");flow.set(\"S2\", \"RED\");flow.set(\"S3\", \"RED\");\n return([{payload: {reason:'enter_auto'}, topic:'EV/FORCE_EVAL'}, { topic: 'RST/SEMS', payload: { reason:'enter_auto'}}])\n}\n// Opcional: al entrar en MANUAL forzar rojo por seguridad\nif (m === 'MANUAL') {\n return ([null, { 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": 2, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 1930, - "y": 880, + "x": 2250, + "y": 940, "wires": [ + [ + "5ecf6d948f96ea9a" + ], [ "49a1dfc2d343bdbb" ] @@ -9215,40 +10577,22 @@ "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", + "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);\nif (t === 'RST/SEMS'){// Ponemos todo en rojo al entrar en AUTO desde MANUAL\n msg.payload = forceNoGreen(msg.payload)\n msg.topic = 'CMD/SET_SEMS'\n return msg\n \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, + "x": 2250, + "y": 1020, "wires": [ [ "bc4418d271c32af0", - "4878484f33b6ea38" + "85025902ef5f0cde" ] ] }, - { - "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", @@ -9287,7 +10631,7 @@ "id": "8dec1861e45c773b", "type": "modbus-flex-getter", "z": "576f3807f61f5098", - "name": "", + "name": "lazos", "showStatusActivities": false, "showErrors": false, "showWarnings": true, @@ -9300,7 +10644,7 @@ "keepMsgProperties": false, "delayOnStart": false, "startDelayTime": "", - "x": 310, + "x": 290, "y": 460, "wires": [ [ @@ -9325,14 +10669,14 @@ "vt": "str" } ], - "repeat": "", + "repeat": "1", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "{ \"fc\": 2, \"unitid\": 1, \"address\": \"0\", \"quantity\": 3 }", "payloadType": "json", - "x": 130, + "x": 140, "y": 460, "wires": [ [ @@ -9346,7 +10690,7 @@ "z": "576f3807f61f5098", "name": "", "registerShowMax": 20, - "x": 510, + "x": 530, "y": 540, "wires": [] }, @@ -9355,7 +10699,7 @@ "type": "debug", "z": "576f3807f61f5098", "name": "debug 31", - "active": true, + "active": false, "tosidebar": true, "console": false, "tostatus": false, @@ -9381,8 +10725,8 @@ "keepMsgProperties": false, "delayOnStart": false, "startDelayTime": "", - "x": 2380, - "y": 1000, + "x": 2720, + "y": 1100, "wires": [ [], [ @@ -9412,8 +10756,8 @@ "topic": "", "payload": "{ \"value\":[false,true,true], \"fc\": 15, \"unitid\": 1, \"address\": 16, \"quantity\": 3}", "payloadType": "json", - "x": 2190, - "y": 1000, + "x": 2500, + "y": 1100, "wires": [ [ "d8112ca72e0a342d" @@ -9442,9 +10786,9 @@ "showMsgUib": false, "title": "", "descr": "", - "editurl": "vscode://file/home/trypton/.node-red/projects/raspipeso20_10_25/uibuilder/semaforos/?windowId=_blank", - "x": 3050, - "y": 500, + "editurl": "vscode://vscode-remote/ssh-remote+192.168.11.6/home/trypton/.node-red/projects/raspipeso20_10_25/uibuilder/semaforos/?windowId=_blank", + "x": 3450, + "y": 980, "wires": [ [ "4333f08dfc27e390", @@ -9475,8 +10819,8 @@ "topic": "", "payload": "{\"payload\":{\"pantalla\":\"menu-principal\"}}", "payloadType": "json", - "x": 2860, - "y": 380, + "x": 3260, + "y": 860, "wires": [ [ "c42bdd023acc8dc3" @@ -9496,8 +10840,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 3270, - "y": 480, + "x": 3670, + "y": 960, "wires": [ [ "1e52a0895d377969", @@ -9542,8 +10886,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3250, - "y": 380, + "x": 3650, + "y": 860, "wires": [] }, { @@ -9554,8 +10898,8 @@ "name": "set semaforo", "mode": "link", "links": [], - "x": 3485, - "y": 500, + "x": 3885, + "y": 980, "wires": [] }, { @@ -9568,8 +10912,8 @@ "links": [ "764c5454d21fc52b" ], - "x": 3435, - "y": 420, + "x": 3835, + "y": 900, "wires": [] }, { @@ -9586,8 +10930,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3470, - "y": 340, + "x": 3870, + "y": 820, "wires": [] }, { @@ -9600,8 +10944,8 @@ "links": [ "53019ab29a07f8f5" ], - "x": 3485, - "y": 540, + "x": 3885, + "y": 1020, "wires": [] }, { @@ -9612,8 +10956,8 @@ "name": "reiniciar", "mode": "link", "links": [], - "x": 3485, - "y": 580, + "x": 3885, + "y": 1060, "wires": [] }, { @@ -9629,8 +10973,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 2830, - "y": 500, + "x": 3230, + "y": 980, "wires": [ [ "c42bdd023acc8dc3", @@ -9652,8 +10996,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3050, - "y": 400, + "x": 3450, + "y": 880, "wires": [] }, { @@ -9664,8 +11008,8 @@ "name": "snapshot_rqst", "mode": "link", "links": [], - "x": 3485, - "y": 620, + "x": 3885, + "y": 1100, "wires": [] }, { @@ -9682,8 +11026,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3530, - "y": 660, + "x": 3930, + "y": 1140, "wires": [] }, { @@ -9693,8 +11037,8 @@ "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, + "x": 3360, + "y": 1120, "wires": [] }, { @@ -9705,8 +11049,8 @@ "name": "activa-barrera", "mode": "link", "links": [], - "x": 3485, - "y": 700, + "x": 3885, + "y": 1180, "wires": [] }, { @@ -9723,8 +11067,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3530, - "y": 740, + "x": 3930, + "y": 1220, "wires": [] }, { @@ -9741,8 +11085,8 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 3590, - "y": 540, + "x": 3990, + "y": 1020, "wires": [] }, { @@ -9757,8 +11101,8 @@ "createDir": true, "overwriteFile": "true", "encoding": "none", - "x": 3570, - "y": 460, + "x": 3970, + "y": 940, "wires": [ [] ] @@ -9770,8 +11114,8 @@ "g": "350484a2e764ef67", "name": "link in 1", "links": [], - "x": 2725, - "y": 540, + "x": 3125, + "y": 1020, "wires": [ [ "49e2b8c24d0ee245" @@ -9787,8 +11131,8 @@ "links": [ "1e52a0895d377969" ], - "x": 2725, - "y": 460, + "x": 3125, + "y": 940, "wires": [ [ "49e2b8c24d0ee245" @@ -9817,8 +11161,8 @@ "topic": "", "payload": "1", "payloadType": "num", - "x": 2790, - "y": 880, + "x": 3250, + "y": 580, "wires": [ [ "6b89fc341280ad29" @@ -9838,8 +11182,8 @@ "sendError": false, "encoding": "none", "allProps": false, - "x": 2960, - "y": 880, + "x": 3420, + "y": 580, "wires": [ [ "e02d2079825430db" @@ -9852,15 +11196,15 @@ "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", + "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\nread = config.general.tipo_prioridad;\nconfig_general.push({ \"parametro\": \"tipo_prioridad\", \"valor\": read })\nflow.set(\"policy_mode\", read)\n\nread = config.general.relief_total;\nconfig_general.push({ \"parametro\": \"relief_total\", \"valor\": read })\nflow.set(\"relief_total\", read)\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_post_bascula\nconfig_tiempos.push({ \"parametro\": \"t_post_bascula\", \"valor\": read })\nflow.set(\"clr_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, + "x": 3610, + "y": 580, "wires": [ [ "7c6babaed4914e41" @@ -9879,8 +11223,8 @@ "links": [ "6a6d080c74f2da43" ], - "x": 2805, - "y": 940, + "x": 3265, + "y": 640, "wires": [ [ "6b89fc341280ad29" @@ -9897,8 +11241,8 @@ "links": [ "8da927c794b40546" ], - "x": 3285, - "y": 880, + "x": 3745, + "y": 580, "wires": [] }, { @@ -9937,8 +11281,8 @@ "g": "e928e581c722fe25", "name": "", "registerShowMax": 20, - "x": 2490, - "y": 1040, + "x": 2760, + "y": 1180, "wires": [] }, { @@ -9948,10 +11292,11 @@ "g": "e928e581c722fe25", "name": "modo", "links": [ - "4232fc1ec89537af" + "4232fc1ec89537af", + "d24eab5d07c0e51d" ], - "x": 1715, - "y": 880, + "x": 1945, + "y": 860, "wires": [ [ "06c0e45f61e0d364" @@ -9968,8 +11313,8 @@ "links": [ "217697ee4e884321" ], - "x": 3285, - "y": 940, + "x": 3745, + "y": 640, "wires": [] }, { @@ -10022,7 +11367,7 @@ "type": "debug", "z": "576f3807f61f5098", "name": "debug 45", - "active": true, + "active": false, "tosidebar": true, "console": false, "tostatus": false, @@ -10039,7 +11384,7 @@ "type": "debug", "z": "576f3807f61f5098", "name": "debug 46", - "active": true, + "active": false, "tosidebar": true, "console": false, "tostatus": false, @@ -10056,7 +11401,7 @@ "type": "debug", "z": "576f3807f61f5098", "name": "debug 47", - "active": true, + "active": false, "tosidebar": true, "console": false, "tostatus": false, @@ -10107,7 +11452,7 @@ "type": "debug", "z": "576f3807f61f5098", "name": "debug 49", - "active": true, + "active": false, "tosidebar": true, "console": false, "tostatus": false, @@ -10123,10 +11468,560 @@ "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", + "name": "Gestion de Semaforos V1.5", + "info": "*Version 1.5* Esta version tiene la particularidad de que dispone de tres modos de gestion de la prioridad\n1.TOUR. Supongamos que tenemos los tres lazos ocupados. Este modo se refiere a que si en esa situacion el sistema\nle va a dar paso al Lazo mas prioritario las veces que de forma continua sigan apareciendo vehiculos en dicho lazo \nsi no ha llegado al maximo numero de vehiculo continuos permitidos. En caso de que le queden aun vehiculos en la cuenta pero ya no tiene mas vehiculos presentes entonces se le da paso al siguiente lazo menos prioritario el numero maximo de vehiculos\nque tenga asignados y si sucede lo mismo es decir no llegan mas vehiculo entonces pasamos al siguiente menos prioritario\ny cuando no haya mas vehiculos en ese lazo en ese momento inicializamos o recargamos todos los contadores de \nvehiculos permitidos de forma continua. En el caso descrito si estamos dando paso al segundo menos prioritario y ahora llega\nun vehiculo al mas prioritario entonces tiene que esperar hasta que el sistema termine la vuelta completa\nEl nombre TOUR tiene que ver con el sistema que se hace de dar una vuelta completa\nantes de actualizar el contador de vehiculos.\n\n2. EPOCH. Partamos del mismo inicio es decir todos los lazos ocupados. Este sistema se basa en una cantidad de creditos\nque posee cada lazo (creditos en este caso son el numero maximo de vehiculos que pueden pasar manteniendo la prioridad )\nEl sistema permite que pasen vehiculos en el mas prioritario siempre que tenga creditos, cada vez que pasa un vehiculo le resta\nun credito. Si no hay vehiculos el sistema pasa al siguiente menos prioritario y deja pasar vehiculo pero si aparece\nahora un vehiculo en el lazo anterior le da paso puesto que es mas prioritario y aun tiene creditos. Si no tuviera creditos\nse continuaría con el lazo menos prioritario. Los creditos se recargan cuando vuelva a haber una competencia entre dos o mas lazos \nque han quedado sin creditos. El nombre EPOCH se refiere a que los creditos se renuevan al cabo de un ciclo o epoca que es el momento en todos gastaron sus creditos pero necesitan tomar decision porque hay una competencia (dos o mas lazos ocupados) \n\n3. EPOCH_RELIEF Partamos siempre del minmo inicio. Ahora ademas del numero de creditos tenemos un numero llamado relief_total\nque indica cuantos vehiculos dejo pasar de un lazo cuando el mas prioritario agoto sus creditos. Supongamos relief_total = 1. Quiere decir que en el momento en que el lazo prioritario termina sus creditos deja pasar un vehiculo del siguiente menos prioritario y recarga los creditos del mas prioritario de manera que cuando el menos prioritario agote sus creditos dejara pasar uno del siguiente\nmenos prioritario y recargara sus creditos. El nombre RELIEF (alivio) viene porque es como un aliviadero cuando se llena la cantidad\nde vehiculos que podian pasar y el relief_total permite volver a rellenar el credito.\n\nEsta version ademas dispone de una pagina web http://IP:1880/control_manual que permita gestionar manualmente los semaforos de los\nlazos y ademas permite visualizar los estados mas importantes del sistema de gestion de semaforos (estado de lazos con nombre asignado, lazos bloqueados, modo de gestion utilizado, estados intermedio de transito)\n\n\nEsta 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": [] + }, + { + "id": "f2e46b18a6973969", + "type": "function", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "rx comandos", + "func": "if(msg.payload.comando === \"MANUAL\"){\n node.send([{payload:\"MANUAL\"}, null])\n}\nelse if(msg.payload.comando === \"AUTO\"){\n node.send([{payload:\"AUTO\"}, null])\n}\nelse if(msg.payload.comando === \"ON/OFF\")\n if(msg.payload.hasOwnProperty(\"control_semaforo\")){\n // Entrada esperada: msg.payload.control_semaforo = \"1,0,0\" (o [1,0,0])\n const raw = msg.payload?.control_semaforo; // evita que al cargar raw de un error en caso que msg.payload este null o undefined\n\n // 1) Normalizar a array de 3 números (0/1)\n let bits;\n if (Array.isArray(raw)) {\n bits = raw;\n } else if (typeof raw === \"string\") {\n bits = raw.split(\",\").map(s => Number(s.trim()));\n } else {\n bits = [];\n }\n bits = [0, 1, 2].map(i => (bits[i] === 1 ? 1 : 0)); // fuerza 3 valores y solo 0/1\n\n // 2) Validar: solo uno en verde\n let greens = 0\n for (let i = 0; i < 3; i++) greens += bits[i];\n if (greens > 1) {\n node.warn(`control_semaforo inválido (${String(raw)}): deben venir exactamente 1 verde. Pongo todos en ROJO.`);\n bits = [0, 0, 0];\n }\n\n // 3) Construir salida\n msg.payload = {\n S1: bits[0] ? \"GREEN\" : \"RED\",\n S2: bits[1] ? \"GREEN\" : \"RED\",\n S3: bits[2] ? \"GREEN\" : \"RED\",\n };\n\n return ([null, { payload: msg.payload, topic: \"MAN/SET_SEMS\"}]);\n\n }\n else {\n node.warn(\"NO EXISTE ORDEN PARA SEMAFOROS\")\n }\nreturn null", + "outputs": 2, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1980, + "y": 1100, + "wires": [ + [ + "06c0e45f61e0d364" + ], + [ + "49a1dfc2d343bdbb" + ] + ] + }, + { + "id": "eb17917cdc2ad86e", + "type": "link in", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "comando semaforos", + "links": [ + "4adc1ea54b29ee78" + ], + "x": 1755, + "y": 1180, + "wires": [ + [ + "f2e46b18a6973969", + "2daec3c01c1f7a1e" + ] + ] + }, + { + "id": "3d15de7453ed0c33", + "type": "uibuilder", + "z": "576f3807f61f5098", + "g": "3a42dfd538259345", + "name": "", + "topic": "", + "url": "control_manual", + "okToGo": true, + "fwdInMessages": false, + "allowScripts": false, + "allowStyles": false, + "copyIndex": true, + "templateFolder": "blank", + "extTemplate": "", + "showfolder": false, + "reload": false, + "sourceFolder": "src", + "deployedVersion": "7.4.3", + "showMsgUib": false, + "title": "", + "descr": "", + "editurl": "vscode://file/home/trypton/.node-red/projects/raspipeso20_10_25/uibuilder/control_manual/?windowId=_blank", + "x": 2310, + "y": 1400, + "wires": [ + [ + "1eb64e619b7e9a05" + ], + [] + ] + }, + { + "id": "1eb64e619b7e9a05", + "type": "function", + "z": "576f3807f61f5098", + "g": "3a42dfd538259345", + "name": "Peticiones UI", + "func": "\nconst topic = msg.topic || \"\";\nconst payload = msg.payload;\n\n// REQUEST_ESTADO\nif (topic === \"REQUEST_ESTADO\") {\n const estado = {\n nombre_S1: flow.get(\"nombre_lazo1\") || \"S1\",\n nombre_S2: flow.get(\"nombre_lazo2\") || \"S2\",\n nombre_S3: flow.get(\"nombre_lazo3\") || \"S3\",\n S1: flow.get(\"S1\") || \"RED\",\n S2: flow.get(\"S2\") || \"RED\",\n S3: flow.get(\"S3\") || \"RED\",\n B_OCC: flow.get(\"B_OCC\") ? 1 : 0,\n mode: flow.get(\"mode\") || \"AUTO\"\n };\n\n return [null, null, { payload: estado }];\n}\n\n//TOGGLE_SEMAFORO\nif (topic === \"TOGGLE_SEMAFORO\") {\n const id = payload; // \"S1\", \"S2\", o \"S3\"\n\n //Leer estado actual del semáforo clickeado\n const estadoActual = flow.get(id) || \"RED\";\n \n let comando;\n \n if (estadoActual === \"GREEN\") {\n // Si ya está verde → apagar (todos en rojo)\n comando = {\n S1: \"RED\",\n S2: \"RED\",\n S3: \"RED\"\n };\n } else {\n // Si está rojo → encender este y apagar los demás\n comando = {\n S1: id === \"S1\" ? \"GREEN\" : \"RED\",\n S2: id === \"S2\" ? \"GREEN\" : \"RED\",\n S3: id === \"S3\" ? \"GREEN\" : \"RED\"\n };\n }\n\n return [null,{topic: \"MAN/SET_SEMS\", payload: comando, id:id},null];\n}\n\n//CAMBIAR_MODO\nif (topic === \"CAMBIAR_MODO\") {\n const m = String(payload || \"\").toUpperCase();\n if (m !== \"AUTO\" && m !== \"MANUAL\") return null;\n\n flow.set(\"mode\", m);\n\n return [{ payload: m }, null, null];\n}\n\nreturn null;", + "outputs": 3, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2550, + "y": 1380, + "wires": [ + [ + "d24eab5d07c0e51d" + ], + [ + "57655daf51fbf286" + ], + [ + "3d15de7453ed0c33" + ] + ] + }, + { + "id": "731bbc2ad85b5397", + "type": "function", + "z": "576f3807f61f5098", + "g": "3a42dfd538259345", + "name": "Actualiza estado UI", + "func": "\n// Enviar estado cuando hay cambios\nconst estado = {\n nombre_S1: flow.get(\"nombre_lazo1\") || \"S1\",\n nombre_S2: flow.get(\"nombre_lazo2\") || \"S2\",\n nombre_S3: flow.get(\"nombre_lazo3\") || \"S3\",\n S1: flow.get(\"S1\") || \"RED\",\n S2: flow.get(\"S2\") || \"RED\",\n S3: flow.get(\"S3\") || \"RED\",\n B_OCC: flow.get(\"B_OCC\") ? 1 : 0,\n mode: flow.get(\"mode\") || \"AUTO\",\n lazo1: flow.get(\"L1\") || 0,\n lazo2: flow.get(\"L2\") || 0,\n lazo3: flow.get(\"L3\") || 0,\n state: flow.get(\"state\") || \"IDLE\",\n policy_mode: flow.get(\"policy_mode\"),\n blocked: flow.get(\"blocked\"),\n priority: flow.get(\"order\"),\n numero_max_vehiculos: [flow.get(\"N1\"), flow.get(\"N2\"), flow.get(\"N3\")]\n};\n\nreturn { payload: estado };", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 2090, + "y": 1400, + "wires": [ + [ + "3d15de7453ed0c33", + "929228d1dc38672d" + ] + ] + }, + { + "id": "fab37c863ac68f06", + "type": "link in", + "z": "576f3807f61f5098", + "g": "3a42dfd538259345", + "name": "link in 3", + "links": [ + "74ea72530de212a4", + "85025902ef5f0cde", + "2dc673e9f13e0f0b", + "c4159a812594fc62" + ], + "x": 1935, + "y": 1400, + "wires": [ + [ + "731bbc2ad85b5397" + ] + ] + }, + { + "id": "2daec3c01c1f7a1e", + "type": "debug", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "debug 50", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 1930, + "y": 1200, + "wires": [] + }, + { + "id": "d24eab5d07c0e51d", + "type": "link out", + "z": "576f3807f61f5098", + "g": "3a42dfd538259345", + "name": "link out 6", + "mode": "link", + "links": [ + "217697ee4e884321" + ], + "x": 2785, + "y": 1340, + "wires": [] + }, + { + "id": "57655daf51fbf286", + "type": "link out", + "z": "576f3807f61f5098", + "g": "3a42dfd538259345", + "name": "link out 7", + "mode": "link", + "links": [ + "8da927c794b40546" + ], + "x": 2755, + "y": 1400, + "wires": [] + }, + { + "id": "85025902ef5f0cde", + "type": "link out", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "link out 8", + "mode": "link", + "links": [ + "fab37c863ac68f06" + ], + "x": 2325, + "y": 1100, + "wires": [] + }, + { + "id": "833d2a4708575d0e", + "type": "function", + "z": "576f3807f61f5098", + "g": "6457918bac63ba59", + "name": "FSM Dispatcher mode", + "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// FSM Dispatcher (SIMPLIFICADO)\n// =============================\nconst topic = msg.topic || '';\nconst now = Date.now();\n\n// Chequea si queremos hacer un reset del sistema\n//---------------------------------------------------------------------------------------------------------------------\nif (msg.topic === 'CMD/RESET_ALL') {\n flow.set('state', 'IDLE');\n flow.set('blocked', { 1: false, 2: false, 3: false });\n // opcional:\n // flow.set('B_OCC', 0);\n\n // semáforos a rojo\n const outSems = cmdSetSems('RED', 'RED', 'RED');\n\n // parar timers (ajusta a los que existan en tu sistema)\n const outTimers = [\n cmdTimer('CMD/STOP_CLR'),\n cmdTimer('CMD/STOP_G1'), cmdTimer('CMD/STOP_G2'), cmdTimer('CMD/STOP_G3'),\n cmdTimer('CMD/STOP_T1'), cmdTimer('CMD/STOP_T2'), cmdTimer('CMD/STOP_T3'),\n ];\n\n updateStatus();\n return [outSems, outTimers, [{ topic: 'CMD/RESET_ALL', payload: { ts: now } }]]; // opcional: propagar reset\n}\n//----------------------------------------------------------------------------------------------------------------------\n\n\n\n// ---------- Debug ON/OFF ----------\nconst DEBUG = flow.get(\"debug\");\nconst dbg = (s) => { if (DEBUG) node.warn(s); };\n\n// ---------- Context helpers ----------\nfunction setState(st) { flow.set('state', st); }\nfunction getState() { return flow.get('state') ?? 'IDLE'; }\n\n// ---------- Blocked management ----------\nlet blocked = flow.get('blocked') ?? { 1:false, 2:false, 3:false };\n\n// Si un lazo pasa a OFF, desbloqueamos ese acceso (resetea fallo por timeout de verde)\nif (topic === 'EV/L1_OFF') blocked[1] = false;\nif (topic === 'EV/L2_OFF') blocked[2] = false;\nif (topic === 'EV/L3_OFF') blocked[3] = false;\nflow.set('blocked', blocked);\n\n// ---------- Demand snapshot ----------\nfunction getDemands() {\n const b = flow.get('blocked') ?? { 1:false,2:false,3:false };\n return {\n 1: (flow.get('L1') ? 1 : 0) && !b[1] ? 1 : 0,\n 2: (flow.get('L2') ? 1 : 0) && !b[2] ? 1 : 0,\n 3: (flow.get('L3') ? 1 : 0) && !b[3] ? 1 : 0\n };\n}\nfunction anyDemand(d) { return (d[1] || d[2] || d[3]) ? 1 : 0; }\n\n// ---------- Command builders ----------\nfunction cmdSetSems(s1, s2, s3) {\n if (flow.get('mode') === \"AUTO\"){\n flow.set('S1', s1); flow.set('S2', s2); flow.set('S3', s3);\n return { topic:'CMD/SET_SEMS', payload:{ S1:s1, S2:s2, S3:s3, ts: now } };\n }\n else{\n return null\n }\n}\nfunction cmdTimer(tpc, payload = {}) {\n return { topic: tpc, payload: { ...payload, ts: now } };\n}\nfunction cmdSched(tpc, payload = {}) {\n return { topic: tpc, payload: { ...payload, ts: now } };\n}\nfunction requestPickIfPossible(outSchedArr) {\n const mode = flow.get(\"policy_mode\") || \"TOUR\";\n const B = flow.get('B_OCC') ? 1 : 0;\n const d = getDemands();\n if (!B && anyDemand(d)) {\n dbg(`[PICK_REQ] st=${getState()} B=${B} demands=${JSON.stringify(d)} order=${JSON.stringify(flow.get('order'))}`);\n outSchedArr.push(cmdSched('CMD/PICK_NEXT', { demands: d, mode }));\n }\n}\n\n// ---------- Outputs ----------\nlet outSems = null;\nlet outTimers = [];\nlet outSched = [];\n\n// ---------- Read current state + B_OCC ----------\nlet state = getState();\nlet B_OCC = flow.get('B_OCC') ? 1 : 0;\ndbg(`[FSM] IN topic=${topic} state=${state} B=${B_OCC}`);\n\n// =========================================================\n// MODO MANUAL\n// En manual ignoramos eventos automáticos de arbitraje\n// pero permitimos eventos de seguridad / servicio\n// =========================================================\nconst mode = flow.get('mode') ?? 'AUTO';\n\nif (mode !== 'AUTO') {\n const allowedInManual =\n topic === 'CMD/RESET_ALL' ||\n topic === 'EV/B_OCC_ON' ||\n topic === 'EV/B_OCC_OFF' ||\n topic === 'EV/CLR_TIMEOUT' ||\n topic === 'EV/G1_TIMEOUT' ||\n topic === 'EV/G2_TIMEOUT' ||\n topic === 'EV/G3_TIMEOUT' ||\n topic === 'EV/T1_TIMEOUT' ||\n topic === 'EV/T2_TIMEOUT' ||\n topic === 'EV/T3_TIMEOUT';\n\n if (!allowedInManual) {\n updateStatus();\n return [null, null, null];\n }\n}\n// =====================================================================\n\n// ---------- Keep B_OCC updated from events ----------\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// =========================================================\n// EVENTO GLOBAL DE SEGURIDAD (SIMPLE Y LIMPIO)\n// Si se ocupa báscula: OCC SIEMPRE (y cancelamos CLR si estaba)\n// =========================================================\nif (topic === 'EV/B_OCC_ON') {\n // Si estabas en CLEAR, para el timer de despeje para que no dispare tarde\n outTimers.push(cmdTimer('CMD/STOP_CLR'));\n outSems = cmdSetSems('RED', 'RED', 'RED');\n setState('OCC');\n updateStatus();\n return [outSems, outTimers.length ? outTimers : null, null];\n}\n\n// =========================================================\n// ESTADO: OCC\n// - B_OCC=1: todo rojo\n// - EV/B_OCC_OFF: ir a CLEAR y arrancar CLR (delay de despeje)\n// =========================================================\nif (state === 'OCC') {\n if (topic === 'EV/B_OCC_OFF') {\n outSems = cmdSetSems('RED', 'RED', 'RED');\n\n const ms = flow.get('clr_ms') ?? 10000;\n outTimers.push(cmdTimer('CMD/START_CLR', { ms }));\n\n setState('CLEAR');\n updateStatus();\n return [outSems, outTimers.length ? outTimers : null, null];\n }\n\n // En OCC ignoramos lo demás\n outSems = cmdSetSems('RED', 'RED', 'RED');\n updateStatus();\n return [outSems, null, null];\n}\n\n// =========================================================\n// ESTADO: CLEAR\n// - Báscula libre pero esperamos despeje\n// - EV/CLR_TIMEOUT: pasar a IDLE y pedir pick\n// - EV/B_OCC_ON ya se manejó globalmente arriba\n// =========================================================\nif (state === 'CLEAR') {\n if (topic === 'EV/CLR_TIMEOUT') {\n setState('IDLE');\n requestPickIfPossible(outSched);\n updateStatus();\n return [null, null, outSched.length ? outSched : null];\n }\n // Durante CLEAR no hacemos arbitraje\n updateStatus();\n return [null, null, null];\n}\n\n// =========================================================\n// ESTADO: IDLE\n// - si llega EV/*: pedir pick si hay demanda\n// - si llega RSP/PICK: poner verde y arrancar Gi, pasar a DANDO_i\n// =========================================================\nif (state === 'IDLE') {\n if (!B_OCC) {\n if (topic === 'RSP/PICK' && msg.payload?.i) {\n dbg(`[PICK_RSP] st=${getState()} payload=${JSON.stringify(msg.payload)} demands_now=${JSON.stringify(getDemands())} order=${JSON.stringify(flow.get('order'))}`);\n const i = Number(msg.payload.i);\n\n outSems = cmdSetSems(\n i === 1 ? 'GREEN' : 'RED',\n i === 2 ? 'GREEN' : 'RED',\n i === 3 ? 'GREEN' : 'RED'\n );\n\n outTimers.push(cmdTimer(`CMD/START_G${i}`));\n setState(`DANDO_${i}`);\n } else if (topic.startsWith('EV/')) {\n requestPickIfPossible(outSched);\n }\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// - EV/Li_OFF: cerrar verde, STOP_Gi, START_Ti, CMD/SERVED, pasar a TRANS_i\n// - EV/Gi_TIMEOUT: bloqueo, STOP_Gi, rojo, volver a IDLE y re-arbitrar\n// =========================================================\nif (state.startsWith('DANDO_')) {\n const i = Number(state.split('_')[1]);\n\n if (topic === `EV/L${i}_OFF`) {\n const mode = flow.get(\"policy_mode\") || \"TOUR\";\n outSems = cmdSetSems('RED','RED','RED');\n outTimers.push(cmdTimer(`CMD/STOP_G${i}`));\n outTimers.push(cmdTimer(`CMD/START_T${i}`));\n outSched.push(cmdSched('CMD/SERVED', { i, mode }));\n outSched.push(cmdSched('CMD/REFRESH_STATUS', { reason: `L${i}_OFF` }));\n setState(`TRANS_${i}`);\n updateStatus();\n return [outSems, outTimers, outSched];\n }\n\n if (topic === `EV/G${i}_TIMEOUT`) {\n outSems = cmdSetSems('RED','RED','RED');\n outTimers.push(cmdTimer(`CMD/STOP_G${i}`));\n\n // Bloqueo hasta que Li haga OFF\n let b = flow.get('blocked') ?? {1:false,2:false,3:false};\n b[i] = true;\n flow.set('blocked', b);\n\n setState('IDLE');\n requestPickIfPossible(outSched);\n updateStatus();\n return [outSems, outTimers.length ? outTimers : null, outSched.length ? outSched : null];\n }\n\n // En DANDO ignoramos otros eventos\n return [null, null, null];\n}\n\n// =========================================================\n// ESTADO: TRANS_i\n// - EV/Ti_TIMEOUT: volver a IDLE y re-arbitrar (si B_OCC sigue libre)\n// - EV/B_OCC_ON ya se maneja globalmente\n// =========================================================\nif (state.startsWith('TRANS_')) {\n const i = Number(state.split('_')[1]);\n\n if (topic === `EV/T${i}_TIMEOUT`) {\n if (!B_OCC) {\n setState('IDLE');\n requestPickIfPossible(outSched);\n updateStatus();\n return [null, null, outSched.length ? outSched : null];\n }\n }\n\n updateStatus();\n return [null, null, null];\n}\n\n// Fallback\nupdateStatus();\nreturn [null, null, null];\n\n\n// =========================\n// Status helper\n// =========================\nfunction updateStatus() {\n const st = flow.get('state') ?? 'IDLE';\n const B = flow.get('B_OCC') ? 1 : 0;\n\n const L1 = flow.get('L1') ? 1 : 0;\n const L2 = flow.get('L2') ? 1 : 0;\n const L3 = flow.get('L3') ? 1 : 0;\n\n const b = flow.get('blocked') ?? {1:false,2:false,3:false};\n const BLK = `${b[1]?1:0}${b[2]?1:0}${b[3]?1:0}`;\n\n const S1 = flow.get('S1') ?? 'RED';\n const S2 = flow.get('S2') ?? 'RED';\n const S3 = flow.get('S3') ?? 'RED';\n\n const dot = (c) => (c === 'GREEN' ? '🟢' : '🟥');\n\n const text =\n ` st:${st} B:${B} L:${L1}${L2}${L3} BLK:${BLK} S:${dot(S1)}${dot(S2)}${dot(S3)} Mode: ${flow.get(\"policy_mode\")}`;\n\n node.status({ fill: B ? 'red' : 'green', shape: 'dot', text });\n}", + "outputs": 3, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1940, + "y": 360, + "wires": [ + [ + "b017ceea4285eb76" + ], + [ + "798cfaee0987cdca" + ], + [ + "67e977418231666d" + ] + ] + }, + { + "id": "7988495e94435bb4", + "type": "function", + "z": "576f3807f61f5098", + "d": true, + "g": "e51291e8f6dc7282", + "name": "Scheduler WRR mode", + "func": "// Chequea si hay que hacer reset del sistema\n\nconst t = msg.topic || '';\nconst order = flow.get('order') ?? [3, 2, 1];\n\n//-------------------------------------------------------------------------------------------------\nif (t === 'CMD/RESET_ALL') {\n context.set('idx', 0);\n context.set('cur', null);\n context.set('served', 0);\n context.set('competitive', false);\n\n // Si tienes modo EPOCH (créditos)\n context.set('epoch_credits', null);\n context.set('policy_mode_last', null);\n\n node.status({ fill: 'grey', shape: 'ring', text: 'RESET_ALL' });\n return null;\n}\n//---------------------------------------------------------------------------------------------------\n\nif(t === 'CMD/REFRESH_STATUS') {\n updateStatus()\n return null\n}\n\n\n// ---------- Debug ON/OFF ----------\nconst DEBUG = flow.get(\"debug\");\nconst dbg = (s) => { if (DEBUG) node.warn(s); };\n\n// =======================\n// CONFIG / PARAMETROS\n// =======================\nconst quota = { // pesos / cuotas por lazo\n 1: (flow.get('N1') ?? 2),\n 2: (flow.get('N2') ?? 5),\n 3: (flow.get('N3') ?? 3),\n};\n\n// Flag de política: \"TOUR\" (actual) o \"EPOCH\" (nuevo)\nconst mode = (msg.payload?.mode || flow.get(\"policy_mode\") || \"TOUR\").toUpperCase();\n\n// =======================\n// HELPERS\n// =======================\nfunction pickNextActiveFrom(startIdx, d) {\n for (let k = 0; k < 3; k++) {\n const j = (startIdx + k) % 3;\n const i = order[j];\n if (d[i]) return { i, j };\n }\n return null;\n}\n\nfunction getDemands() {\n return msg.payload?.demands || {\n 1: flow.get('L1') ? 1 : 0,\n 2: flow.get('L2') ? 1 : 0,\n 3: flow.get('L3') ? 1 : 0,\n };\n}\n\n// ============================================================\n// MODO TOUR (tu lógica actual) - estado en context (SIN CAMBIOS)\n// ============================================================\nlet idx = context.get('idx') ?? 0; // índice dentro de order (0..2)\nlet cur = context.get('cur') ?? null; // carril actual del lote (1/2/3 o null)\nlet served = context.get('served') ?? 0; // servidos dentro del lote actual\nlet competitive = context.get('competitive') ?? false;\n\nconst saveTour = () => {\n context.set('idx', idx);\n context.set('cur', cur);\n context.set('served', served);\n context.set('competitive', competitive);\n};\n\n// ============================================================\n// MODO EPOCH (créditos por lazo + reset por época)\n// estado en context separado\n// ============================================================\nlet credits = context.get('epoch_credits') ?? null; // {1:x,2:y,3:z} o null para forzar recarga\nlet lastMode = context.get('policy_mode_last') ?? null;\n\nfunction saveEpoch() {\n context.set('epoch_credits', credits);\n}\n\nfunction ensureCreditsLoaded(d) {\n // Si no hay créditos o si algún carril no tiene clave, (re)cargamos\n if (!credits || credits[1] == null || credits[2] == null || credits[3] == null) {\n credits = { 1: quota[1] ?? 1, 2: quota[2] ?? 1, 3: quota[3] ?? 1 };\n return;\n }\n // Opcional: si cambian quotas en caliente, podrías normalizar aquí (lo dejo conservador)\n}\n\nfunction anyDemand(d) {\n return !!(d[1] || d[2] || d[3]);\n}\n\nfunction pickEpoch(d) {\n // 1) candidatos: demanda y crédito > 0, en orden de prioridad (order)\n for (const i of order) {\n if (d[i] && (credits[i] ?? 0) > 0) return i;\n }\n return null;\n}\n\nfunction resetEpoch() {\n credits = { 1: quota[1] ?? 1, 2: quota[2] ?? 1, 3: quota[3] ?? 1 };\n}\n\n// ============================================================\n// Cambio de modo (si alternas en caliente)\n// ============================================================\nif (lastMode !== mode) {\n // Reset suave del modo al que entras para evitar estados \"a medias\"\n if (mode === \"EPOCH\") {\n credits = null; // fuerza recarga en la próxima decisión\n saveEpoch();\n } else { // TOUR\n // no destruyo TOUR, pero si quieres reinicio limpio al entrar:\n // idx = 0; cur = null; served = 0; competitive = false; saveTour();\n }\n context.set('policy_mode_last', mode);\n lastMode = mode;\n}\n\n// ============================================================\n// HANDLER: CMD/SERVED\n// ============================================================\nif (t === 'CMD/SERVED') {\n const i = Number(msg.payload?.i);\n\n if (mode === \"EPOCH\") {\n // Descontar crédito SIEMPRE que se sirvió un vehículo de ese lazo\n ensureCreditsLoaded({ 1: 1, 2: 1, 3: 1 }); // por si acaso\n if (credits[i] != null && credits[i] > 0) credits[i]--;\n saveEpoch();\n return null;\n }\n\n // --- TOUR (tu lógica actual) ---\n if (competitive && cur === i) {\n served++;\n if (served >= (quota[i] ?? 1)) {\n idx = (idx + 1) % 3;\n cur = null;\n served = 0;\n }\n }\n updateStatus();\n saveTour();\n return null;\n}\n\n// ============================================================\n// Solo respondemos a CMD/PICK_NEXT\n// ============================================================\nif (t !== 'CMD/PICK_NEXT') return null;\n\nconst d = getDemands();\nconst act = [1, 2, 3].filter(i => d[i]);\nconst compNow = act.length > 1;\nupdateStatus();\n// ============================================================\n// MODO EPOCH: créditos + reset por época\n// ============================================================\nif (mode === \"EPOCH\") {\n if (!act.length) {\n // nadie pide\n // no hace falta tocar credits\n saveEpoch();\n return null;\n }\n\n // Si solo hay uno, puedes darle paso directo (sin tocar créditos aquí),\n // o también consumir crédito. Lo correcto es consumir cuando llegue CMD/SERVED.\n if (!compNow) {\n saveEpoch();\n return { topic: 'RSP/PICK', payload: { i: act[0], mode: \"EPOCH\", solo: true, credits } };\n }\n\n // Competitivo: aseguramos créditos cargados\n ensureCreditsLoaded(d);\n\n // Intento 1: elegir candidato con crédito >0\n let pick = pickEpoch(d);\n\n // Si hay demanda pero nadie tiene créditos, reset de época y reintento\n if (!pick && anyDemand(d)) {\n resetEpoch();\n pick = pickEpoch(d);\n }\n\n // Fallback (no debería pasar si hay demanda)\n if (!pick) {\n saveEpoch();\n return { topic: 'RSP/PICK', payload: { i: act[0], mode: \"EPOCH\", fallback: true, credits } };\n }\n //updateStatus();\n saveEpoch();\n dbg(`[SCHED/EPOCH] pick=${pick} credits=${JSON.stringify(credits)} d=${JSON.stringify(d)} order=${JSON.stringify(order)}`);\n return { topic: 'RSP/PICK', payload: { i: pick, mode: \"EPOCH\", credits, epoch: true } };\n}\n\n// ============================================================\n// MODO TOUR: lógica original (tal cual)\n// ============================================================\n\n// Si cambia el modo (de no competitivo a competitivo), arrancamos desde el más prioritario\nif (!competitive && compNow) {\n idx = 0; // order[0] es el más prioritario (p.ej. 3)\n cur = null;\n served = 0;\n}\ncompetitive = compNow;\n\nif (!act.length) {\n cur = null; served = 0;\n saveTour();\n return null;\n}\n\nif (!competitive) {\n // Solo uno esperando: dar paso directo, sin lotes\n cur = null; served = 0;\n saveTour();\n return { topic: 'RSP/PICK', payload: { i: act[0], mode: \"TOUR\", solo: true } };\n}\n\n// 1) Mantener el lote si aún tiene demanda y no agotó cuota\nif (cur && d[cur] && served < (quota[cur] ?? 1)) {\n saveTour();\n return { topic: 'RSP/PICK', payload: { i: cur, served, quota: quota[cur] ?? 1, mode: \"TOUR\" } };\n}\n\n// 2) Elegir siguiente carril activo empezando desde idx (rotación justa)\nconst pick = pickNextActiveFrom(idx, d);\nif (!pick) {\n saveTour();\n return { topic: 'RSP/PICK', payload: { i: act[0], fallback: true, mode: \"TOUR\" } };\n}\n\ncur = pick.i;\nidx = pick.j; // idx apunta al carril elegido\nserved = 0;\n\nsaveTour();\ndbg(`[SCHED/TOUR] pick=${cur} idx=${idx} served=${served}/${quota[cur] ?? 1} d=${JSON.stringify(d)}`);\nreturn { topic: 'RSP/PICK', payload: { i: cur, served, quota: quota[cur] ?? 1, wrr: true, mode: \"TOUR\" } };\n\n\nfunction updateStatus(){\n const cr = context.get('epoch_credits') ?? { 1: '-', 2: '-', 3: '-' };\n const st = context.get('served_total') ?? { 1: 0, 2: 0, 3: 0 };\n const se = context.get('served_epoch') ?? { 1: 0, 2: 0, 3: 0 };\n\n const tick = (context.get('tick') ?? 0) ^ 1;\n context.set('tick', tick);\n if(flow.get(\"policy_mode\") === \"TOUR\"){\n node.status({ text:`${tick ? '·' : '/'} TOT ${st[1]}-${st[2]}-${st[3]} | EPO ${se[1]}-${se[2]}-${se[3]}`}); \n }\n else if(flow.get(\"policy_mode\") === \"EPOCH\"){\n node.status({ text:`${tick ? '·' : '/'} EPO ${se[1]}-${se[2]}-${se[3]} | CR ${cr[1]}-${cr[2]}-${cr[3]}`});\n }\n}\n\n\n\n\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1860, + "y": 660, + "wires": [ + [ + "15131de35b7d0425" + ] + ] + }, + { + "id": "fb90705e73d8ee4a", + "type": "inject", + "z": "576f3807f61f5098", + "g": "9b68013ae7e6f87d", + "name": "TOUR", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "TOUR", + "payloadType": "str", + "x": 970, + "y": 1060, + "wires": [ + [ + "e91fc83a620b3648" + ] + ] + }, + { + "id": "e91fc83a620b3648", + "type": "change", + "z": "576f3807f61f5098", + "g": "9b68013ae7e6f87d", + "name": "Mode", + "rules": [ + { + "t": "set", + "p": "policy_mode", + "pt": "flow", + "to": "payload", + "tot": "msg" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 1170, + "y": 1080, + "wires": [ + [] + ] + }, + { + "id": "f7711ab022abbe49", + "type": "inject", + "z": "576f3807f61f5098", + "g": "9b68013ae7e6f87d", + "name": "EPOCH", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "EPOCH", + "payloadType": "str", + "x": 970, + "y": 1100, + "wires": [ + [ + "e91fc83a620b3648" + ] + ] + }, + { + "id": "45a87cfad42d825f", + "type": "inject", + "z": "576f3807f61f5098", + "g": "9b68013ae7e6f87d", + "name": "Reset", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "CMD/RESET_ALL", + "payload": "1", + "payloadType": "num", + "x": 970, + "y": 1200, + "wires": [ + [ + "77cf26031855da2d" + ] + ] + }, + { + "id": "77cf26031855da2d", + "type": "link out", + "z": "576f3807f61f5098", + "g": "9b68013ae7e6f87d", + "name": "link out 4", + "mode": "link", + "links": [ + "e7754eeae28a6256" + ], + "x": 1085, + "y": 1200, + "wires": [] + }, + { + "id": "ab9eeeb391287001", + "type": "inject", + "z": "576f3807f61f5098", + "g": "9b68013ae7e6f87d", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "true", + "payloadType": "bool", + "x": 970, + "y": 1260, + "wires": [ + [ + "f28f2c7106d2590f" + ] + ] + }, + { + "id": "f28f2c7106d2590f", + "type": "change", + "z": "576f3807f61f5098", + "g": "9b68013ae7e6f87d", + "name": "debug ON/OFF", + "rules": [ + { + "t": "set", + "p": "debug", + "pt": "flow", + "to": "payload", + "tot": "msg" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 1140, + "y": 1280, + "wires": [ + [] + ] + }, + { + "id": "d7abaec22c1a4d68", + "type": "inject", + "z": "576f3807f61f5098", + "g": "9b68013ae7e6f87d", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "false", + "payloadType": "bool", + "x": 970, + "y": 1300, + "wires": [ + [ + "f28f2c7106d2590f" + ] + ] + }, + { + "id": "5ecf6d948f96ea9a", + "type": "link out", + "z": "576f3807f61f5098", + "g": "e928e581c722fe25", + "name": "link out 3", + "mode": "link", + "links": [ + "e7754eeae28a6256" + ], + "x": 2375, + "y": 880, + "wires": [] + }, + { + "id": "94af1c83dc635ea7", + "type": "inject", + "z": "576f3807f61f5098", + "g": "9b68013ae7e6f87d", + "name": "EPOCH_RELIEF", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "EPOCH_RELIEF", + "payloadType": "str", + "x": 1000, + "y": 1140, + "wires": [ + [ + "e91fc83a620b3648" + ] + ] + }, + { + "id": "4011f30e8a41b199", + "type": "function", + "z": "576f3807f61f5098", + "g": "e51291e8f6dc7282", + "name": "Scheduler WRR 3 modos", + "func": "// Chequea si hay que hacer reset del sistema\n\nconst t = msg.topic || '';\nconst order = flow.get('order') ?? [3, 2, 1];\n\n// ---------- Debug ON/OFF ----------\nconst DEBUG = flow.get(\"debug\");\nconst dbg = (s) => { if (DEBUG) node.warn(s); };\n\n//-------------------------------------------------------------------------------------------------\nif (t === 'CMD/RESET_ALL') {\n context.set('idx', 0);\n context.set('cur', null);\n context.set('served', 0);\n context.set('competitive', false);\n\n // Si usamos modo EPOCH (créditos)\n context.set('epoch_credits', null);\n context.set('policy_mode_last', null);\n\n // Si usamos modo RELIEF (créditos)\n context.set('relief_active', false);\n context.set('relief_from', null);\n context.set('relief_remaining', 0);\n resetRelief()\n \n node.status({ fill: 'grey', shape: 'ring', text: 'RESET_ALL' });\n return null;\n}\n//---------------------------------------------------------------------------------------------------\n\nif(t === 'CMD/REFRESH_STATUS') {\n updatestatus()\n return null\n}\n\n\n\n\n// =======================\n// CONFIG / PARAMETROS\n// =======================\nconst quota = { // pesos / cuotas por lazo\n 1: (flow.get('N1') ?? 2),\n 2: (flow.get('N2') ?? 5),\n 3: (flow.get('N3') ?? 3),\n};\n\n// Flag de política: \"TOUR\" (actual) o \"EPOCH\" (nuevo)\nconst mode = (msg.payload?.mode || flow.get(\"policy_mode\") || \"TOUR\").toUpperCase();\nconst relief_total = (flow.get('relief_total') ?? 1); // cantidad de vehiculos del menos prioritario cuando se vacian creditos del mas prioritario\n// =======================\n// HELPERS\n// =======================\nfunction pickNextActiveFrom(startIdx, d) {\n for (let k = 0; k < 3; k++) {\n const j = (startIdx + k) % 3;\n const i = order[j];\n if (d[i]) return { i, j };\n }\n return null;\n}\n\nfunction getDemands() {\n return msg.payload?.demands || {\n 1: flow.get('L1') ? 1 : 0,\n 2: flow.get('L2') ? 1 : 0,\n 3: flow.get('L3') ? 1 : 0,\n };\n}\n// =======================\n// RELIEF helpers/state (solo para EPOCH_RELIEF)\n// =======================\nlet relief_active = context.get('relief_active') ?? false;\nlet relief_from = context.get('relief_from') ?? null; // 1/2/3\nlet relief_remaining = context.get('relief_remaining') ?? 0; // entero >=0\n\nfunction saveRelief() {\n context.set('relief_active', relief_active);\n context.set('relief_from', relief_from);\n context.set('relief_remaining', relief_remaining);\n}\n\nfunction resetRelief() {\n relief_active = false;\n relief_from = null;\n relief_remaining = 0;\n saveRelief();\n}\n\nfunction lowerPriorityListOf(i) {\n const idx = order.indexOf(Number(i));\n return idx >= 0 ? order.slice(idx + 1) : []; // “por debajo” de i según tu order (ej: [3,2,1])\n}\n\nfunction isLowerThan(servedLane, fromLane) {\n const a = order.indexOf(Number(fromLane));\n const b = order.indexOf(Number(servedLane));\n return (a >= 0 && b >= 0 && b > a);\n}\n// FINAL DE LOS HELPERS \n// ===========================================================\n\n\n// ============================================================\n// MODO TOUR (logica original) - estado en context (SIN CAMBIOS)\n// ============================================================\nlet idx = context.get('idx') ?? 0; // índice dentro de order (0..2)\nlet cur = context.get('cur') ?? null; // carril actual del lote (1/2/3 o null)\nlet served = context.get('served') ?? 0; // servidos dentro del lote actual\nlet competitive = context.get('competitive') ?? false;\n\nconst saveTour = () => {\n context.set('idx', idx);\n context.set('cur', cur);\n context.set('served', served);\n context.set('competitive', competitive);\n};\n\n// ============================================================\n// MODO EPOCH (créditos por lazo + reset por época)\n// estado en context separado\n// ============================================================\nlet credits = context.get('epoch_credits') ?? null; // {1:x,2:y,3:z} o null para forzar recarga\nlet lastMode = context.get('policy_mode_last') ?? null;\n\nfunction saveEpoch() {\n context.set('epoch_credits', credits);\n}\n\nfunction ensureCreditsLoaded(d) {\n // Si no hay créditos o si algún carril no tiene clave, (re)cargamos\n if (!credits || credits[1] == null || credits[2] == null || credits[3] == null) {\n credits = { 1: quota[1] ?? 1, 2: quota[2] ?? 1, 3: quota[3] ?? 1 };\n return;\n }\n // Opcional: si cambian quotas en caliente, podrías normalizar aquí (lo dejo conservador)\n}\n\nfunction anyDemand(d) {\n return !!(d[1] || d[2] || d[3]);\n}\n\nfunction pickEpoch(d) {\n // 1) candidatos: demanda y crédito > 0, en orden de prioridad (order)\n for (const i of order) {\n if (d[i] && (credits[i] ?? 0) > 0) return i;\n }\n return null;\n}\n\nfunction resetEpoch() {\n credits = { 1: quota[1] ?? 1, 2: quota[2] ?? 1, 3: quota[3] ?? 1 };\n}\n\n// ============================================================\n// Cambio de modo (si alternas en caliente)\n// ============================================================\nif (lastMode !== mode) {\n // Reset suave del modo al que entras para evitar estados \"a medias\"\n if (mode === \"EPOCH\" || mode === \"EPOCH_RELIEF\") {\n credits = null; // fuerza recarga en la próxima decisión\n saveEpoch();\n }\n // Relief solo aplica en EPOCH_RELIEF\n if (mode !== \"EPOCH_RELIEF\") {\n resetRelief();\n }\n // Relief solo aplica en EPOCH_RELIEF\n if (mode !== \"EPOCH_RELIEF\") {\n resetRelief();\n \n } \n else { // TOUR\n // no destruyo TOUR, pero si quieres reinicio limpio al entrar:\n // idx = 0; cur = null; served = 0; competitive = false; saveTour();\n resetRelief();\n }\n context.set('policy_mode_last', mode);\n lastMode = mode;\n}\n\n// ============================================================\n// HANDLER: CMD/SERVED\n// ============================================================\nif (t === 'CMD/SERVED') {\n const i = Number(msg.payload?.i);\n\n if (mode === \"EPOCH\" || mode === \"EPOCH_RELIEF\") {\n\n ensureCreditsLoaded({ 1: 1, 2: 1, 3: 1 }); // por si acaso\n\n // 1) Consumir crédito del lazo servido\n if (credits[i] != null && credits[i] > 0) credits[i]--;\n\n // 2) Si estamos en RELIEF y ha pasado un lazo inferior, consumimos relief_remaining\n if (mode === \"EPOCH_RELIEF\" && relief_active && relief_from != null && relief_remaining > 0) {\n if (isLowerThan(i, relief_from)) {\n relief_remaining--;\n\n // Si se agotó el cupo de relief: recargar SOLO el lazo que cedió\n if (relief_remaining <= 0) {\n credits[relief_from] = quota[relief_from] ?? 1;\n resetRelief(); // guarda en context también\n } else {\n saveRelief();\n }\n }\n }\n\n \n // 3) Activar relief SOLO si no hay relief activo (evitar anidación)\n if (mode === \"EPOCH_RELIEF\" && !relief_active && (credits[i] ?? 0) <= 0) {\n const lowers = lowerPriorityListOf(i);\n\n // OJO: aquí usamos el estado real (flow L1..L3) porque CMD/SERVED no trae demands filtradas\n const dNow = {\n 1: flow.get('L1') ? 1 : 0,\n 2: flow.get('L2') ? 1 : 0,\n 3: flow.get('L3') ? 1 : 0,\n };\n\n const hasLowerDemand = lowers.some(k => dNow[k]);\n\n if (hasLowerDemand) {\n relief_active = true;\n relief_from = i;\n relief_remaining = relief_total;\n saveRelief();\n }\n }\n\n saveEpoch();\n updatestatus();\n return null;\n }\n\n // --- TOUR (lógica original) ---\n if (competitive && cur === i) {\n served++;\n if (served >= (quota[i] ?? 1)) {\n idx = (idx + 1) % 3;\n cur = null;\n served = 0;\n }\n }\n updatestatus();\n saveTour();\n return null;\n}\n// ============================================================\n// Solo respondemos a CMD/PICK_NEXT\n// ============================================================\nif (t !== 'CMD/PICK_NEXT') return null;\n\nconst d = getDemands();\nconst act = [1, 2, 3].filter(i => d[i]);\nconst compNow = act.length > 1;\n\n// ============================================================\n// MODO EPOCH: créditos + reset por época\n// ============================================================\nif (mode === \"EPOCH\") {\n if (!act.length) {\n // nadie pide\n // no hace falta tocar credits\n saveEpoch();\n return null;\n }\n\n // Si solo hay uno, puedes darle paso directo (sin tocar créditos aquí),\n // o también consumir crédito. Lo correcto es consumir cuando llegue CMD/SERVED.\n if (!compNow) {\n saveEpoch();\n return { topic: 'RSP/PICK', payload: { i: act[0], mode: \"EPOCH\", solo: true, credits } };\n }\n\n // Competitivo: aseguramos créditos cargados\n ensureCreditsLoaded(d);\n\n // Intento 1: elegir candidato con crédito >0\n let pick = pickEpoch(d);\n\n // Si hay demanda pero nadie tiene créditos, reset de época y reintento\n if (!pick && anyDemand(d)) {\n resetEpoch();\n pick = pickEpoch(d);\n }\n\n // Fallback (no debería pasar si hay demanda)\n if (!pick) {\n saveEpoch();\n return { topic: 'RSP/PICK', payload: { i: act[0], mode: \"EPOCH\", fallback: true, credits } };\n }\n saveEpoch();\n updatestatus();\n dbg(`[SCHED/EPOCH] pick=${pick} credits=${JSON.stringify(credits)} d=${JSON.stringify(d)} order=${JSON.stringify(order)}`);\n return { topic: 'RSP/PICK', payload: { i: pick, mode: \"EPOCH\", credits, epoch: true } };\n}\n\n// ============================================================\n// MODO EPOCH_RELIEF: epoch + alivio controlado (relief_total)\n// ============================================================\nif (mode === \"EPOCH_RELIEF\") {\n if (!act.length) {\n saveEpoch();\n return null;\n }\n\n // Si solo hay uno, pasas directo (como EPOCH). Relief aquí no pinta.\n if (!compNow) {\n saveEpoch();\n return { topic: 'RSP/PICK', payload: { i: act[0], mode: \"EPOCH_RELIEF\", solo: true, credits } };\n }\n\n // Competitivo\n ensureCreditsLoaded(d);\n\n // -----------------------------\n // FASE RELIEF (si está activa)\n // -----------------------------\n if (relief_active && relief_from != null && relief_remaining > 0) {\n const lowers = lowerPriorityListOf(relief_from);\n \n // candidatos: solo “por debajo” de relief_from, con demanda y crédito > 0\n for (const k of lowers) {\n if (!d[k]) continue;\n\n // ✅ Si un inferior tiene demanda pero está a 0, lo recargamos “al vuelo”\n if ((credits[k] ?? 0) <= 0) {\n credits[k] = quota[k] ?? 1;\n saveEpoch();\n }\n\n // ya con crédito asegurado, puede ser elegido\n if ((credits[k] ?? 0) > 0) {\n saveEpoch();\n updatestatus();\n saveRelief();\n dbg(`[SCHED/EPOCH_RELIEF] RELIEF pick=${k} from=${relief_from} rem=${relief_remaining} credits=${JSON.stringify(credits)} d=${JSON.stringify(d)} order=${JSON.stringify(order)}`);\n return {\n topic: 'RSP/PICK',\n payload: { i: k, mode: \"EPOCH_RELIEF\", credits, relief: { active: true, from: relief_from, rem: relief_remaining } }\n };\n }\n }\n\n // Si no hay inferior elegible: cerramos relief y recargamos el que cedió\n credits[relief_from] = quota[relief_from] ?? 1;\n resetRelief();\n saveEpoch();\n // y seguimos a fase normal (sin return)\n }\n\n // -----------------------------\n // FASE NORMAL (igual a EPOCH)\n // -----------------------------\n let pick = pickEpoch(d);\n\n if (!pick && anyDemand(d)) {\n resetEpoch();\n pick = pickEpoch(d);\n }\n\n if (!pick) {\n saveEpoch();\n return { topic: 'RSP/PICK', payload: { i: act[0], mode: \"EPOCH_RELIEF\", fallback: true, credits } };\n }\n\n saveEpoch();\n updatestatus();\n saveRelief();\n dbg(`[SCHED/EPOCH_RELIEF] pick=${pick} relief=${relief_active ? `ON from=${relief_from} rem=${relief_remaining}` : 'OFF'} credits=${JSON.stringify(credits)} d=${JSON.stringify(d)} order=${JSON.stringify(order)}`);\n return { topic: 'RSP/PICK', payload: { i: pick, mode: \"EPOCH_RELIEF\", credits, relief: { active: relief_active, from: relief_from, rem: relief_remaining } } };\n}\n\n\n// ============================================================\n// MODO TOUR: lógica actual (tal cual)\n// ============================================================\n\n// Si cambia el modo (de no competitivo a competitivo), arrancamos desde el más prioritario\nif (!competitive && compNow) {\n idx = 0; // order[0] es el más prioritario (p.ej. 3)\n cur = null;\n served = 0;\n}\ncompetitive = compNow;\n\nif (!act.length) {\n cur = null; served = 0;\n saveTour();\n return null;\n}\n\nif (!competitive) {\n // Solo uno esperando: dar paso directo, sin lotes\n cur = null; served = 0;\n saveTour();\n return { topic: 'RSP/PICK', payload: { i: act[0], mode: \"TOUR\", solo: true } };\n}\n\n// 1) Mantener el lote si aún tiene demanda y no agotó cuota\nif (cur && d[cur] && served < (quota[cur] ?? 1)) {\n saveTour();\n return { topic: 'RSP/PICK', payload: { i: cur, served, quota: quota[cur] ?? 1, mode: \"TOUR\" } };\n}\n\n// 2) Elegir siguiente carril activo empezando desde idx (rotación justa)\nconst pick = pickNextActiveFrom(idx, d);\nif (!pick) {\n saveTour();\n return { topic: 'RSP/PICK', payload: { i: act[0], fallback: true, mode: \"TOUR\" } };\n}\n\ncur = pick.i;\nidx = pick.j; // idx apunta al carril elegido\nserved = 0;\n\nsaveTour();\ndbg(`[SCHED/TOUR] pick=${cur} idx=${idx} served=${served}/${quota[cur] ?? 1} d=${JSON.stringify(d)}`);\nreturn { topic: 'RSP/PICK', payload: { i: cur, served, quota: quota[cur] ?? 1, wrr: true, mode: \"TOUR\" } };\n\n\nfunction updatestatus(){\n const cr = context.get('epoch_credits') ?? { 1: '-', 2: '-', 3: '-' };\n const st = context.get('served_total') ?? { 1: 0, 2: 0, 3: 0 };\n const se = context.get('served_epoch') ?? { 1: 0, 2: 0, 3: 0 };\n const ra = context.get('relief_active') ? 1 : 0;\n const rf = context.get('relief_from') ?? '-';\n const rr = context.get('relief_remaining') ?? 0;\n const tick = (context.get('tick') ?? 0) ^ 1;\n context.set('tick', tick);\n \n //node.status({text:`TOT ${st[1]}-${st[2]}-${st[3]} | EPO ${se[1]}-${se[2]}-${se[3]}`});\n //node.status({ text: `${tick ? '·' : ' '} EPO ${se[1]}-${se[2]}-${se[3]} | CR ${cr[1]}-${cr[2]}-${cr[3]}` });\n node.status({ text: `${tick ? '·' : ' '} EPO ${se[1]}-${se[2]}-${se[3]} | CR ${cr[1]}-${cr[2]}-${cr[3]} | R${ra}:${rf}/${rr}` });\n\n}", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1870, + "y": 600, + "wires": [ + [ + "15131de35b7d0425" + ] + ] + }, + { + "id": "929228d1dc38672d", + "type": "debug", + "z": "576f3807f61f5098", + "name": "debug 51", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 2340, + "y": 1280, + "wires": [] } ] \ No newline at end of file diff --git a/uibuilder/control_manual/LICENSE b/uibuilder/control_manual/LICENSE new file mode 100644 index 0000000..a16c46a --- /dev/null +++ b/uibuilder/control_manual/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/control_manual/README.md b/uibuilder/control_manual/README.md new file mode 100644 index 0000000..b5257b4 --- /dev/null +++ b/uibuilder/control_manual/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/control_manual/api/.keep b/uibuilder/control_manual/api/.keep new file mode 100644 index 0000000..e69de29 diff --git a/uibuilder/control_manual/dist/.keep b/uibuilder/control_manual/dist/.keep new file mode 100644 index 0000000..e69de29 diff --git a/uibuilder/control_manual/eslint.config.mjs b/uibuilder/control_manual/eslint.config.mjs new file mode 100644 index 0000000..e1008e0 --- /dev/null +++ b/uibuilder/control_manual/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/control_manual/package.json b/uibuilder/control_manual/package.json new file mode 100644 index 0000000..833ab51 --- /dev/null +++ b/uibuilder/control_manual/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": "^51.3.1", + "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/control_manual/routes/.keep b/uibuilder/control_manual/routes/.keep new file mode 100644 index 0000000..e69de29 diff --git a/uibuilder/control_manual/src/index.css b/uibuilder/control_manual/src/index.css new file mode 100644 index 0000000..b9c3925 --- /dev/null +++ b/uibuilder/control_manual/src/index.css @@ -0,0 +1,303 @@ +/* =============================== BASE GLOBAL =============================== */ + + + +body, +html { + margin: 0; + padding: 0; + font-family: "Segoe UI", sans-serif; + background-color: #fff; + color: #333; +} + +.container { + max-width: 400px; + margin: auto; + padding: 20px; +} + +#pantalla-tabla-general .container { + padding-left: 8px; + padding-right: 8px; +} + +hr { + margin: 10px 0 20px; + border: 1px solid #ccc; +} + +/* =============================== ENCABEZADO =============================== */ + +.header, +.encabezado { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; +} + +.logo { + width: 40px; + height: 40px; +} + +.titulo, +h3 { + text-align: center; + margin-top: 10px; + margin-bottom: 3px; + font-size: 26px; + font-weight: bold; +} + + +/* =============================== PANTALLA SEMÁFOROS =============================== */ + + +.oculto { + display: none; +} + + +.modo-label { + font-weight: bold; + color: #555; +} + + +/* Toggle de modo */ +.modo-toggle { + margin: 15px 0; +} + +/* Alerta báscula */ +.alerta-bascula { + background: linear-gradient(135deg, #ff4757 0%, #dc3545 100%); + border: 4px solid #bd2130; + color: #fff; + padding: 16px; + border-radius: 12px; + text-align: center; + font-weight: 900; + font-size: 16px; + letter-spacing: 1px; + margin: 15px 0; +} +.transito-div { + + gap: 8px; + align-items: center; + margin-top: 5px; + text-align: center; + font-size: 20px; + font-weight: 700; + color: rgb(240, 235, 235) +} +.param-div { + gap: 8px; + align-items: center; + text-align: center; + font-size: 14px; + font-weight: 600; + color: rgb(198, 241, 252) +} +.blk { + display: inline-block; + min-width: 1ch; + /* alinea bonito */ + font-weight: 700; +} + +.blk-on { + color: red; +} + +.blk-off { + color: inherit; +} +.blk-name { + font-weight: 700; + padding: 0 6px; + border-radius: 6px; +} + +.param { + display: block; +} +.is-transito { + color: rgb(4, 201, 119); +} +.nombre-lazo { + font-weight: 700; + color: red; + font-size: 20px; +} +.bascula{ + + gap: 16px; + align-items: center; + text-align: center; + font-size: 32px; + font-weight: 800; +} +.is-true { + color: red; +} + +.is-false { + color: green; +} +.is-clr { + color: rgb(255, 94, 1); +} + +/* Grid de semáforos */ +.semaforos-container { + display: flex; + justify-content: center; + gap: 5px; + margin: 20px 0; +} + + +/* Card de cada semáforo */ +.semaforo-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + border-radius: 10px; + +} + +.semaforo-label { + font-size: 24px; + font-weight: bold; + color: #333; +} + +/* LED del semáforo */ +.led-semaforo { + width: 80px; + height: 80px; + border-radius: 50%; + background-color: #ccc; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); + transition: background-color 0.3s ease, box-shadow 0.3s ease; + +} + +.led-semaforo.red { + background-color: #dc3545; + box-shadow: 0 0 20px rgba(220, 53, 69, 0.6); +} + +.led-semaforo.green { + background-color: #28a745; + box-shadow: 0 0 20px rgba(40, 167, 69, 0.8); +} + + +/* Botón de cada semáforo */ +.btn-semaforo { + padding: 10px 20px; + font-size: 14px; + font-weight: bold; + border: none; + border-radius: 6px; + color: white; + transition: all 0.2s ease; + min-width: 130px; +} + +.btn-semaforo.green{ + background-color: #28a745; + cursor: pointer; +} +.btn-semaforo.blue { + background-color: #3498db; + cursor: pointer; +} +/* =============================== BOTONES =============================== */ + +.btn { + padding: 16px; + font-size: 16px; + font-weight: bold; + border: none; + border-radius: 10px; + color: white; + cursor: pointer; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.btn.big { + width: 100%; + margin-top: 10px; +} + +/* Colores */ +.btn.yellow, +.btn.amarillo { + background: #f1c40f; + color: #444; +} + +.btn.orange, +.btn.naranja { + background: #e67e22; +} + +.btn.green, +.btn.verde { + background: #28a745; +} +.btn.blue { + background: #0fcff5; +} +.btn.teal, +.btn.verde-oscuro { + background: #049a81; +} + +.btn.red, +.btn.rojo { + background: firebrick; +} + +.btn.blue, +.btn.azul { + background: #3498db; +} + +.btn.darkblue, +.btn.azuloscuro { + background: #0567a9; +} + +.btn.aqua, +.btn.celeste { + background: #04fbc7; + color: #333; +} + +.btn.purple, +.btn.morado { + background: #9b59b6; +} + +.btn.gray, +.btn.gris { + background: #707b7c; +} + +.btn.gold, +.btn.mostaza { + background: #DAB957; + color: #444; +} + + + +@import url("../uibuilder/uib-brand.min.css"); \ No newline at end of file diff --git a/uibuilder/control_manual/src/index.html b/uibuilder/control_manual/src/index.html new file mode 100644 index 0000000..c9889d7 --- /dev/null +++ b/uibuilder/control_manual/src/index.html @@ -0,0 +1,84 @@ + + + + + + Control de Semáforos + + + + + + + + +
+
+ + +
+ +

Trypton Software

+
+ +

CONTROL DE SEMÁFOROS

+ +
+ + +
+ +
+ EN TRANSITO + +
+ +
+ BASCULA + +
+ + +
+ +
+ + + +
+ +
+
S1
+
+ +
+ +
+
S2
+
+ +
+ +
+
S3
+
+ +
+ +
+ + + +
+
+ + + diff --git a/uibuilder/control_manual/src/index.js b/uibuilder/control_manual/src/index.js new file mode 100644 index 0000000..3a63c64 --- /dev/null +++ b/uibuilder/control_manual/src/index.js @@ -0,0 +1,218 @@ +uibuilder.start(); + +let estado = {}; + +// Pedimos estado inicial +uibuilder.send({ topic: "REQUEST_ESTADO" }); + +// ========================= +// REFERENCIAS DOM +// ========================= +const btnModo = document.getElementById("btn-modo"); +const btnModoText = document.getElementById("btn-modo-text"); +const contenedorSemaforos = document.getElementById("semaforos-container"); +const botonesSemaforo = document.querySelectorAll(".btn-semaforo"); +const alerta = document.getElementById("alerta-bascula"); + +const labelS1 = document.querySelector('[data-name="S1"]'); +const labelS2 = document.querySelector('[data-name="S2"]'); +const labelS3 = document.querySelector('[data-name="S3"]'); +const transito = document.getElementById('transito'); +const param = document.getElementById('param'); +const param2 = document.getElementById('param-2'); + +// ========================= +// RECEPCIÓN DE ESTADO +// ========================= +uibuilder.onChange('msg', (msg) => { + + if (!msg.payload) return; + + // Sobrescribimos estado con todo el payload que llega + estado = { ...estado, ...msg.payload }; + + // Actualizar nombres de semáforos + if (labelS1 && estado.nombre_S1) labelS1.textContent = estado.nombre_S1; + if (labelS2 && estado.nombre_S2) labelS2.textContent = estado.nombre_S2; + if (labelS3 && estado.nombre_S3) labelS3.textContent = estado.nombre_S3; + + // Si la báscula esta ocupada mostramos alerta y ocultamos semáforos + /* + if (estado.B_OCC == 1) { + alerta.classList.remove("oculto"); + contenedorSemaforos.className = "oculto" + } else { + alerta.classList.add("oculto"); + contenedorSemaforos.className = "semaforos-container" + } + */ + labelS1.style.color = estado.lazo1 ? 'red' : 'green'; + labelS2.style.color = estado.lazo2 ? 'red' : 'green'; + labelS3.style.color = estado.lazo3 ? 'red' : 'green'; + // chequea el estado de transito para indicar el lazo que esta actualmente en transito + const m = (estado.state || '').match(/^TRANS_(\d+)$/); + transito.classList.toggle('is-transito', !!m); + if (m) { + const nombre = estado[`nombre_S${m[1]}`] || `L${m[1]}`; + transito.innerHTML = `«${nombre}» EN TRANSITO`; + } else { + transito.textContent = 'EN TRANSITO'; + } + + // Hace display de los lazos bloqueados y del policy mode actual ----- Linea 1 ------- + const b = estado.blocked ?? { 1: false, 2: false, 3: false }; + const policy_mode = estado.policy_mode ?? '-'; + + const nombre = { + 1: estado.nombre_S1 ?? 'L1', + 2: estado.nombre_S2 ?? 'L2', + 3: estado.nombre_S3 ?? 'L3', + }; + + // Creamos etiquetas por lazo, en orden 1-2-3 (o cámbialo si prefieres otro orden) + const labels = [1, 2, 3].map(i => { + const cls = b[i] ? 'blk-on' : 'blk-off'; + return `${nombre[i]}`; + }).join('-'); // separador bonito + + param.innerHTML = `BLK: ${labels}    MODO: ${policy_mode}`; + + // ----- Línea 2 ----- + const pri = Array.isArray(estado.priority) ? estado.priority : [3, 2, 1]; // ej [3,2,1] + const max = Array.isArray(estado.numero_max_vehiculos) ? estado.numero_max_vehiculos : [0, 0, 0]; // ej [1,1,3] = [N1,N2,N3] + // nombres por lazo + + // Ejemplo Prioridad = [Talleres, Entrada, Fosos] + const prioridadNombres = pri.map(i => nombre[i] ?? `L${i}`); + // Maximo Vehiculos = [N3, N2, N1] (valores de N en el MISMO orden de prioridad) + const maxVehiculos = pri.map(i => max[i - 1] ?? '-'); // porque max viene como [N1,N2,N3] + // Mostrar + param2.textContent = + `Prioridad = [${prioridadNombres.join(', ')}] Vehiculos = [${maxVehiculos.join(', ')}]`; + + // Chequea bascula para indicar estado + setColor('bascula', estado.B_OCC, estado.state); + + // Sincronizar UI + initUI(); + actualizarUI(); + +}); + +// ========================= +// EVENTOS +// ========================= + +// Botón Modo +btnModo.addEventListener("click", () => { + // Cambiamos localmente para feedback inmediato + estado.mode = (estado.mode === "AUTO") ? "MANUAL" : "AUTO"; + + // Actualizamos UI + initUI(); + actualizarUI(); + + // Enviamos cambio + uibuilder.send({ + topic: "CAMBIAR_MODO", + payload: estado.mode + }); +}); + +// Botones semáforo +botonesSemaforo.forEach(btn => { + btn.addEventListener("click", () => { + const id = btn.dataset.semaforo; + + if (estado[id] === "GREEN") { + // Apagar este semáforo + estado[id] = "RED"; + } else { + // Encender este y apagar los demás + ["S1", "S2", "S3"].forEach(key => { + estado[key] = (key === id) ? "GREEN" : "RED"; + }); + } + + actualizarUI(); + + uibuilder.send({ + topic: "TOGGLE_SEMAFORO", + payload: id + }); + }); +}); + +// ========================= +// FUNCIONES UI +// ========================= +function initUI() { + if (estado.mode === "MANUAL") { + btnModoText.textContent = "CAMBIAR A AUTOMATICO"; + btnModo.className = "btn big amarillo"; + habilitarSemaforos(true); + } else { + btnModoText.textContent = "CAMBIAR A MANUAL"; + btnModo.className = "btn big blue"; + habilitarSemaforos(false); + resetearLEDs(); + } +} + +function actualizarUI() { + //Solo actualizar si estamos en MANUAL. Comentada la siguiente linea para que permita visualizar los semaforos incluso en automatico + //if (estado.mode !== "MANUAL") return; + + ["S1", "S2", "S3"].forEach(key => { + const led = document.querySelector(`[data-led="${key}"]`); + const textoBtn = document.querySelector(`[data-semaforo="${key}"] span`); + + if (!led || !textoBtn) return; + + if (estado[key] === "GREEN") { + led.className = "led-semaforo green"; + textoBtn.textContent = "DESACTIVAR"; + } else { + led.className = "led-semaforo red"; + textoBtn.textContent = "ACTIVAR"; + } + }); +} + +function resetearLEDs() { + // Poner en gris cuando esta en modo automático + ["S1", "S2", "S3"].forEach(key => { + const led = document.querySelector(`[data-led="${key}"]`); + const textoBtn = document.querySelector(`[data-semaforo="${key}"] span`); + + if (led) led.className = "led-semaforo"; // Sin clase red/green = gris + + }); +} + +function habilitarSemaforos(activo) { + botonesSemaforo.forEach(btn => { + btn.disabled = !activo; + btn.className = activo ? "btn-semaforo blue" : "btn-semaforo"; + }); +} + +function setColor(id, val, state) { // funcion para darle color a tru or false en un id determinado + console.log("state = " + state) + const el = document.getElementById(id); + if (!el) return; + val = !!val //forzamos a boolean + const isClear = state === 'CLEAR'; + el.classList.remove('is-true', 'is-false', 'is-clr'); // elimina todas las clases + if (val) { + el.classList.add('is-true'); + el.textContent = 'BASCULA OCUPADA'; + } else if (isClear) { + el.classList.add('is-clr'); + el.textContent = 'LIBERANDO BASCULA'; + } else { + el.classList.add('is-false'); + el.textContent = 'BASCULA LIBRE'; + } + +} \ No newline at end of file diff --git a/uibuilder/control_manual/tsconfig.json b/uibuilder/control_manual/tsconfig.json new file mode 100644 index 0000000..38f9ea4 --- /dev/null +++ b/uibuilder/control_manual/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/control_manual/types/uibuilder.d.ts b/uibuilder/control_manual/types/uibuilder.d.ts new file mode 100644 index 0000000..0d359af --- /dev/null +++ b/uibuilder/control_manual/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/control_manual/types/uibuilder.module.d.ts b/uibuilder/control_manual/types/uibuilder.module.d.ts new file mode 100644 index 0000000..44eecc7 --- /dev/null +++ b/uibuilder/control_manual/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