Communication Flutter <-> Raspberry Pi
L’étape suivante du projet IT Seminar a été de réussir à faire communiquer l’application Flutter avec notre Raspberry. Ceci a été accompli grâce à des recherches sur les permissions bluetooth, le scan des appareils à proximité ainsi que des moyens de contrôler un Raspberry à partir d’une application Android. Ce travail a été partagé avec ma collègue Joanna Baranowska. La première version réussie de cette application fonctionne principalement grâce à 2 packages : flutter_bluetooth_serial qui permet la connexion Bluetooth entre 2 appareils et permission_handler qui facilite la gestion des permissions.
La gestion des permissions est très importante, car sans elle, Android ne permet aucune utilisation de Bluetooth. Nous avons donc dû implémenter cette gestion à 2 endroits :
- Dans le MainPage.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
...
Future<void> _requestBluetoothPermissions() async {
// Declaration and call of all needed permissions
PermissionStatus statusBluetooth = await Permission.bluetooth.status;
PermissionStatus statusLocation = await Permission.location.status;
PermissionStatus statusBluetoothConnect =
await Permission.bluetoothConnect.status;
PermissionStatus statusScan = await Permission.bluetoothScan.status;
PermissionStatus statusAdvertise =
await Permission.bluetoothAdvertise.status;
// Check if all permissions have been given and wait until they have
if (statusBluetooth.isDenied ||
statusLocation.isDenied ||
statusBluetoothConnect.isDenied ||
statusScan.isDenied ||
statusAdvertise.isDenied) {
Map<Permission, PermissionStatus> statuses = await [
Permission.bluetooth,
Permission.location,
Permission.bluetoothConnect,
Permission.bluetoothScan,
Permission.bluetoothAdvertise
].request();
print(statuses[Permission.bluetooth]);
print(statuses[Permission.location]);
print(statuses[Permission.bluetoothConnect]);
print(statuses[Permission.bluetoothScan]);
print(statuses[Permission.bluetoothAdvertise]);
}
}
...
- Dans le AndroidManifext.xml
1
2
3
4
5
6
7
8
...
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
...
Ces deux éléments permettent de préparer Android à l’utilisation des permissions Bluetooth puis de les demander à l’utilisateur lors de l’ouverture de l’application. Une fois cette étape passée (qui n’était pas des moindres…) nous avons pu nous concentrer sur la transmission de messages. Ceci a donc été accompli avec le package flutter_bluetooth_serial.
Une fois les permissions Bluetooth reçues ainsi que la localisation activée, on fournit une liste des appareils préalablement couplés à l’appareil ainsi que leur disponibilité actuelle :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
// Setup a list of the bonded devices
FlutterBluetoothSerial.instance
.getBondedDevices()
.then((List<BluetoothDevice> bondedDevices) {
setState(() {
devices = bondedDevices
.map(
(device) => _DeviceWithAvailability(
device,
widget.checkAvailability
? _DeviceAvailability.maybe
: _DeviceAvailability.yes,
),
)
.toList();
});
});
...
Le vif du sujet arrive maintenant avec le composant communication.dart. Ici, nous allons gérer la connexion grâce à l’adresse MAC enregistrée, préparer les données à transmettre en les encodant en UTF-8 et préparer la réception de données en retour.
Au début la connexion se fait donc à l’aide des adresses MAC pré-enregistrées dans l’appareil :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
// Connect to the device via Bluetooth
Future<void> connectBl(address) async {
await BluetoothConnection.toAddress(address).then((_connection) {
print('Connected to the device');
connection = _connection;
// Creates a listener to receive data
connection.input.listen(onDataReceived).onDone(() {});
// Catch connection errors
}).catchError((error) {
print('Cannot connect, exception occured');
});
}
...
La transmission de messages se fait après l’encodage du message :
1
2
3
4
5
6
7
8
9
10
11
12
13
...
// To send Message
Future<void> sendMessage(String text) async {
text = text.trim();
if (text.length > 0) {
try {
connection.output.add(utf8.encode(text + "\r\n"));
await connection.output.allSent;
} catch (e) {}
}
}
...
Une fois que toutes les transmissions sont terminées il est important de pouvoir terminer la connexion :
1
2
3
4
5
6
7
8
9
10
...
// End connection
Future<void> dispose() async {
fls.setPairingRequestHandler(null);
if (connection.isConnected) {
connection.dispose();
connection = null;
}
}
...
Ceci résume le fonctionnement de cette application Flutter, mais le projet a pour but connecter cette app à un Raspberry. Nous devons donc aussi préparer ce dernier à la connexion ainsi qu’y écrire un programme pouvant gérer les messages entrants et en renvoyer.
La première modification à faire est l’activation du Serial Peripheral Interface ou SPI. Ce dernier permet la communication avec des périphériques haut-débit (tels que via Bluetooth). Ceci se fait via la page de configuration du Raspberry accessible dans le menu déroulant au sommet de l’écran.
Nous devons ensuite accéder à un fichier de configuration du service Bluez en utilisant la commande :
1
sudo nano /etc/systemd/system/dbus-org.bluez.service
Une fois dans le document il faudra rajouter les éléments mis en évidence ci-dessous :
Ici, nous allons également donner un nom unique au Raspberry afin de ne pas les confondre. Pour ce faire il faut créer un nouveau fichier :
1
sudo /etc/machine-info
Dans ce fichier, on écrira
1
PRETTY_HOSTNAME=device-name
On peut ensuite redémarrer le service bluetooth :
1
service bluetooth restart
Nous devons finalement procéder à la réinitialisation des services impactés par ces changements en entrant les commandes suivantes :
Ce premier va recharger tous les fichiers compris dans le dossier systemd
1
sudo systemctl daemon-reload
Celui-ci va redémarrer le service bluetooth
1
sudo systemctl restart bluetooth.service
Une fois ces étapes passées, le Raspberry a été configuré pour recevoir notre connexion Bluetooth. On peut donc passer au pairing avec notre téléphone.
Pour ce faire on va utiliser le terminal et la commande bluetoothctl :
1
sudo bluetoothctl
1
agent on
1
default-agent
1
scan on
Une liste des appareils à proximité s’affiche. Il faut ici noter l’adresse MAC de l’appareil à connecter puis :
1
pair XX:XX:XX:XX:XX:XX
Attention : s’assurer que l’appareil à connecter est bien découvrable!
Un pop-up devrait apparaître sur l’appareil avec un code à 6 chiffres. S’il correspond à celui qui s’affiche dans le terminal, accepter la connexion des deux côtés.
Voilà, le Raspberry est configuré!
Il nous faut maintenant installer une librairie permettant de recevoir, interpréter et renvoyer les données. Nous allons utiliser bluedot et l’installer avec la commande :
1
sudo pip3 install bluedot
Nous allons utiliser python pour gérer la communication du côté Raspberry en créant un serveur Bluetooth qui va simplement recevoir les données, les imprimer dans la console et les renvoyer précédées du message “message received on your RaspberryPi :”. Pour ce faire il faut d’abord installer “MU”, un éditeur de code pratique. Dans l’onglet déroulant des “Preferences” accessible via la barre de menu au sommet de l’écran, on peut accéder au “Recommended Software”. Dans la nouvelle fenêtre on peut aller chercher “MU” dans l’onglet “Programming”, le sélectionner et cliquer sur “Apply”.
Une fois l’installation terminée on peut créer notre script python qui s’occupera de la communication Bluetooth ainsi que du contrôle des moteurs :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from bluedot.btcomm import BluetoothServer
from signal import pause
from gpiozero import Robot
robby = Robot(left=(19,16), right=(26,20))
def data_received(data):
if 'forward' in data:
print('Going forward')
robby.forward()
elif 'stop' in data:
print('Stopping')
robby.stop()
elif 'left' in data:
print('Going left')
robby.left()
elif 'right' in data:
print('Going right')
robby.right()
elif 'backward' in data:
print('Going backward')
robby.backward()
s.send('Message received on your RaspberryPi : ' + data)
s = BluetoothServer(data_received)
pause()
Et voilà! En exécutant ce script python, la communication se fait et les messages envoyés de l’app sont transmis au Raspberry qui renvoie un message automatiquement :
Les moteurs peuvent aussi être contrôlés via les messages en envoyant les commandes appropriées. Naturellement l’étape suivante a été de créer une interface de contrôle bien plus agréable et fonctionnelle que ce messaging. C’est pourquoi nous avons créé une interface utilisant des flèches :
Pour encore faciliter la connexion, on peut rajouter l’exécution de ce script au démarrage du Raspberry. Ceci supprime le besoin d’avoir un écran, clavier et souris à chaque fois que l’on veut connecter l’app au Raspberry et contrôler le buggy. En utilisant le fichier rc.local qui s’exécute dès le démarrage de l’OS. Pour ce faire il suffit de le modifier :
1
sudo nano /etc/rc.local
Une fois dans le fichier on peut rajouter la ligne :
1
python /home/pi/Desktop/RFcomm.py
Attention : ce chemin doit pointer vers l’endroit où vous avez sauvegardé votre script et peut varier du mien
Une fois sauvegardé il faut s’assurer que le fichier rc.local est exécutable en modifiant ses permissions :
1
sudo chmod +x /etc/rc.local
Pour tester le tout il suffit de reboot son Raspberry.
1
sudo reboot
Et si la connexion se fait une fois le reboot terminé, tout a bien été fait!
Ce projet nous a également apporté d’autres apprentissages plus généraux tels que :
- Le debugging en utilisant notre propre téléphone au lieu d’un émulateur (via les options développeur sur Android)
- Scanner les appareils actuellement découvrables autour de notre appareil
- Navigation entre widgets sur Flutter