{"id":2632,"date":"2020-08-24T21:33:40","date_gmt":"2020-08-24T21:33:40","guid":{"rendered":"http:\/\/blog.rastersoft.com\/?p=2632"},"modified":"2020-10-21T12:13:45","modified_gmt":"2020-10-21T12:13:45","slug":"a-ritmo-de-conga-12","status":"publish","type":"post","link":"https:\/\/blog.rastersoft.com\/?p=2632","title":{"rendered":"A ritmo de conga (12)"},"content":{"rendered":"\n<p>Hasta ahora he estado utilizando la web app para controlar mi aspiradora robot tanto desde el ordenador como desde el m\u00f3vil. El problema es que es bastante pesado, en el m\u00f3vil, tener que:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Abrir el navegador<\/li><li>Abrir una pesta\u00f1a en blanco<\/li><li>Escribir la IP del servidor n\u00famero a n\u00famero<\/li><\/ul>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"192\" style=\"aspect-ratio: 320 \/ 192;\" width=\"320\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/06\/working.mp4\"><\/video><\/figure>\n\n\n\n<p>As\u00ed que decid\u00ed que ten\u00eda que hacer una app para Android. Obviamente no me iba a matar repitiendo todo el trabajo que ya hab\u00eda hecho en JavaScript, as\u00ed que la soluci\u00f3n obvia era hacer una aplicaci\u00f3n que simplemente tuviese un <a rel=\"noreferrer noopener\" href=\"https:\/\/developer.android.com\/guide\/webapps\/webview\" target=\"_blank\">WebView<\/a> (que no es m\u00e1s que un widget con un navegador web completo) y que cargase autom\u00e1ticamente la web app de OpenDo\u00f1ita autom\u00e1ticamente cada vez que se abriese. As\u00ed cualquier cambio que hiciese a la web app aparecer\u00eda autom\u00e1ticamente tambi\u00e9n en la app de Android.<\/p>\n\n\n\n<p>Sin embargo, antes que eso ten\u00eda que resolver un problema nada trivial: \u00bfc\u00f3mo saber la IP del servidor de OpenDo\u00f1ita? S\u00ed, en mi casa se cual es, pero obviamente si quiero que otras personas puedan usarlo, no es plan de tener que poner la IP \u00aba mano\u00bb. Adem\u00e1s \u00bfy si el DHCP hace de las suyas en un reinicio y cambia la IP?<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"162\" style=\"aspect-ratio: 288 \/ 162;\" width=\"288\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/08\/search-1.mp4\"><\/video><\/figure>\n\n\n\n<p>La soluci\u00f3n vino de la mano de <a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/Universal_Plug_and_Play\" target=\"_blank\">uPnP<\/a>. Se trata de un est\u00e1ndar para que dispositivos multimedia puedan anunciarse en una red dom\u00e9stica, y que otros dispositivos puedan identificarlos y comunicarse con ellos de manera estandarizada. Tambi\u00e9n sirve (y ser\u00e1 lo que les suene a muchos) para poder abrir puertos externos en el router cuando usamos <a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/Network_address_translation\" target=\"_blank\">NAT<\/a>.<\/p>\n\n\n\n<p>El protocolo uPnP es, en esencia, relativamente sencillo: se utiliza la direcci\u00f3n <a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/Multicast\" target=\"_blank\">multicast<\/a> 239.255.255.250 y el puerto 1900 para enviar y recibir paquetes <a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/User_Datagram_Protocol\" target=\"_blank\">UDP<\/a>. As\u00ed, si un dispositivo quiere anunciar que cumple con el est\u00e1ndar uPnP, emitir\u00e1 una serie de paquetes <strong>NOTIFY<\/strong> a dicha direcci\u00f3n y puerto, y aquellos dispositivos interesados estar\u00e1n suscritos a ella para detectar dichos mensajes. Otra manera es que un dispositivo env\u00ede a dicha direcci\u00f3n y puerto un paquete <strong>M-SEARCH<\/strong>, y los dispositivos responder\u00e1n cada uno indicando sus capacidades y dem\u00e1s.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"148\" style=\"aspect-ratio: 220 \/ 148;\" width=\"220\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/08\/speaker.mp4\"><\/video><\/figure>\n\n\n\n<p>Por supuesto, cuando entramos en detalles nos encontramos con que el protocolo es mucho m\u00e1s rico y complejo de lo que parece. Pero afortunadamente existe el m\u00f3dulo de python <a rel=\"noreferrer noopener\" href=\"https:\/\/pypi.org\/project\/iot-upnp\/\" target=\"_blank\">iot-upnp<\/a>, que permite de manera sencilla configurar un dispositivo como servidor uPnP. Precisamente ella ha sido el motivo de que convirtiese el c\u00f3digo a <a rel=\"noreferrer noopener\" href=\"https:\/\/docs.python.org\/3\/library\/asyncio-task.html\" target=\"_blank\">asyncio<\/a>. B\u00e1sicamente basta con asignar un UUID y un par de cosas m\u00e1s a un diccionario, y nuestro programa ya es un servidor uPnP y responde a los anuncios. Este c\u00f3digo est\u00e1 a\u00f1adido en la \u00faltima versi\u00f3n del <a href=\"https:\/\/gitlab.com\/rastersoft\/opendonita\" target=\"_blank\" rel=\"noreferrer noopener\">servidor OpenDo\u00f1ita<\/a>.<\/p>\n\n\n\n<p>La segunda parte es conseguir que la app de Android pida a los dispositivos uPnP que se identifiquen. En este caso no me compliqu\u00e9 y me limit\u00e9 a crear un socket UDP y enviar directamente una petici\u00f3n uPnP de tipo M-SEARCH, que es un paquete con estos datos:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted mycode\">M-SEARCH * HTTP\/1.1\\r\\n\nHOST: 239.255.255.250:1900\\r\\n\nMAN: \\\"ssdp:discover\\\"\\r\\n\nMX: 2\\r\\n\nST: upnp:rootdevice\\r\\n\\r\\n<\/pre>\n\n\n\n<p>Con eso recibir\u00e9 un paquete UDP por cada dispositivo ra\u00edz uPnP directamente al mismo socket desde el que envi\u00e9 el paquete. Y como s\u00f3lo quiero conocer la IP y nada m\u00e1s, lo \u00fanico que necesito hacer es esperar a que aparezca uno que contenga el UUID que env\u00eda mi web app, y ese ser\u00e1.<\/p>\n\n\n\n<p>Por supuesto, las cosas no son tan sencillas, pues la clase de sockets, <a rel=\"noreferrer noopener\" href=\"https:\/\/developer.android.com\/reference\/java\/net\/DatagramSocket\" target=\"_blank\">DatagramSocket<\/a>, es s\u00edncrona, lo que significa que no la podemos utilizar desde el bucle principal de Android sino que necesitamos crear un <em>thread<\/em>. Para ello utilic\u00e9 una clase que extiende <a rel=\"noreferrer noopener\" href=\"https:\/\/developer.android.com\/reference\/android\/os\/AsyncTask\" target=\"_blank\">AsyncTask<\/a>. Aunque es un m\u00e9todo obsoleto, lo es a partir de la API 30, la cual fue lanzada ayer como quien dice (pertenece a Android 11), por lo que prefiero utilizarla y garantizar que mi c\u00f3digo va a funcionar en m\u00f3viles antiguos. Ahora simplemente implemento el m\u00e9todo <em>doInBackground()<\/em> y dentro hago un bucle en el que env\u00edo el paquete anterior, me pongo a esperar respuestas con un timeout de 2 segundos (que coincide con el valor de MX de mi petici\u00f3n), y cuando salte \u00e9ste, si no he conseguido la IP, repito el proceso. Pero si en alguno de los paquetes ven\u00eda el UUID correcto, salgo del bucle y retorno del m\u00e9todo. Es entonces cuando se ejecutar\u00e1 el m\u00e9todo <em>onPostExecute()<\/em> recibiendo como par\u00e1metro el valor que devolv\u00ed en <em>doInBackground()<\/em>. Lo interesante es que mientras que \u00e9sta \u00faltima se ejecutaba en otro thread, <em>onPostExecute()<\/em> se ejecuta en el mismo thread desde el que se cre\u00f3 el objeto, o sea, desde el bucle principal en mi caso, con lo que ah\u00ed podr\u00e9 llamar al m\u00e9todo <em>loadUrl()<\/em> del WebView para que cargue la p\u00e1gina.<\/p>\n\n\n\n<p>La otra cuesti\u00f3n importante es poder capturar el bot\u00f3n de <em>Atras<\/em> de Android para poder ocultar la pantalla de configuraci\u00f3n si se pulsa, pero salir de la app si se pulsa desde la pantalla principal. Para ello sobreescribo en la actividad principal el m\u00e9todo <em>onBackPressed()<\/em>, que es el que se llama cuando el usuario pulsa el bot\u00f3n, y dentro de \u00e9l utilizo <em>evaluateJavascript()<\/em> para llamar a la funci\u00f3n <em>back_android()<\/em> de la web app. Esta funci\u00f3n har\u00e1 lo que tenga que hacer y devolver\u00e1 el valor 0 si no se debe hacer nada, o 1 si se debe salir de la aplicaci\u00f3n.<\/p>\n\n\n\n<p>Y el resto no es m\u00e1s que el t\u00edpico c\u00f3digo para gestionar el <a rel=\"noreferrer noopener\" href=\"https:\/\/developer.android.com\/guide\/components\/activities\/activity-lifecycle\" target=\"_blank\">ciclo de vida de una aplicaci\u00f3n de Android<\/a>.<\/p>\n\n\n\n<p>El c\u00f3digo est\u00e1 disponible en mi <a rel=\"noreferrer noopener\" href=\"https:\/\/gitlab.com\/rastersoft\/opendonita_android\" target=\"_blank\">repositorio gitlab de OpenDo\u00f1ita para Android<\/a>.<\/p>\n\n\n\n<p><a href=\"https:\/\/blog.rastersoft.com\/?p=2642\">Parte 13<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hasta ahora he estado utilizando la web app para controlar mi aspiradora robot tanto desde el ordenador como desde el m\u00f3vil. El problema es que es bastante pesado, en el m\u00f3vil, tener que: Abrir el navegador Abrir una pesta\u00f1a en blanco Escribir la IP del servidor n\u00famero a n\u00famero As\u00ed que decid\u00ed que ten\u00eda que &hellip; <a href=\"https:\/\/blog.rastersoft.com\/?p=2632\" class=\"more-link\">Seguir leyendo <span class=\"screen-reader-text\">A ritmo de conga (12)<\/span> <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,16,5,6],"tags":[],"class_list":["post-2632","post","type-post","status-publish","format-standard","hentry","category-cacharreo","category-opendonita","category-programacion","category-trucos"],"_links":{"self":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2632","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2632"}],"version-history":[{"count":6,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2632\/revisions"}],"predecessor-version":[{"id":2654,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2632\/revisions\/2654"}],"wp:attachment":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2632"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2632"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2632"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}