Découverte de buildroot avec un raspberrypi

Présentation

Il y a quelque temps, j’avais monté un système audio avec un raspberrypi pi 0, permettant de brancher la carte à ma chaine hifi et diffuser ainsi la musique dans le salon. En équipant la carte d’un DAC de qualité, je centralise la musique qui peut etre jouée depuis n’importe quel PC, et comme la carte est équipée d’une connexion bluetooth, cela permet aussi d’y connecter les smartphones.

Dans mon montage originel, je partais d’une distribution raspbian que j’avais modifié manuellement. À la suite d’une erreur de manipulation, j’ai dû refaire le paramétrage une seconde fois, et c’est depuis quelque chose qui me préoccupait: il faudrait que je puisse disposer d’un système déjà configuré qui fonctionnerait « out of the box », sans que j’aie à toucher à la configuration.

J’ai découvert entre-temps buildroot (via le projet showmewebcam) qui est un ensemble de scripts permettant de construire un système complet. Il prend en charge un grand nombre d’architectures différentes, et possède une configuration de base pour construire les applications les plus courantes. Il ne reste plus qu’à l’utiliser pour reconstruire un client pulseaudio sur une carte raspberrypi 0, qui offre un service via le wifi et le bluetooth.

Je vous propose ici de suivre le détail des modifications apportées pour construire le système (le projet est disponible sur mon dépôt git – mais pas l’image finale).

Article avancé

Je ne pensais pas qu’un jour j’écrirais ce genre d’en tête, mais il s’agit ici d’un article avancé, qui nécessite de connaître le fonctionnement du système linux.

Nous allons en effet construire un système de zéro, pour un usage spécifique, ce qui oblige à sélectionner les services dont nous avons besoin (et savoir lesquels sélectionner), et comment les configurer pour obtenir le système attendu.

Aucune interface graphique ne sera installée sur la carte, et toute la configuration devra être anticipée pour que la carte démarre dès son branchement.

Construction du système

Initialisation

Je fournis un script build.sh qui prends en paramètres:

  1. une architecture
  2. une commande buildroot (all pour lancer la création complète du système)

pour l’instant, seules deux architectures ont été testées: un raspberrypi3 (modèle A), et un raspberrypi0w:

./build.sh
usage: BUILDROOT_DIR=../buildroot ./build.sh {boardname} all
boardname: raspberrypi0w, raspberrypi3

Lors de l’exécution de la commande, le système va fusionner le paramétrage par défaut de la carte, qui est chargé dans la configuration de buildroot, ainsi qu’une liste de paquets spécifiques qui sont propres à notre installation, et listés dans le fichier configs/config

Sélection des paquets

Cette configuration par défaut inclue les paquets nécessaires pour faire fonctionner le réseau (wifi), pulseaudio, et le bluetooth (ainsi que les dépendances pour mettre toute cette architecture en place). Si l’on souhaite modifier un paquet, on peut regarder ceux disponibles en lançant une interface de sélection:

./build.sh raspberrypi3 menuconfig
L’interface menuconfig

Toutefois, je n’aime pas cette manière de procéder: la configuration ainsi générée se retrouve spécifique à une carte donnée, et l’on perd la possibilité d’appliquer cette même configuration sur un matériel différent.

Aussi, et pour être sûr d’avoir un système qui soit reproductible, je préfère noter les paquets dont j’ai besoin, et je les reporte dans le fichier configs/config qui sera utilisé lors de la prochaine construction.

Configuration du système

En parallèle de la construction du système, nous avons à nous occuper de sa configuration initiale.

Cela est réalisé à travers deux scripts, le premier, post-build.sh, est exécuté après l’installation des paquets, le second, post-image.sh après la finalisation de l’image finale. Dans le premier nous allons modifier les fichiers de configuration du système, alors que dans le second nous traiterons les paramètres donnés lors du boot.

Je présente ci-dessous quelques modifications apportées au système pour le faire correspondre exactement au besoin:

Carte en lecture seule

Étant donné que nous n’aurons pas d’interface pour nous connecter à la carte, elle va etre éteinte « sauvagement » régulièrement. Je préfère donc la basculer en lecture seule, ce qui permet aussi d’etre sûr que tout le système restera immuable sur la durée.

sed -ie '/^\/dev\/root/ s/rw 0 1/ro 0 0/' "${TARGET_DIR}/etc/fstab"

La modification est faite avec sed, et permet de suite de voir un peu comment on va modifier le système:

  • Chaque modification sera faite à l’aide des outils disponibles dans bash (sed, cp, cat, …), il faut donc déjà etre à l’aise avec le terminal avant d’aller modifier la configuration
  • Chaque command nécessite de savoir comment sont construits les fichiers que l’on veut modifier
  • Dans le cas où nous ajoutons des nouvelles entrées dans un fichier, il faudra contrôler avant que cette entrée n’est pas déjà présente
  • Et enfin, nous allons avoir du boulot pour modifier chaque élément un à un…

La variable ${TARGET_DIR} est fournie par buildroot et permet de cibler l’image que nous sommes en train de construire (par opposition au système hote dans lequel nous compilons le système). Ici, nous modifions bien le fichier /etc/fstab final, et non pas notre système linux!

Bonne pratique

Au vu des risques en cas d’erreur de saisie, il est bon de rajouter cette ligne en début de script:

# Traite les variables non définies comme des erreurs lors de la substitution.
set -u

Nous serons ainsi sûrs de ne pas modifier notre système par erreur (ce qui ne devrait pas arriver, car nous ne travaillons pas sous root)

Ajout du wifi

Pour activer le wifi, nous allons laisser systemd faire le travail, wpa_supplicant propose en effet un service paramétré, qui active l’interface spécifiée dans le nom (ici wlan0), il suffit donc de créer un lien symbolique pour que celui-ci soit activé au démarrage:

# Create the link to interface wlan0 directly in the system configuration
ln -sf /usr/lib/systemd/system/wpa_supplicant@.service "${TARGET_DIR}/usr/lib/systemd/system/multi-user.target.wants/wpa_supplicant@wlan0.service"

(par contre, cela implique d’installer systemd sur la carte, ce qui surcharge les dépendances, mais cela simplifie notre travail finalement…)

La configuration de la carte est comme d’habitude dans un fichier wpa_supplicant.conf qui n’est pas versionné, mais qui sera copié s’il est présent dans le répertoire.

if [ -f "$BR2_EXTERNAL_PIAUDIO_PATH/wpa_supplicant.conf" ]; then
    cat "$BR2_EXTERNAL_PIAUDIO_PATH/wpa_supplicant.conf" > "${TARGET_DIR}/etc/wpa_supplicant/wpa_supplicant-wlan0.conf"
    fi

La variable $BR2_EXTERNAL_PIAUDIO_PATH correspond au répertoire de base du système, c’est-à-dire notre répertoire racine. Il s’agit également d’une autre variable d’environnement fournie par buildroot qui nous permet de ne pas avoir à nous soucier de l’emplacement du script quand nous lançons notre compilation.

Pulseaudio

Vient ensuite pulseaudio. Nous n’avons pas à nous occuper de le démarrer avec le système car cela est fourni par un paquet ayant été sélectionné.

Par contre, puisque le système est en lecture seule, nous avons quelques modifications à réaliser pour que le service fonctionne: nous allons créer un répertoire, et le monter en mémoire au démarrage afin que pulseaudio puisse y stocker ses fichiers:

create_missing_dir "/var/lib/pulse"
if ! grep -qE '/var/lib/pulse' "${TARGET_DIR}/etc/fstab"; then
    cat << __EOF__ >> "${TARGET_DIR}/etc/fstab"
tmpfs /var/lib/pulse tmpfs rw 0 0
__EOF__
fi

Ensuite, nous allons modifier le fichier /etc/pulse/system.pa afin d’y ajouter les modules bluetooth, et la connexion distante:

if ! grep -qE '^load-module module-native-protocol-tcp' "${TARGET_DIR}/etc/pulse/system.pa"; then
    cat << __EOF__ >> "${TARGET_DIR}/etc/pulse/system.pa"
load-module module-bluetooth-policy
load-module module-bluetooth-discover
load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1;192.168.0.0/24;2a01:e35:8ac8:0e00::/64 auth-anonymous=1
__EOF__
fi

Bluetooth

D’abord, pour activer le bluetooth, nous allons indiquer au noyau de le charger au démarrage:

cat << __EOF__ >> "${BINARIES_DIR}/rpi-firmware/config.txt"
dtparam=krnbt=on
__EOF__

Cette option ajoutée récemment dans les raspberrypi va nous éviter d’avoir à lancer les commande bt-attach ou hciattach. le bluetooth est automatiquement branché sur le port uart, à la vitesse maximale prise en charge par la carte. Cela résoud en une ligne énormément de problèmes rencontrés lors de la configuration du bluetooth si l’on avait voulu faire cela manuellement.

Voir la liste des options possibles sur la page suivante: https://github.com/raspberrypi/firmware/tree/master/boot/overlays

Par contre, il faut libérer pour cela la connexion série, qui est configurée par défaut sur le port ttyAMA0. Il nous faut supprimer la ligne suivante dans le fichier cmdline.txt : console=ttyAMA0,115200

Ensuite, comme précédemment, nous allons également créer un répertoire en mémoire pour pallier le système en lecture seule de la carte:

create_missing_dir "/var/lib/bluetooth/"
if ! grep -qE '/var/lib/bluetooth' "${TARGET_DIR}/etc/fstab"; then
    cat << __EOF__ >> "${TARGET_DIR}/etc/fstab"
tmpfs /var/lib/bluetooth tmpfs rw 0 0
__EOF__
fi

La configuration du bluetooth est lue dans le fichier /etc/bluetooth/main.conf, c’est donc là que nous allons y stocker les paramètres génériques: classe de l’appareil et timeouts:

[General]

Class = 200428
DiscoverableTimeout = 0
PairableTimeout = 0

[Policy]
AutoEnable=true

Enfin, nous avons besoin d’autoriser les connexions externes, et ce, sans aucun accès au système. Cela s’effectue par le biais d’un agent qui demande la confirmation d’un code auprès des deux parties (celui qui veut se connecter, et celui qui accepte les connexions) pour éviter les erreurs. Ici, nous faire fi de la sécurité, et ouvrir les connexions à n’importe qui. Les risques sont limités (le système est en lecture seule, aucune des applications de base permettant de détourner l’ordinateur n’est installée).

C’est un problème que j’avais rencontré dans mon système d’origine, et qui nécessitait de se connecter sur le raspberrypi pour autoriser le périphérique. J’ai finalement trouvé une solution (qui se résume en quelques lignes, mais qui m’a occupé quelques heures avant d’en arriver là).

On va se créer un service systemd (puisqu’il est là…) qui lance un agent perpétuel chargé d’accepter toutes les connexions:

cat << __EOF__ > "${TARGET_DIR}/etc/systemd/system/bt-agent.service"
[Unit]
Description=Bluetooth Agent
After=bt-audio.service
Requires=bt-audio.service

[Service]
Type=simple
ExecStartPre=bt-adapter --set Discoverable 1
ExecStart=bt-agent -c NoInputNoOutput
RestartSec=5
Restart=always
KillSignal=SIGUSR1

[Install]
WantedBy=bluetooth.target
__EOF__

Au passage, ce service déclare le bluetooth en mode « découvrable », ce qui rend vraiment le service ouvert à tous.

Connexion série

Enfin, il faut que nous puissions nous connecter à la carte pour contrôler qu’elle démarre correctement. Or, nous n’avons installé aucun serveur ssh: celle-ci est donc vérouillée. Par contre, le raspberrypi 0 nous offre une chance, puisqu’il est possible de s’y connecter en le branchant directement en usb, mais encore faut-il activer cette option au démarrage…

Note

Cela fonctionne également avec un raspberrypi 4 ou un raspberrypi 3 A, mais pour ce dernier, il faut trouver un cable USB male/male pour faire la connexion. Dans mes tests avec ce dernier, j’ai triché et installé dropbear qui est un serveur ssh!

Il suffit d’ajouter une ligne dans le fichier cmdline.txt et le tour est joué :

if ! grep -qE 'modules-load=dwc2,g_serial' "${BINARIES_DIR}/rpi-firmware/cmdline.txt"; then
    sed '/^root=/ s/$/ modules-load=dwc2,g_serial/' -i "${BINARIES_DIR}/rpi-firmware/cmdline.txt"
fi

Build

Il ne nous reste plus qu’à lancer la compilation du système, et attendre le résultat:

./build.sh raspberrypi0w all

Une fois terminé, on peut copier l’image générée et la brancher dans le raspberry pi

sudo dd if=output/raspberrypi0w/images/sdcard.img of=/dev/XXX bs=4k

Démarrage et premier lancement

La carte connectée en USB

Une fois la carte branchée, on peut se connecter dessus à l’aide de la commande suivante:

screen /dev/ttyACM0

Récupérer le nom de la connexion pulseaudio

# su -s /usr/bin/sh pulse
$ pactl info
…
Default Sink: alsa_output.0.stereo-fallback

C’est cette valeur qui est à utiliser dans le tunnel que nous allons mettre en place. Sur notre pc de bureau, nous allons modifier le fichier /etc/pulse/default.pa et ajouter cette ligne:

load-module module-tunnel-sink sink_name=rpi_tunnel server=tcp:${IP}:4713 sink=alsa_output.0.stereo-fallback

Comme notre raspberrypi se connecte en DHCP, son adresse IP est fournie par le routeur. Je conseille de lui associer une IP fixe afin que la configuration ne change pas:)

La connexion pulseaudio

Nommage du service bluetooth

Un dernier point que je n’ai pas réussi à comprendre, le nom du service bluetooth est Bluez au lieu du nom de la carte (piaudio). Cela semble être du au fait que la carte soit en lecture seule. La solution consiste à basculer la carte en écriture, relancer le service bluetooth, et redémarrer:

mount -o remount,rw /
systemctl restart bluetooth
reboot

Cette opération est à faire une fois seulement, il n’est pas nécessaire d’y revenir ensuite.

Se connecter en bluetooth

La connexion bluetooth

Comme nous avons tout automatisé, c’est encore plus simple! en quelques clics l’association est faite!