Jupyter notebook en Raspberry Pi con Docker

Actualizado el 31 de Octubre de 2021

Docker

Introducción

Últimamente he estado usando Azure notebooks para mis notas en Jupyter pero parece que finalmente Microsoft ha decidido abandonarlo y da varias alternativas para la migración, pero ninguna me ha terminado de convencer, ya sea por estar atado a otro componente de Azure o por ser de pago, así que decidí incluir Jupyter dentro de una de mis Raspberry.

Uno de los problemas es la mala compatibilidad de Jupyter con Git, apenas existen extensiones que sean fiables así que decidí usar JupyterLab con su extensión de Git que es bastante completa.

Finalmente continuando con lo que parece que será la senda a seguir en un futuro cercano he optado a hacerlo en Docker, principalmente porque así lo puedo mover cómodamente entre mis Raspberry sin tener que instalarlo todo de nuevo (bueno, para lo que se supone que es Docker).

Estos son los pasos a seguir, en este caso use una Raspberry Pi 3 con 1 GB de RAM para comprobar el rendimiento, aunque Docker tiene más sentido en una Raspberry Pi 4 con al menos 4GB de RAM.

Configuración del sistema

Es recomendable leer este enlace antes de seguir, da instrucciones e ideas para instalar Docker.

DockerFile

Este es el fichero Dockerfile necesario para crear la imagen, vamos a usar como imagen base la versión ARM de Debian, posiblemente con Alpine el tamaño de la imagen sería menor (actualmente 1.2GB) pero puede haber algunos problemas de rendimiento.

# 0. Image and labels
FROM debian:latest
LABEL "guru.raraavis.creator"="blog@raraavis.guru"
LABEL "guru.raraavis.version"="1.1.0"
LABEL "guru.raraavis.release-date"="31/10/2021"
LABEL "guru.raraavis.description"="Raspberry Pi image with JupyterLab"

# 1. Environment vars
ARG DEBIAN_FRONTEND=noninteractive
ARG NODE_OPTIONS=--max-old-space-size=768
ARG TZ=Europe/Madrid
ENV NODE_VERSION=16
ENV JUPYTER_ID=1000
ENV TZ=$TZ

# 2. Install packages
# 2.1 Update system and install necessary packages
RUN     apt-get update && \
        apt-get install -y --no-install-recommends \
        python3-dev \
        python3-venv \
        libatlas-base-dev \
        libblas-dev \
        liblapack-dev \
        python-dev \
        gfortran \
        tzdata \
        software-properties-common \
        sudo \
        gosu \
        bzip2 \
        git \
        dumb-init \
        curl && \
        curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - && \
        apt-get install -y --no-install-recommends nodejs && \
        rm -rf /var/lib/apt/lists/* && \
        rm -rf /tmp/*
# 2.2 Install pip
ADD     https://bootstrap.pypa.io/get-pip.py get-pip.py
RUN     python3 get-pip.py
RUN     python3 -m pip config --global set global.extra-index-url https://www.piwheels.org/simple
# 2.3 Install pip packages and Jupyter
RUN     python3 -m pip install --upgrade \
        conda \
        virtualenv \
        ipykernel \
        jupyter \
        jupyterlab \
        jupyterlab-git \
        jupyter_contrib_nbextensions \
        autopep8

RUN     jupyter lab build
RUN     jupyter contrib nbextension install --system

# 3. Add jupyter user
RUN     adduser --uid $JUPYTER_ID --disabled-password --gecos '' jupyter
RUN     adduser jupyter sudo
RUN     echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

# 4. Install Miniconda
WORKDIR /home/jupyter
ADD --chown=jupyter     http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-armv7l.sh Miniconda3.sh
RUN     chmod 755 Miniconda3.sh
RUN     md5sum Miniconda3.sh
RUN     ./Miniconda3.sh -b -p /home/jupyter/miniconda3
RUN     ./miniconda3/bin/conda config --add channels rpi
RUN     rm ./Miniconda3.sh

# 5. Configure container
RUN     mkdir -p /home/jupyter/notebooks
RUN     chown -R jupyter:jupyter /home/jupyter
ONBUILD SHELL   ["/bin/bash","-c"]
VOLUME /home/jupyter/notebooks
EXPOSE 8888
USER jupyter

# 6. Execute jupyter
CMD ["jupyter", "lab", "--port=8888", "--no-browser", "--ip=0.0.0.0", "--allow-root", "--notebook-dir=/mnt/jupyter/notebooks", "--ServerApp.token=", "--ServerApp.password="]

Variables de entorno (1)

El paquete tzdata cuando se instala necesita especificar una zona horaria, así que establecemos la variable de entorno TZ, si se establece en tiempo de ejecución indicará la zona horaria a usar para configurar el sistema, se puede volver a configurar usando dpkg-reconfigure, tzdata también necesitará establecer DEBIAN_FRONTEND=noninteractive para evitar preguntas durante la instalación.
Enlazamos ARG con ENV, ya que ARG solo se puede usar en tiempo de compilación la enlazamos con ENV para que se pueda establecer también en tiempo de ejecución.
NODE_OPTIONS se usará más adelante para establecer parámetros en NodeJs, principalmente en el uso de la memoria, ya que a no ser que la Raspberry tenga más de 2GB fallará durante la creación de la extensión de Git, para una RPI4 se podría eliminar o ampliar este parámetro.
Existe también la opción de parametrizar que versión de Node se quiere instalar -necesario para JupyterLab- por defecto la 16 que es la última disponible actualmente.
La variable JUPYTER_ID se usa para indicar que id de usuario tendrá el usuario Jupyter que luego crearemos, esto es muy útil para poder establecer permisos en el host y evitar algunos problemas.

Instalar paquetes (2)

El proceso de instalación consta de un solo comando RUN para evitar crear capas intermedias. También crea e instala la extensión de Git, hay que tener en cuenta que no existe una forma oficial de generar la extensión y luego instalarla, de ser así una opción habría sido crearla en un host y luego copiarla en otro.
También se incluyen varias librerías de sistema, muchas de ellas son necesarias para instalar ciertos paquetes Python (principalmente librerías científicas) así que es bueno tenerlas ya instaladas.

Actualizar el sistema e instalar Jupyter (2.1)

Los comandos básicos para actualizar el sistema e instalar Jupyter con sus dependencias, se usa –no-install-recommends y finalmente se elimina de la cache los paquetes para aligerar el tamaño del fichero Docker.

Instalar pip (2.2)

Aquí descargamos e instalamos pip, también añadimos el repositorio de paquetes Python piwheels, este repositorio contiene muchos de los paquetes Python preparados para Raspberry lo que ahorra mucho tiempo de compilación e instalación, también indicamos que este repositorio sea global para que cualquier usuario o entorno virtual se beneficie del mismo.

Instalar librerías Python y Jupyter (2.3)

Aquí se instalan todos los paquetes Python necesarios para JupyterLab incluyendo algunos que son útiles -como virtualenv- extensiones a JupyterLab -como la propia de Git– y finalmente el propio Jupyterlab, después solo hay que llamar al comando build. Aprovechamos también para instalar algunas extensiones útiles para JupyterLab.

Agregar usuario Jupyter (3)

Ahora creamos el usuario jupyter que usaremos a partir de ahora, lo configuramos sudo y configuramos la carpeta de trabajo.

Instalar Miniconda (4)

En este paso vamos a instalar Miniconda para el usuario Jupyter, se hace una descarga e instalación desatendida dentro de la carpeta que hemos creado previamente. Miniconda instala principalmente el gestor de paquetes Conda y el resto de paquetes habría que instalarlos por separado, hay que tener en cuenta que el soporte de Conda para Raspberry no es muy bueno.
Finalmente se agrega el canal (repositorio) de rpi a conda para poder descargar paquetes orientados a Raspberry Pi (similar a como hemos hecho antes para pip).
Al instalar Conda de manera desatendida no se incluyen los binarios de Conda en el PATH por lo tanto para usar los binarios de conda hay que ubicarse en la carpeta bin de la carpeta miniconda como se ve al usar el comando conda config, esta opción la he preferido para no tener que usar la versión de Python de Conda que generalmente es inferior a la instalada en el sistema.

Contenedor (5)

Establecemos una carpeta de trabajo donde crearemos los notebooks y la usaremos también para clonar repositorios, establecemos los permisos adecuados para el usuario Jupyter.

También usamos ONBUILD para indicar que las imágenes que hereden de esta usarán bash como shell, esto es importante porque sino algunos comandos como source muy usados para crear entornos virtuales no estarán disponibles.

Añadimos la configuración de los puertos y la carpeta que se usará como volumen para mapear con el host que es donde residen los cuadernos.

Ejecutar Jupyter

Establecemos el comando que arrancará Jupyter, algunas consideraciones:

  • –no-browser: para evitar abrir el navegador al arrancar.
  • –allow-root: permite ejecutar Jupyter como root.
  • –notebook-dir: carpeta raíz de Jupyter, es decir de la que leerá los ficheros, que a su vez es la misma que usamos para Git.
  • –NotebookApp.token y NotebookApp.password lo establecemos con cadenas vacías para indicar que no queremos usar autenticación, esto para un entorno particular como suele ser el de una Raspberry es muy adecuado, en entornos compartidos quizás tenga más sentido no usar estos valores y establecerlos a través del archivo de configuración.

Compilación

Ejecutamos con el siguiente comando para compilar, previamente hacemos una limpieza para evitar posibles errores (errores GPG con las claves de los orígenes de los paquetes por ejemplo). Es posible que de algún error al compilar por falta de memoría, quitar algunos servicios o programas activos para ganar memoria y reintentar puede ayudar.

docker image prune -f
docker image build --tag user/image_name .

Ejecución

Una vez terminada la compilación con el siguiente comando arrancamos la imagen:

docker container run --init --publish 8888:8888 --detach --volume /home/pi/jupyter:/home/jupyter/notebooks user/image_name

Estamos mapeando el puerto 8888 al mismo en puerto en el host y también estamos mapeando la carpeta en Rasperry como un volumen en el host en la carpeta home del usuario pi, en id_imagen indicamos el id de la imagen a ejecutar. El parámetro –init es un valor recomendado para Jupyter que le proporciona más estabilidad.
Se pueden establecer también las variables de entorno con –env para JUPYTER_ID, NODE_VERSION o TZ o se pueden dejar los valores por defecto.

Push & Pull

Para subir la imagen solo tenemos que ejecutar el siguiente comando e introducir nuestras credenciales de Docker.

docker login

Para subir la imagen usamos la información del repositorio que hemos creado, normalmente usamos como user/name lo mismo que hayamos especificado en el tag que hemos creado con el comando docker build.

docker push user/image_name

Y para descargarlo en otra máquina simplemente hacemos:

docker pull user/image_name

Aunque si lo invocamos directamente con docker run tendrá el mismo efecto.

Configurar Git

Si arrancamos un navegador y vamos a la dirección de la Raspberry podremos ver el entorno de JupyterLab, vamos a configurar Git desde aquí.

La mayoría de las tareas básicas se pueden hacer desde los menús pero para comandos más avanzados podemos abrir un terminal y lanzar comandos desde ahí o para verificar que todo ha sido correcto, por ejemplo vamos a configurar Git con nuestro usuario e email, (nota, pegar se realiza con mayus+Insert).

git config user.email "coyote@acme.com"
git config user.name "El Coyote"

Para evitar que nos pregunte usuario y contraseña para los commit vamos a configurarlo también, desde el terminal hacemos:

git config credential.helper store
git pull

Introducimos el usuario y contraseña cuando lo pregunte, con esto quedarán almacenadas y no lo preguntará para el resto de acciones que hagamos desde la consola o desde la interfaz.

Ahora para clonar el repositorio usamos la interfaz, desde el menú Git -> Clone a Repository introducimos la URL del repo, nos preguntará por unas credenciales que tendremos que tener previamente configuradas en el servidor Git.

Esto nos descargará el código en la carpeta que hemos indicado, ahora podemos probar a crear cualquier archivo y realizar un commit, en la parte de la izquierda pulsemos el icono de Git y veremos los archivos modificados, seleccionamos los que queremos subir y en la parte inferior escribimos un resumen y una descripción opcional, a continuación pulsamos Commit, como ya hemos configurado el nombre y el correo en Git el commit no dará ningún error, sino que saldrá una ventana pidiendo esos datos.

Para hacer Push hay dos formas, se puede hacer directamente desde esta misma ventana usando el icono o desde el menú Git -> Push to remote.

Desde esta misma ventana también podemos cambiar de ramas o ver el historial.

(Opcional) Proxy Apache

Podemos usar Apache como proxy para acceder y así configurar estas URL por algo más fácil de recordar, para ello en Apache creamos un virtual host que nos haga de proxy con el servidor de JupyterLab, creamos un fichero en la carpeta /etc/apache2/sites-available/005-jupyterlab.conf con el siguiente contenido, obviamente deberemos tener un sistema DNS que nos resuelva correctamente el nombre jupyterlab, vamos a suponer que el servidor está en la dirección 192.168.1.4

Tenemos que habilitar algunos módulos, para permitir que funcionen los websockets y cors.

a2enmod headers
a2enmod proxy
a2enmod proxy_http
a2enmod proxy_wstunnel
a2enmod headers

Y ahora configuramos el servidor virtual

<VirtualHost spotify:80>
  ServerName jupyterlab.domain
  SererAlias jupyterlab
  Header set Access-Control-Allow-Origin "*"
  ProxyPreserveHost On
  ProxyPass / http://192.168.1.4:8888/
  ProxyPassReverse / http://192.168.1.4:8888/
  <Location "terminals/websocket">
      ProxyPass "ws://192.168.1.4:8888/terminals/websocket"
  </Location>
  <Location "/api/kernels/">
      ProxyPass "ws://192.168.1.4:8888/api/kernels/"
  </Location>
  CustomLog /var/log/apache2/jupyterlab.log combined
  ErrorLog /var/log/apache2/jupyterlab.error.log
</VirtualHost>

Para habilitar el sitio ejecutamos:

sudo a2ensite 005-jupyterlab.conf

Referencias

Configurar tzdata en Docker
Jupyter Notebook Dockerfile

Jupyter notebook en Raspberry Pi con Docker

Compilaciones deterministas

Una buena característica que se puede añadir a una librería además de SourceLink o crear la documentación es configurar la compilación como determinista.

Esto quiere decir que compilar ensamblados bajo las mismas condiciones de entrada producirá el mismo binario equivalente byte a byte. Por condiciones de entrada se entienden:

  • La secuencia de parámetros de entrada al compilador (flags).
  • Los contenidos del archivo de respuesta del compilador .rsp.
  • La versión exacta del compilador usado así como de los ensamblados referenciados.
  • Ruta completa del directorio actual (se pueden abreviar usando relativas)
  • Contenidos binarios de todos los ficheros pasados al compilador
    • Código fuente
    • Ensamblados referenciados
    • Módulos referenciados
    • Recursos
    • El nombre del fichero de claves (strong name)
    • Archivos de respuesta @
    • Analizadores
    • Conjuntos de reglas
    • «archivos adicionales» que puedan ser usados por analizadores
  • La cultura actual (se refiere a la cultura en la que se producen los mensajes de excepción y diagnóstico)
  • La codificación por defecto (o la actual) si la codificación no se ha especificado
  • La existencia, o no existencia, y contenidos de archivos en las rutas de búsqueda del compilador ( p.ej /lib, /recurse)
  • La plataforma CLR en la cual el compilador funciona (esto se nota por ejemplo al calcular precisiones en algunos tipos numéricos)
  • El valor de %LIBPATH% ya que esto puede afectar la carga del analizador de dependencias
  • Ahora mismo el compilador también depende de la hora del día y números aleatorios generados para GUID que no es determinístico si no se especifica /deterministic.

Permitir que el binario resultante sea el mismo proporciona varias ventajas por ejemplo para mecanismos de caché y optimización de test. También asegura que tanto la herramienta de compilación como la forma en que se ha compilado es consistente y verificable tanto por si misma con respecto al código fuente utilizado.

De igual forma en compilaciones incrementales incrementa la rapidez de compilación al compilar solo algunas partes, por ejemplo los ficheros de salida que ya están actualizados respecto a los ficheros de entrada no son ejecutados.

En el mundo DevOps proporciona ciertas ventajas para saber que pasos de compilación que dependen de cambios en un binario tienen que ser ejecutados.

La opción determinista está habilitada por defecto desde VisualStudio 2017, si se quiere habilitar para una versión anterior o deshabilitarla, tendríamos que editar el fichero .csproj.

<PropertyGroup>
  <Deterministic>true</Deterministic>
</PropertyGroup>

Es posible que se quiera deshabilitar ya que la opción determinista no permite establecer un número de versión autoincremental (*) por razones obvias este valor cambia en cada compilación por lo que no es determinista.

Este valor también se puede pasar al compilador a través del flag /deterministic

DevOps

Los PDBs contienen las rutas de los ficheros que se usarán para depurar, esto en entornos locales no es un problema, pero en entornos DevOps puede ser un problema al no tener acceso a esas rutas o incluso a la máquina, para esto existe la directiva <DeterministicSourcePaths/> que debe ser establecida en true, esto hace que la compilación sea completamente determinista tanto en local como en entornos DevOps.

Configuración

Con todo lo anterior lo que habría que hacer es establecer las directivas <Deterministic/> y <ContinuousIntegrationBuild/> con los valores comentados.

Además hay que incluir la directiva <EmbedUntrackedSources /> para que se incluya también el código generado por el compilador como AssemblyInfo.cs, hay que tener en cuenta que si ya está configurado para SourceLink entonces esta directiva ya estará incluida.

Además para entornos DevOps hay que añadir una configuración adicional.

Azure DevOps

<PropertyGroup Condition="'$(TF_BUILD)' == 'true'">
  <Deterministic>True</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

GitHub

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
  <Deterministic>True</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

Si se está usando un target de SDK inferior a 3.1.300 hará falta agregar un fichero Directory.Build.targets con la siguiente configuración, también hará falta para librerías .NetStandard (por lo menos hasta la versión 2.1 incluida).

<Project>
  <PropertyGroup>    
    <TargetFrameworkMonikerAssemblyAttributesPath>
$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 
    </TargetFrameworkMonikerAssemblyAttributesPath>
  </PropertyGroup>
  <ItemGroup>
    <EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
  </ItemGroup>
</Project>

Si se está usando Coverlet entonces habrá que usar la siguiente configuración para no tener problemas durante la integración continua.

<Project>
  <PropertyGroup>    <TargetFrameworkMonikerAssemblyAttributesPath>$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))</TargetFrameworkMonikerAssemblyAttributesPath>
  </PropertyGroup>
  <ItemGroup>
    <EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
  </ItemGroup>
  <ItemGroup>
    <SourceRoot Include="$(NuGetPackageRoot)" />
  </ItemGroup>
  <Target Name="CoverletGetPathMap"
          DependsOnTargets="InitializeSourceRootMappedPaths"
          Returns="@(_LocalTopLevelSourceRoot)"
          Condition="'$(DeterministicSourcePaths)' == 'true'">
    <ItemGroup>
      <_LocalTopLevelSourceRoot Include="@(SourceRoot)" Condition="'%(SourceRoot.NestedRoot)' == ''"/>
    </ItemGroup>
  </Target> 
</Project>

Mapear rutas

Si no se compila en formato portable es posible que las rutas dentro de los ficheros PDB no sean correctas o cambien, es posible mapear la ruta física respecto a la ruta que aparece en los PDB mediante la directiva PathMap, como:

<PropertyGroup>
  <Deterministic>true</Deterministic>
  <PathMap>$(EnlistmentRoot)=C:\</Features>
</PropertyGroup>

Siendo EnlistmentRoot la variable que apunta al raíz del código fuente, el parámetro del compilador sería:

-pathmap:path1=sourcePath1,path2=sourcePath2

Verificar

Para probarlo localmente se puede compilar pasando la propiedad TF_BUILD al compilador (si es orientado a Azure DevOps), por ejemplo:

dotnet build /p:TF_BUILD=true

Para comprobar que todo está correcto, después de compilar empaquetamos, usando por ejemplo la opción pack del proyecto y al abrirlo con NuGet Package Explorer deberíamos ver el tick verde en Deterministic.

Esto tiene una ventana añadida, podemos verificar que el paquete es correcto antes de aplicarlo a un pipe DevOps.

Referencias

Ventajas de compilaciones deterministas
Compilaciones deterministas en Roslyn
Deterministic source paths
Compilaciones incrementales
Entradas deterministas
Problemas con versionado automático
Configuración Deterministic Build
Configuración PathMap
Opción del compilador (-pathmap)
Opción del compilador (-deterministic)
Portable PDB

Compilaciones deterministas

USB externo en Raspberry Pi

Actualizado el 22 de Noviembre de 2020

Una forma rápida y sencilla de añadir un disco duro externo por USB y asociarlo a una carpeta, primero como siempre actualizar:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade

Antes de empezar tenemos que tener en cuenta que hay unidades USB con alimentación externa y otros sin ella, con alimentación externa no habrá problemas pero debido a las limitaciones de la RaspberryPi en términos de alimentación es posible que no pueda arrancar un disco que requiera alimentación por USB, así que lo primero será encontrar las unidades que tenemos conectadas.

sudo blkid

Si no vemos nuestra unidad y no tiene alimentación externa podemos probar a hacer los siguientes pasos:

Archivos sin fuente de alimentación externa

Editamos el fichero config.txt

sudo nano /boot/config.txt

Agregamos esta línea al final del fichero

max_usb_current=1

Reiniciamos

sudo reboot

Ahora volveríamos a probar el comando blkid y ver si aparece nuestra unidad, de no ser así podemos probar lo siguiente.

Instalamos wringPi desde aquí y comprobamos si tenemos el pin GPIO 38 activo que es el que nos permitirá doblar la potencia de la RaspberryPi y alimentar dispositivos externos:

gpio -g read 38

Si devuelve 1 lo tenemos habilitado, si vemos un 0 lo habilitamos con:

gpio -g write 38 1

En caso de no funcionar hay que usar una fuente de alimentación de más calidad, para verificar si el problema es la fuente de alimentación al arrancar la RaspberryPi tenemos que fijarnos en la cantidad de iconos de frambuesa que veamos, cuantos más mejor y sobre todo fijarnos de no ver ningún símbolo de un rayo lo que querría decir que no hay potencia suficiente.

Configurando unidad externa

Cuando veamos nuestra unidad usamos el siguiente comando:

sudo lsblk -o UUID,NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL,MODEL

Veremos un listado de unidades conectadas, una buena pista suele ser mirar la columna LABEL y MODEL también por el tamaño podemos averiguar cual es nuestra unidad.

La columna FSTYPE muestra el tipo del sistema de archivos, hay que tener instalado el driver adecuado para que se pueda leer, por ejemplo para exFAT:

sudo apt install exfat-fuse

O para NTFS (poder leer y escribir):

sudo apt install ntfs-3g

Tenemos que crear la carpeta en la que queramos mapear la unidad de red, por ejemplo:

mkdir /media/usb/acme

Ahora localizamos el valor UUID (la primera columna) lo guardamos y editamos el fichero /etc/fstab, incluyendo esta línea:

UUID=12345678ABCDEFGH /media/usb/acme ntfs-3g default,uid=pi,gid=pi,nofail 0 0

Donde UUID es el valor que teníamos guardado, luego tenemos la ruta a la carpeta que hemos creado /media/usb/acme, ntfs-3g es el driver necesario para leer la unidad, en este caso ntfs-3g porque es una unidad con sistema de archivos ntfs, uid es el nombre de usuario que se usará para acceder a la unidad (en este ejemplo el usuario pi) y gid es el nombre del grupo con permiso para acceder (en este caso el grupo pi), nofail sirve para indicar que si la unidad no se puede montar en el arranque no se produzca un error.

En este caso hemos usado el usuario pi que viene por defecto en la instalación, si quisiéramos usar otro haríamos:

sudo adduser acme

Igual puede interesar crear un usuario solo para validarse por red pero no para poder iniciar sesión como un usuario normal, algo parecido a una cuenta de servicio.

sudo adduser -shell /bin/false --no-create-home acme

Ahora solo nos queda montar la unidad USB con:

sudo mount -a

Si accedemos a la carpeta que hemos creado veremos el contenido del disco, también podemos compartirla por unidad de red siguiendo los detalles de este post.

Referencias

https://www.htpcguides.com/power-2-5-hard-drive-with-raspberry-pi-b/
https://www.raspberrypi.org/documentation/configuration/external-storage.md
https://www.raspberrypi.org/forums/viewtopic.php?t=238095

USB externo en Raspberry Pi

Configuración servidor de impresión y escaner con Raspberry Pi

Con una impresora antigua y una Raspberry es posible crear un servidor de impresión, estos son los pasos:

Primero como siempre actualizar Raspberry.

sudo apt-get update
sudo apt-get upgrade

Ahora instalamos CUPS, el servidor de impresión por excelencia de UNIX.

sudo apt-get install cups

Esto nos creará un grupo de administración de impresoras llamado lpadmin, agregamos el usuario pi a este grupo, se puede elegir cualquier usuario y cuando se acceda a través de web será este el usuario que se tendrá que validar.

sudo usermod -a -G lpadmin pi

Tenemos que habilitar el acceso remoto más allá de localhost, para que se puedan administrar las impresoras desde otra máquina en la red de área local.

sudo cupsctl --remote-any
sudo /etc/init.d/cups restart

Ahora podemos probar la web, usando la dirección de la Raspberry y el puerto 631 que es el de defecto, si quisiéramos cambiar el puerto se puede editar desde /etc/cups/cupsd.conf en la línea marcada como Port.

Si no tenemos instalado samba, estos serían los pasos rápidos:

sudo apt-get install samba

Ahora hay que editar el archivo de configuración de samba para agregar la entrada de la impresora, realmente esta entrada ya está creada solo hay que verificar que sea igual que esta:

# CUPS printing.  
[printers]
comment = All Printers
browseable = no
path = /var/spool/samba
printable = yes
guest ok = yes
read only = yes
create mask = 0700

# Windows clients look for this share name as a source of downloadable
# printer drivers
[print$]
comment = Printer Drivers
path = /var/lib/samba/printers
browseable = yes
read only = no
guest ok = no

También tenemos que establecer correctamente el valor del parámetro workgroup y de wins support:

workgroup = my_workgroup
wins support = yes

Ahora reiniciamos samba para hacer efectivos estos cambios.

sudo /etc/init.d/samba restart

Ahora a través de la web accedemos a la sección de administración, desde allí agregamos una impresora con el botón «Add printer» posiblemente haya que acceder con la cuenta que hemos agregado anteriormente al grupo lpadmin, si CUPS es capaz de detectar la impresora la veremos aquí como una impresora local, la seleccionamos y continuamos, cambiamos nombre y descripción o aceptamos las que vienen por defecto y marcamos compartir impresora «Share this printer» con esto ya tendremos una impresora configurada en red.

Esta es una configuración por defecto que funcionará tanto para pcs como para dispositivos móviles.

Mas info:

https://pimylifeup.com/raspberry-pi-print-server/

 

Configuración servidor de impresión y escaner con Raspberry Pi

Crítica velada a TDD

Empezaré diciendo que no he sido nunca un gran practicante de TDD pero sin embargo no critico las pruebas unitarias y si creo que son útiles por multiples razones.

TDD ha calado entre la comunidad poco más o menos como «la solución definitiva» a la calidad del software ganando un montón de adeptos que se han hecho fanáticos de este sistema como es habitual entre la comunidad cuando llega una nueva teoría que gana adeptos muy rapidamente y arrasa haciendo que todo el mundo tenga que aceptarla so pena de parecer un ignorante, aunque hablar de estas olas que más parecen modas que realmente grandes avances sería tema de otro artículo.

TDD surgió como un método para proporcionar una alta calidad a las aplicaciones algo en lo que no pongo dudas ya que todo el desarrollo está centrado en las pruebas, lo cual proporciona el rol dominante a las pruebas por encima de otras consideraciones.

Esto implica varias cosas, TDD impone algunos requisitos de una forma más o menos explícita, por ejemplo cosas obvias como que es necesario tener un buen conocimiento de frameworks de tests unitarios pero tambien otras no tan obvias.

Por ejemplo la refactorización constante, el test siempre va primero esto quiere decir que durante el desarrollo a medida que se implementen nuevos requisitos o cambien algunos habrá que ajustar la aplicación respetando las pruebas unitarias que constituyen la parte principal en este sistema, esto implica continuos cambios en el código para evitar que quede disperso y sin estructura, para mantener la calidad del código a este nivel hace falta tener buenos desarrolladores.

Tambien cualquier sistema de pruebas, no solo TDD, implica tener que hacerlas esto quiere decir una inversión de tiempo, obviamente más pruebas implica más tiempo y cuanto más centrado este el sistema en las pruebas más tiempo conlleva.

Aquí llegamos a un punto clave, la combinación tiempo y recursos, estos dos factores conllevan inevitablemente un aumento del coste y con TDD este coste no es precisamente bajo, necesitas buenos desarrolladores y tiempo para hacer la aplicación.

Habrá quien diga que este sistema no excluye programadores junior que pueden estar realizando otras tareas de bajo nivel y que en el futuro aprenderán un buen sistema de desarrollo, habrá también quien diga que el producto de calidad implica tiempo y no se pueden pedir las cosas «de un día para otro», incluso dirán que un equipo disciplinado en TDD no implica una variación sustancial en tiempo.

La realidad no es esa, no todos los clientes van a aceptar algo así y no todas las empresas pueden permitirse ofertar ese tipo de desarrollos, independientemente de que se vendan como productos de alta calidad.

Tampoco pretendo quitar valor a TDD, simplemente que no sirve para todos los desarrollos, en mi opinión tiene cabida en sectores críticos, por ejemplo: defensa, sanidad, … donde la calidad ha de ser máxima y los errores implican grandes pérdidas ya sean materiales o humanas.

A fin de cuentas la conclusión es, si para que TDD funcione necesitas, aparte de una gran comprensión por parte del cliente,  tiempo y recursos, ¿cómo de malo tendría que ser para que aún asi no funcionase?

Crítica velada a TDD

Configurando Raspberry Pi como servidor de música [Spotify]

Actualizado el 26 de Septiembre de 2022

Para configurar una Raspberry Pi y que funcione como servidor de música usando una cuenta de Spotify usaremos el paquete mopidy. Existen distribuciones ya preparadas con el servidor de música configurado pero en este caso estas instrucciones son para hacerlo sobre una distribución ya existente.

Primero hay que agregar el repositorio de mopidy y para ello primero agregamos las claves:
wget -q -O – https://apt.mopidy.com/mopidy.gpg | sudo apt-key add –

Ahora el repositorio (en este caso para una distribución jessie, habría que cambiar por la que corresponda):
sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/jessie.list

A continuación actualizar e instalar:

sudo apt-get update
sudo apt-get install python-spotify

Ahora instalamos el paquete de Spotify, según la página solo funciona para cuentas premium aunque hay gente que comenta que lo ha usado también para cuentas gratuitas:
sudo python3 -m pip install Mopidy-Spotify

Existen varias formas de arrancarlo, vamos a optar por hacerlo como servicio. Con este comando lo arrancaremos al inicio como servicio:
sudo systemctl enable mopidy

Otra opción para sistemas Debian es:
sudo dpkg-reconfigure mopidy

Estos son los clásicos comandos para arrancar, parar y reiniciar:

sudo service mopidy start
sudo service mopidy stop
sudo service mopidy restart

Ahora solo queda configurar el servidor web, viene instalado por defecto y lo configuramos en la siguiente ruta:
sudo nano /etc/mopidy/mopidy.conf

Creamos una sección [http]

[http]
enabled = true
hostname = ::
port = 6680

Lo único destacable aquí es que el nombre de host es «::» esto indica que escuchará en todas las direcciones IP, no hay validación de usuario así que cualquiera podrá acceder.

Y una sección para el usuario y contraseña de Spotify , los datos de client_id y client_secret se pueden obtener aquí.


username = hola
password = caracola
client_id = XXX
client_secret = YYY

Ahora si intentamos acceder al servidor y al puerto nos dirá que no hay cliente web instalado, existen varias alternativas.

Para poder instalar estos paquetes primero hay que instalar pip, un instalador de paquetes en Python:
sudo apt-get install python-pip

En este caso opto por Mopify y se instala con:
sudo pip install Mopidy-Mopify

Y en el fichero de configuración añadimos

[mopify]
enabled = true
debug = false

Si lo queremos actualizar:
sudo pip install –upgrade Mopidy-Mopify

También existe este otro cliente, más parecido a la interfaz de Spotify y más sencillo:
sudo pip install Mopidy-Iris

Para actualizar
sudo pip install –upgrade Mopidy-Iris

Y como configuración

[iris] 
enabled = true 
country = ES 
locale = es_ES

Es necesario reiniciar el servicio para ver los cambios, al acceder al servidor (http://servidor:6680) veremos el cliente.

Si queremos comprobar primero que el sonido está bien configurado podemos usar este comando:
aplay /usr/share/sounds/alsa/Front_Center.wav

Se pueden cambiar la salida de audio usando raspi-config (System->Audio)  o de forma manual junto con algunas otras opciones con alguno de estos dos métodos dependiendo de la versión:

Versión antigua

Usar HDMI si está conectado, sino jack de 3.5» (si este comando da un error probar con la versión nueva):
sudo amixer cset numid=3 0

Forzar usar solo jack de 3.5:
sudo amixer cset numid=3 1

Forzar usar solo HDMI:
sudo amixer cset numid=3 2

Para controlar el volumen se puede poner estos alias en ~/.bashrc
# Increase volume by 5%
alias volu=’amixer set PCM — $[$(amixer get PCM|grep -o [0-9]*%|sed ‘s/%//’)+5]%’
# Decrease volume by 5%
alias vold=’amixer set PCM — $[$(amixer get PCM|grep -o [0-9]*%|sed ‘s/%//’)-5]%’

O establecer directamente el valor con (donde 90% es el volumen deseado):
amixer sset PCM,0 90%

Version nueva

Para cambiar las opciones de audio hay que editar el fichero .asoundrc que tendremos en el directorio home del usuario, también podemos guardarlo en la ruta /etc/asound.conf en cuyo caso será para todos los usuarios, sino lo tenemos lo creamos:

pcm.!default {
  type asym
  playback.pcm {
    type plug
    slave.pcm "output"
  }
  capture.pcm {
    type plug
    slave.pcm "input"
  }
}

pcm.output {
  type hw
  card 0
}

ctl.!default {
  type hw
  card 0
}

Donde indica card 0 si ponemos card 1 usaremos la salida Headphones sino card 0 usará HDMI después de crear este fichero hay que reiniciar la sesión usando su pi si no lo hemos hemos en /etc/asound.conf

Para controlar el volumen se puede poner estos alias en ~/.bashrc, este ejemplo es para el jack de 3.5» sino se ha establecido la salida de jack usando el fichero anterior este comando dará error (igual para HDMI).
# Increase volume by 5%
alias volu=’amixer set Headphone — $[$(amixer get Headphone|grep -o [0-9]*%|sed ‘s/%//’)+5]%’
# Decrease volume by 5%
alias vold=’amixer set Headphone — $[$(amixer get Headphone|grep -o [0-9]*%|sed ‘s/%//’)-5]%’

Para HDMI haríamos:
# Increase volume by 5%
alias volu=’amixer set HDMI — $[$(amixer get HDMI |grep -o [0-9]*%|sed ‘s/%//’)+5]%’
# Decrease volume by 5%
alias vold=’amixer set HDMI — $[$(amixer get HDMI |grep -o [0-9]*%|sed ‘s/%//’)-5]%’

O establecer directamente el valor con (donde 90% es el volumen deseado) para el jack de 3.5»
amixer sset Headphone,0 90%

O para HDMI
amixer sset HDMI,0 90%

Acceso a la Web

Ahora solo queda acceder a la web con la ip y puerto configurado y elegir el cliente, es posible que haya que echar un vistazo a la configuración del cliente para ver que todo está correcto como dar acceso a spotify.

A veces el refresco de algunas playlist no funciona correctamente y puede ser necesario borrar cookies o recargar la página, se puede probar desde otra máquina para verificar si realmente refresca o no.

Ahora vamos a ver como configurar estas rutas por algo más fácil de recordar, para ello en Apache creamos un virtual host que nos haga de proxy con el servidor de spotify, creamos un fichero en la carpeta /etc/apache2/sites-available/003-spotify.conf con el siguiente contenido, obviamente deberemos tener un sistema DNS que nos resuelva correctamente el nombre spotify, vamos a suponer que el servidor está en la dirección 192.168.1.4

<VirtualHost spotify:80>
  ServerName spotify.domain
  SererAlias spotify
  ProxyPreserveHost On
  ProxyPass / http://192.168.1.4:6680/
  ProxyPassReverse / http://192.168.1.4:6680/
  CustomLog /var/log/apache2/spotify.log combined
  ErrorLog /var/log/apache2/spotify.error.log
</VirtualHost>

Para habilitar el sitio ejecutamos:

sudo a2ensite 004-spotify.conf

Fuentes:

https://github.com/mopidy/mopidy-spotify

https://www.mopidy.com/

Configurando Raspberry Pi como servidor de música [Spotify]

Global Robot Expo 2017

Patrocinado por la FNAC tuve ocasión de echar un vistazo a la Expo Robot que se celebró este año en Madrid, un espacio amplio donde había varias áreas temáticas.

La primera sección dedicada a robótica industrial, robots que se mueven por ejemplo siguiendo una línea o que se dedican a automatizar ciertas tareas.

Img 1

Img 2

Después un paseo por las cada vez más pujantes impresoras 3D, se pueden apreciar creaciones de cierta complejidad.

Img 3

Los robots educativos que se puedan programar siguen apareciendo aunque es verdad que todavía sigue siendo un poco cara la gama alta.

Img 4

Img 5

Una sección dedicada a la domótica casera, por ejemplo el clásico ejemplo de la aspiradora:

Img 6

O un autónomo limpiador de cristales al más puro estilo spiderman:

Img 7

O algo un poco más complejo como un pequeño asistente casero:

Img 8

Ahora un espacio dedicado a los fans de los clásicos:

Img 9

Una de las secciones más grandes estuvo dedicada a los drones habiendo incluso competiciones entre ellos, siendo algunos de ellos de gama profesional necesitando sistemas de manejo en remoto.

Img 10

Img 11

Img 12

Una de las secciones más interesantes aunque no tuviera grandes máquinas a la vista, la UPV con su hyperloop de Elon Musk:

Img 13

Una sección dedicada a armazones asistidos, principalmente orientado a discapacidad, aunque más de uno estaría pensando en ser el próximo IronMan:

Img 14

Img 15

También se pudieron ver ejemplos de asistentes, cada vez un poco mejores:

Img 16

Img 17

Img 18

Interesantes de ver en acción:

Vid 1

O más interactivos:

Vid 2

O un ejemplo muy interesante de una araña artificial, muy logrado:

Vid 3

En general muy entretenido e interesante, habrá que volver el próximo año.

Global Robot Expo 2017

Desplegando en una Raspberry Pi Parte II (Git && Symfony)

Ahora que tenemos el entorno vamos a descargar los fuentes en la carpeta, evidentemente la carpeta donde desplegamos tiene que estar configurada en Apache, nos colocamos en la carpeta de despliegue y lanzamos un git clone, en este caso suponemos un hosting en BitBucket que nos descargará todo el código.

# git clone https://usuario@bitbucket.org/acme/coyote.git

Siguiendo los consejos de Symfony2 vamos a verificar que está todo correcto, para esto primero tenemos que instalar composer y que nos ponga al día todos los paquetes, es recomendable tener accesible el fichero composer.json para evitar tener que repetir configuraciones.

# sudo curl -sS https://getcomposer.org/installer | sudo php
# sudo php composer.phar install --no-dev --optimize-autoloader

El fichero composer.json tiene que estar accesible en la misma ruta ya que es el fichero que usará para descargar los paquetes correctos.

Puede dar un error de «ErrorException: proc_open(): fork failed – Cannot allocate memory» una forma de arreglarlo es crear una unidad swap con los siguientes comandos:

# sudo /bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=1024
# sudo /sbin/mkswap /var/swap.1
# sudo /sbin/swapon /var/swap.1

Es recomendable que el fichero parameters.yml esté fuera del código fuente pero es necesario mantener parameters.yml.dist ya que en esta es donde se establecen los nuevos parámetros con sus valores por defecto. Después de esto los valores concretos de los parámetros se podrán modificar en parameteres.yml.

Es necesario una vez que se descarga la nueva versión desde git limpiar la cache y establecer de nuevos los permisos en la cache, para todos estos procesos se puede usar el siguiente script ubicándolo en la localización correcta.

# sudo git pull
# sudo php app/console cache:clear --env=prod
# sudo chown -R www-data:coyote app/cache

 

Desplegando en una Raspberry Pi Parte II (Git && Symfony)

Recopilatorio

Esta vez toca recopilatorio, sobre artículos que escribí en mi antigua empresa y que ahora agrupo para todos aquellos que les pueda resultar de utilidad o interés, tanto en castellano como inglés, los coloco cronológicamente aunque realmente se pueden leer en cualquier orden.

[Spanish]

Si … o no si ( To if or not to if )

Desmontando a imperativo (Deconstruyendo imperativo)

Bit veo, bit quiero

Quo vadis: Filosofía del programador

Cui bono? O el arte de la reutilización de código

[English]

Si … o no si ( To if or not to if )

Desmontando a imperativo (Deconstructing imperative)

Bit I see, bit I want

Quo vadis: Programmer’s Philosophy

Cui bono? Or the art of code reusing

Recopilatorio

Consultas MongoDb

En esta ocasión un pequeño recopilatorio sobre comandos básicos de MongoDb:

Las consultas se realizan con el comando find()

db.foo.find(query, projection)

query indica cual es el criterio a buscar.

projection  indica que campos hace falta, son campos que se pueden incluir o excluir.

db.foo.find({_id:1}, {_id:1})

El 1 indica que ese campo se incluye, un 0 no incluiría ese, hay una excepción y es el campo _id que siempre se incluye a no ser que se excluya de forma explícita, no se puede combinar o se incluyen todos o se excluyen todos (todos 1s o todo 0s).

A continuación una tabla tipo con los criterios de búsqueda más utilizados:

{$gt} Mayor que db.foo.find({_id: {$gt:5}},{_id:1})
{$lt} Menor que db.foo.find({_id: {$lt:5}},{_id:1})
{$lte} Menor que o igual db.foo.find({_id: {$lte:5}},{_id:1})
{$gte} Mayor que o igual db.foo.find({_id: {$gte:5}},{_id:1})
{$not} Negar la premisa db.foo.find({_id: {$not: {$gt:2}}}, {_id:1})
{$in} Valores en rango db.foo.find({_id: {$in: [1,3]}},{_id:1})
{$nin} Valores no en rango db.foo.find({_id: {$nin: [1,3]}},{_id:1})
{$all} Deben coincidir todos los valores db.foo.find({tags: {$all: [‘cute’,’ocean’]}}, {name:1})
{$exists} Verifica si el campo existe o no (true, false) db.foo.find({“info.canFly”: {$exists:false}},{name:1})

Se pueden concatenar filtros, aquí se estaría indicando todos los que sean mayores que uno y menores que 4 (2 y 3).

db.foo.find({_id: {$gte:2,$lt:4}},{_id:1})

Para buscar por texto hay que tener en cuenta que el texto es sensible a mayúsculas y minúsculas, tanto el nombre del campo como el valor.

db.foo.find({tags: ‘cute’}, {name:1})

 Para indicar un nulo se haría:

db.foo.find({“info.canFly”:null},{name:1})

 Esto devuelve tanto campos nulos como campos que no existen.

Consultas MongoDb