{"id":11297,"date":"2025-12-03T17:17:15","date_gmt":"2025-12-03T16:17:15","guid":{"rendered":"https:\/\/bitacora.eniac2000.com\/?p=11297"},"modified":"2025-12-03T17:17:17","modified_gmt":"2025-12-03T16:17:17","slug":"trazabilidad-de-activos-con-lorawan-e-ia-generativa-dispositivos-y-codificacion-para-chirpstack","status":"publish","type":"post","link":"https:\/\/bitacora.eniac2000.com\/?p=11297","title":{"rendered":"Trazabilidad de activos con LoRaWAN e IA generativa. Dispositivos y codificaci\u00f3n para ChirpStack"},"content":{"rendered":"<div class=\"seriesmeta\">Esta entrada es la parte 4 de 7 de la serie <a href=\"https:\/\/bitacora.eniac2000.com\/?series=trazabilidad-de-activos-con-lorawan-e-ia-generativa\" class=\"series-1870\" title=\"Trazabilidad de activos con LoRaWAN e IA generativa\">Trazabilidad de activos con LoRaWAN e IA generativa<\/a><\/div>\n<p>Una vez solventados los asuntos de la infraestructura LoRaWAN, era el momento de ponerse a pensar en los dispositivos. En este sentido, dado que se trataba de un proyecto de posicionamiento y trazabilidad de activos, el primer punto a considerar era el de disponer de posicionamiento GPS. Y, obviamente, comunicaciones LoRaWAN. En este sentido, ten\u00eda a mi disposici\u00f3n tres tipos de dispositivos:<\/p>\n\n\n\n<p><strong><a href=\"https:\/\/heltec.org\/project\/htcc-ab01-v2\/\" target=\"_blank\" rel=\"noreferrer noopener\">Heltec Cubecell AB01<\/a>:<\/strong> Dispositivos de prototipado de Heltec, en la gama de Cubecell. Esta gama es una gama estupenda para trabajar con LoRaWAN. Programables como dispositivos Arduino, compatibles con la mayor\u00eda de los desarrollos para esta plataforma, y muy econ\u00f3mico desde el punto de vista de uso de la energ\u00eda. Sin embargo, carece de algo importante: el GPS. Sin embargo, esto se puede solucionar de manera sencilla acopl\u00e1ndole un m\u00f3dulo GPS externo, como es el caso del Neo 6M.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"395\" src=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/cubecell-ab01-gps-neo-6m-1024x395.png\" alt=\"\" class=\"wp-image-11298\" srcset=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/cubecell-ab01-gps-neo-6m-1024x395.png 1024w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/cubecell-ab01-gps-neo-6m-300x116.png 300w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/cubecell-ab01-gps-neo-6m-768x296.png 768w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/cubecell-ab01-gps-neo-6m-1536x593.png 1536w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/cubecell-ab01-gps-neo-6m.png 1755w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Diagrama Cubecell AB01 y el Neo 6M<\/figcaption><\/figure>\n\n\n\n<p>La conexi\u00f3n con el Neo 6M se realiza usando los puertos 3V3 y GND, en lo referente a alimentaci\u00f3n, y GPIO4 y GPIO5 para las comunicaciones E\/S del dispositivo. Dista de ser una soluci\u00f3n ideal, dado que el CubeCell puede entrar en modo de ahorro de energ\u00eda, pero el m\u00f3dulo Neo 6M no lo hace. Sin embargo, como soluci\u00f3n para prototipado es perfectamente v\u00e1lida, es muy econ\u00f3mica, apenas unos pocos euros por cada uno de los dispositivos. Fue el primer dispositivo con el que empec\u00e9 a trabajar en el proyecto.,<\/p>\n\n\n\n<p><strong><a href=\"https:\/\/heltec.org\/project\/htcc-ab02s\/\" target=\"_blank\" rel=\"noreferrer noopener\">Dispositivos Heltec CubeCell AB02S:<\/a> <\/strong>De nuevo, de la familia del AB01, pero a diferencia del anterior, integra una peque\u00f1a pantalla LCD y, sobre todo, un m\u00f3dulo GPS Air530 o Air530Z (dependiendo de la fecha de fabricaci\u00f3n del dispositivo). Su principal ventaja es la integraci\u00f3n del m\u00f3dulo GPS, que permite ir a un modo de ahorro de energ\u00eda de apenas 21 uA en modo <em>deep sleep<\/em>, pero tiene como inconveniente un precio que ronda la treintena de euros. Sigue sin ser, sin embargo, un importe disparatado.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"800\" src=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/heltec-ab02s.jpg\" alt=\"\" class=\"wp-image-11299\" style=\"width:401px;height:auto\" srcset=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/heltec-ab02s.jpg 800w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/heltec-ab02s-300x300.jpg 300w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/heltec-ab02s-150x150.jpg 150w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/heltec-ab02s-768x768.jpg 768w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><figcaption class=\"wp-element-caption\">Heltec AB02S<\/figcaption><\/figure>\n\n\n\n<p>El dispositivo, como dec\u00eda, integra un m\u00f3dulo GPS y una antena en la propia placa, pero he podido comprobar que esta antena no es demasiado efectiva, por lo que es necesario contar con una antena GPS externa. Desde el punto de vista de la alimentaci\u00f3n, me decid\u00ed a usar una alimentaci\u00f3n por el puerto integrado que permite suministrarles 3.7v, lo que es perfecto para usar una bater\u00eda recargable 18650. As\u00ed que con una carcasa impresa en 3D, queda algo tan solvente como lo que sigue:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"845\" height=\"1024\" src=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/heltec-ab02s-18650-845x1024.jpg\" alt=\"\" class=\"wp-image-11303\" style=\"aspect-ratio:0.825195991962743;width:420px;height:auto\" srcset=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/heltec-ab02s-18650-845x1024.jpg 845w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/heltec-ab02s-18650-248x300.jpg 248w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/heltec-ab02s-18650-768x930.jpg 768w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/heltec-ab02s-18650.jpg 857w\" sizes=\"auto, (max-width: 845px) 100vw, 845px\" \/><figcaption class=\"wp-element-caption\">Carcasa 3D con bater\u00eda 18650 y regulador de carga<\/figcaption><\/figure>\n\n\n\n<p><strong><a href=\"https:\/\/www.dragino.com\/products\/tracker\/item\/142-lgt-92.html\" target=\"_blank\" rel=\"noreferrer noopener\">Dragino LGT-92<\/a><\/strong>: Sin duda, es el dispositivo m\u00e1s interesante. Es un dispositivo acabado, con carcasa externa, pulsador, bater\u00eda y un aceler\u00f3metro de 9 ejes. Tambi\u00e9n es el m\u00e1s caro, rondando los 90 euros, aunque es posible encontrarlo de oferta por importes sensiblemente inferiores.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1000\" height=\"489\" src=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/dragino-lgt92.jpg\" alt=\"\" class=\"wp-image-11300\" srcset=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/dragino-lgt92.jpg 1000w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/dragino-lgt92-300x147.jpg 300w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/dragino-lgt92-768x376.jpg 768w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">Dragino LGT-92<\/figcaption><\/figure>\n\n\n\n<p>Este dispositivo, aparte de ser programable, presenta una soluci\u00f3n s\u00f3lida con una buena antena GPS. Adem\u00e1s el fabricante proporciona los <em>codecs<\/em> para ChirpStack, aunque en su versi\u00f3n 3.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Codificaci\u00f3n de los dispositivos e integraci\u00f3n con ChirpStack<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Dispositivos Heltec<\/h3>\n\n\n\n<p>En el caso de los dispositivos Heltec, es preciso pasar por un proceso de programaci\u00f3n, que puede realizarse con el IDE de Arduino. En ambos casos, uno de los puntos clave es aprovechar las capacidad de <em>deep sleep<\/em> de los dispositivos. El AB01, para comunicarse con el GPS, hace uso de la librer\u00eda TinyGPS++, mientras que el AB02S hace uso de la GPS_Air530.h (o la variante Z si el dispositivo tiene esta versi\u00f3n del m\u00f3dulo).<\/p>\n\n\n\n<p>El punto clave en ambos casos es la preparaci\u00f3n de la carga \u00fatil para la transmisi\u00f3n de los datos. En mi caso, como part\u00ed de la versi\u00f3n AB01 con el Neo para la obtenci\u00f3n de informaci\u00f3n, opt\u00e9 por transmitir una serie de valores b\u00e1sicos: latitud, longitud, altitud, velocidad codificada en km\/h y rumbo en grados. Para transmitir estos valores hago uso de 14 bytes, de la siguiente manera:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>4 bytes para la latitud<\/li>\n\n\n\n<li>4 bytes para la longitud<\/li>\n\n\n\n<li>2 bytes para la altitud<\/li>\n\n\n\n<li>2 bytes para la velocidad<\/li>\n\n\n\n<li>2 bytes para el rumbo<br><\/li>\n<\/ul>\n\n\n\n<p>&#8230;codificado de la siguiente manera:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>      \/\/ Codificaci\u00f3n simple: multiplica por 1e6 para enviar como entero 4 bytes cada uno\n      int32_t lat_enc = (int32_t)(lat * 1e6);\n      int32_t lng_enc = (int32_t)(lng * 1e6);\n      int16_t alt_enc = (int16_t)gps.altitude.meters();  \/\/ Altitud en metros\n      uint16_t spd_enc = (uint16_t)(gps.speed.kmph() * 100); \/\/ Velocidad x100 para decimales\n      uint16_t crs_enc = (uint16_t)(gps.course.deg() * 100); \/\/ Curso\/direcci\u00f3n en grados multiplicada por 100 (dos decimales)\n\n\n      appDataSize = 14;  \n      appData&#91;0] = (lat_enc >> 24) &amp; 0xFF;\n      appData&#91;1] = (lat_enc >> 16) &amp; 0xFF;\n      appData&#91;2] = (lat_enc >> 8) &amp; 0xFF;\n      appData&#91;3] = lat_enc &amp; 0xFF;\n      appData&#91;4] = (lng_enc >> 24) &amp; 0xFF;\n      appData&#91;5] = (lng_enc >> 16) &amp; 0xFF;\n      appData&#91;6] = (lng_enc >> 8) &amp; 0xFF;\n      appData&#91;7] = lng_enc &amp; 0xFF;\n      appData&#91;8] = (alt_enc >> 8) &amp; 0xFF;\n      appData&#91;9] = alt_enc &amp; 0xFF;\n      appData&#91;10] = (spd_enc >> 8) &amp; 0xFF;\n      appData&#91;11] = spd_enc &amp; 0xFF;\n      appData&#91;12] = (crs_enc >> 8) &amp; 0xFF;\n      appData&#91;13] = crs_enc &amp; 0xFF;\n<\/code><\/pre>\n\n\n\n<p>Esto implica que, a la hora de crear el perfil de dispositivos en ChirpStack v4, es preciso definir una funci\u00f3n de decodificaci\u00f3n, que convierta la codificaci\u00f3n anterior a un JSON con los valores obtenidos:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function decodeUplink(input) {\n  const bytes = input.bytes;\n  if (bytes.length &lt; 8) {\n    return {\n      data: {},\n      errors: &#91;\"Payload demasiado corto\"]\n    };\n  }\n\n  \/\/ Reconstruir enteros 32 bits big-endian para lat y lng\n  const lat_enc = (bytes&#91;0] &lt;&lt; 24) | (bytes&#91;1] &lt;&lt; 16) | (bytes&#91;2] &lt;&lt; 8) | bytes&#91;3];\n  const lng_enc = (bytes&#91;4] &lt;&lt; 24) | (bytes&#91;5] &lt;&lt; 16) | (bytes&#91;6] &lt;&lt; 8) | bytes&#91;7];\n  \n  \n  \/\/ Altitud (2 bytes, big-endian, signed)\n  const alt_enc = (bytes&#91;8] &lt;&lt; 8) | bytes&#91;9];\n  \n  \/\/ Velocidad (2 bytes, big-endian, unsigned)\n  const spd_enc = (bytes&#91;10] &lt;&lt; 8) | bytes&#91;11];\n\n  \/\/ Curso (2 bytes, big-endian, unsigned)\n  const crs_enc = (bytes&#91;12] &lt;&lt; 8) | bytes&#91;13];\n\n\n\n  \/\/ Convertir a signed (considerando complemento a dos)\n  const lat = lat_enc >> 0; \n  const lng = lng_enc >> 0;\n  const alt = (alt_enc &amp; 0x8000) ? alt_enc - 0x10000 : alt_enc; \/\/ Convierte a signed int16\n  const spd = spd_enc \/ 100.0;  \/\/# convertido a km\/h con decimales\n  const crs = crs_enc \/ 100.0;  \/\/# grados con decimales\n\n  return {\n    data: {\n      latitud: lat \/ 1e6,\n      longitud: lng \/ 1e6,\n      altitud: alt,\n      velocidad: spd,\n      rumbo: crs,\n    },\n    errors: &#91;],\n    warnings: &#91;]\n  };\n}<\/code><\/pre>\n\n\n\n<p>&#8230;lo que produce el siguiente resultado al decodificarlo en ChirpStack:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"281\" height=\"137\" src=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/12\/lorawan-cubecell.png\" alt=\"\" class=\"wp-image-11302\"\/><figcaption class=\"wp-element-caption\">JSON procesado por ChirpStack<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Dispositivos Dragino<\/h3>\n\n\n\n<p>En el caso de Dragino, la soluci\u00f3n es bastante m\u00e1s sencilla a priori. Los dispositivos ya vienen codificados para funcionar desde el principio, lo \u00fanico necesario es definir el perfil de los dispositivos en ChirpStack, y <a href=\"https:\/\/www.dragino.com\/downloads\/index.php?dir=LGT_92\/Decoder\/\" target=\"_blank\" rel=\"noreferrer noopener\">el fabricante proporciona los <em>codecs<\/em><\/a>. Sin embargo, en el caso de ChirpStack s\u00f3lo son compatibles para la v3. En mi caso, y con la ayuda de Perplexity, he codificado una versi\u00f3n para v4:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function decodeUplink(input) {\n  var bytes = input.bytes;\n  var fPort = input.fPort;\n\n  \/\/ Aqu\u00ed puedes usar tu l\u00f3gica para decodificar los bytes seg\u00fan el puerto o tipo de mensaje\n  var data = {\n    latitud: (bytes&#91;0]&lt;&lt;24 | bytes&#91;1]&lt;&lt;16 | bytes&#91;2]&lt;&lt;8 | bytes&#91;3]) \/ 1000000,\n    longitud: (bytes&#91;4]&lt;&lt;24 | bytes&#91;5]&lt;&lt;16 | bytes&#91;6]&lt;&lt;8 | bytes&#91;7]) \/ 1000000,\n    ALARM_status: (bytes&#91;8] &amp; 0x40) > 0,\n    BatV: ((bytes&#91;8] &amp; 0x3f)&lt;&lt;8 | bytes&#91;9]) \/ 1000,\n    MD: { \"0\": \"Disable\", \"1\": \"Move\", \"2\": \"Collide\", \"3\": \"Custom\" }&#91;bytes&#91;10]>>6],\n    LON: (bytes&#91;10] &amp; 0x20) ? \"ON\" : \"OFF\",\n    FW: 150 + (bytes&#91;10] &amp; 0x1f),\n    Roll: (bytes&#91;11]&lt;&lt;24>>16 | bytes&#91;12]) \/ 100,\n    Pitch: (bytes&#91;13]&lt;&lt;24>>16 | bytes&#91;14]) \/ 100\n  };\n\n  return { data: data }; \/\/ Es importante devolver un objeto con la propiedad data\n}<\/code><\/pre>\n\n\n\n<p>Con todo esto, los dispositivos est\u00e1n listos para su uso en ChirpStack, con lo que estamos listos para el siguiente punto del proyecto: el desarrollo de la aplicaci\u00f3n de trazabilidad de activos.<\/p>\n","protected":false},"excerpt":{"rendered":"<div class=\"seriesmeta\">Esta entrada es la parte 4 de 7 de la serie <a href=\"https:\/\/bitacora.eniac2000.com\/?series=trazabilidad-de-activos-con-lorawan-e-ia-generativa\" class=\"series-1870\" title=\"Trazabilidad de activos con LoRaWAN e IA generativa\">Trazabilidad de activos con LoRaWAN e IA generativa<\/a><\/div><p>Una vez solventados los asuntos de la infraestructura LoRaWAN, era<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"advanced_seo_description":"","jetpack_seo_html_title":"","jetpack_seo_noindex":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[13],"tags":[1872,1873,133,412,489,745,789,829,839,998],"series":[1870],"class_list":["post-11297","post","type-post","status-publish","format-standard","hentry","category-informatica","tag-ab01","tag-ab02s","tag-arduino","tag-chirpstack","tag-cubecell","tag-gps","tag-heltec","tag-ide","tag-impresora-3d","tag-lorawan","series-trazabilidad-de-activos-con-lorawan-e-ia-generativa"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11297","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=11297"}],"version-history":[{"count":2,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11297\/revisions"}],"predecessor-version":[{"id":11304,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11297\/revisions\/11304"}],"wp:attachment":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=11297"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=11297"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=11297"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=11297"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}