Je me suis retrouvé à vouloir envoyer des fichiers depuis une carte Arduino vers un ordinateur, sans brancher de fil ni configurer une connexion réseau. Le bluetooth semble être une bonne option.
🚧 Il y a sûrement des méthodes moins "bricolage" pour envoyer des fichiers en bluetooth,
mais j'ai décidé de foncer sans trop faire de recherches.
Petite introduction au BLE GATT
Le BLE (Bluetooth Low Energy), c'est comme le bluetooth mais en mieux (?). Peut-être que ça sert pas à la même chose ? Je sais pas trop mais "Low Energy" ça sonne bien donc j'ai choisi de partir là dessus.
Il y a déjà assez de ressources en ligne pour comprendre en quoi ça consiste (comme ici, ou là, encore par là). Pour résumer ce que j'ai compris du BLE, il y a :
- Des Serveurs, qui ont
- Des Services, qui ont
- Des Caractéristiques, qui ont
- Une valeur, qui peut changer
- La capacité d'émettre un évènement pour prévenir que leur valeur a changé
- Des Caractéristiques, qui ont
- Des Services, qui ont
- Des Clients, qui
- Se connectent à des serveurs et
- Parcourent leurs services et
- Parcourent leurs Caractéristiques et
- Lisent leur valeur
- Peuvent s'abonner aux évènements, ou quelque chose dans le genre.
- Parcourent leurs Caractéristiques et
- Parcourent leurs services et
- Se connectent à des serveurs et
Ce qui est utilisé
Le serveur
Dans mon cas j'utilisais un EmotiBit. L'idée était qu'il prenne des mesures pendant un certain temps, puis les transmette à une machine immobile à la fin de la journée. L'Emotibit repose sur l'Adafruit HUZZAH32, c'est une carte ESP32 qui sait faire du bluetooth et un tas d'autres choses. Il y a cette librairie qui fonctionne bien sur cette carte pour faire du BLE.
Le client
J'ai écrit le client en python, en utilisant la librairie Bleak. Initiallement je comptais utiliser pybluez, mais la dernière release avait 3 ans, ça m'a effrayé.
L'objectif
L'objectif c'est d'atteindre un système qui fonctionne comme ça :
- Si rien n'est connecté au serveur on ne fait rien de spécial côté BLE
- Si quelque chose est connecté, la valeur de la caractéristique contient le nombre de fichiers disponibles
- Une fois que le nombre de fichier a été lu par un client, et s'il y a un fichier à envoyer : a. La valeur de la caractéristique prend la valeur des 256 prochains octets du fichier - s'il n'y a plus rien à lire dans le fichier, on le supprime et retour à l'étape 2 b. Une fois que la valeur a été lue, on retourne à l'étape 3.a
Le code
Côté arduino/esp32
🚧 Je ne maitrise pas du tout le C++, ça se sent dans mon code mais il a le mérite de fonctionner.
On met en place le BLE
Créer le service, les caractéristiques
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLECharacteristic.h>
#include <BLE2902.h>
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
BLECharacteristic* strCharacteristic = NULL;
void setup()
{
...
//Setup BLE
BLEDevice::init("Mon device BLE");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(SERVICE_UUID);
strCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_INDICATE
);
strCharacteristic->addDescriptor(new BLE2902());
pService->start();
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
...
}
En fouillant un peu je me suis rendu compte qu'on peut mettre en place des callbacks, ça a l'air pratique ! Donc on les défini :
class MyBLEServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer){
bleState = CONNECTED; // ça je l'explique plus tard, c'est pour une state machine
fileTransferState = NO_FILE; // ça je l'explique plus tard, c'est pour une state machine
currentFile = SD.open("jexistepas");
}
void onDisconnect(BLEServer* pServer){
onClientLost();
BLEDevice::startAdvertising();
}
};
class ContentCharacteristicCallbacks: public BLECharacteristicCallbacks {
void onStatus(BLECharacteristic* pCharacteristic, Status s, uint32_t code){
if(s == Status::SUCCESS_INDICATE){
if(fileTransferState == FILE_DONE) // ça je l'explique plus tard, c'est pour une state machine
fileTransferState = FILE_DONE_CONFIRMED; // ça je l'explique plus tard, c'est pour une state machine
else
fileTransferState = FILE_PART_RECEIVED; // ça je l'explique plus tard, c'est pour une state machine
//TODO delete file if FILE_DONE_CONFIRMED
}
else
if(s == Status::ERROR_NO_CLIENT){
onClientLost();
}
}
};
Et ensuite on les ajoute à notre serveur et à la caractéristique dans la fonction setup
.
...
BLEDevice::init("Mon device BLE");
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyBLEServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID);
strCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_INDICATE
);
strCharacteristic->setCallbacks(new ContentCharacteristicCallbacks());
...
Machine à état, et logique d'envoi de fichiers
Reste encore à mettre en place la logique pour envoyer le fichier.
🚧 Si on était sérieux on mettrai en place un peu de chiffrement lors du transfert des données,
peut-être même des logiques de contrôle d'intégrité, mais là
👏 j'ai 👏 pas 👏 envie 👏 de 👏 m'embêter 👏.
Déjà une machine à état (deux en fait) pour savoir où on en est.
enum BleState {
NOT_CONNECTED = 0,
CONNECTED = 1
};
enum FileTransferState {
NO_FILE = 9999,
FILE_AVAILABLE = 10000,
SENDING_FILE_PART = 10001,
FILE_PART_RECEIVED = 10002,
FILE_DONE = 10003,
FILE_DONE_CONFIRMED = 10004,
SENDING_AVAILABLE_FILE = 10005
};
BleState bleState = NOT_CONNECTED; // L'état initial
FileTransferState fileTransferState = NO_FILE; // L'état initial
Ensuite, dans le void loop
on peut se mettre en place un truc comme ça :
if(bleState == CONNECTED)
{
if(fileTransferState == NO_FILE || fileTransferState == FILE_AVAILABLE || fileTransferState == FILE_DONE_CONFIRMED){
String valueStr = String(fileTransferState)+"|"+String(bleFilesCount());
strCharacteristic->setValue(valueStr.c_str());
strCharacteristic->indicate();
}
else {
if(fileTransferState == SENDING_FILE_PART || fileTransferState == FILE_DONE){
// On appelle indicate pour prévenir le client qu'il a quelque chose à lire
strCharacteristic->indicate();
}
if(fileTransferState == FILE_PART_RECEIVED){
bleFileNext();
}
}
delay(10);
}
Il manque la partie où on modifie la caractéristique BLE pour envoyer le fichier pour de vrai par paquets de 256 octets, en fait ça se passe dans la fonction bleFileNext
:
bool bleFileNext(){
if(!currentFile){
currentFile = emotibit.getFirstFile();
}
if(!currentFile){
fileTransferState = NO_FILE;
return false;
}
byte buffer [256];
int bytesRead = currentFile.read(buffer, 256);
String data = String(bytesRead)+"|";
if(0 == bytesRead){
currentFile.close();
//TODO delete the file if that was read
currentFile = SD.open("jexistepas"); // un petit bricolage parce que je maitrise pas trop le C++
fileTransferState = FILE_DONE;
strCharacteristic->setValue(data.c_str());
return false;
}
for(int i = 0 ; i < bytesRead ; i++){
data += (char) buffer[i];
}
strCharacteristic->setValue(data.c_str());
fileTransferState = SENDING_FILE_PART;
return true;
}
Il manque aussi la fonction bleFilesCount
, qui lit le nombre de fichiers à envoyer et transitionne la machine à état en conséquence :
int bleFilesCount(){
int count = combienYATIlDeFichiersDansMaCarteSD(); // en fait j'ai pas encore implémenté ça
if(count > 0){
fileTransferState = FILE_AVAILABLE;
} else {
fileTransferState = NO_FILE;
}
return count;
}
Un client en python
En python c'était plus simple,
On commence par reporter les différents 'statuts' qu'on a prévu dans les valeurs de la caractéristique bluetooth :
class StartValues(str, Enum):
NO_FILE = "9999"
FILE_AVAILABLE = "10000"
SENDING_FILE_PART = "10001"
FILE_PART_RECEIVED = "10002"
FILE_DONE = "10003"
FILE_DONE_CONFIRMED = "10004"
SENDING_AVAILABLE_FILE = "10005"
Lister les appareils à proximité :
>>> from bleak import BleakScanner
>>> import asyncio
>>> loop = asyncio.new_event_loop()
>>> loop.run_until_complete(BleakScanner.discover())
[BLEDevice(666AFE17-6931-F308-9D7F-452FC325A891, A5 Emotibit), BLEDevice(FB8EE2A1-2B7C-CD5E-11D5-26BBE9B7C141, iPhone de Elias)]
>>>
Se connecter
Et un peu de logique pour se connecter et écouter les messages :
class BluetoothDeviceObserver:
def __init__(self, device_id):
self.device_id = device_id
self.lib_device = None
self.client: BleakClient = None
self.is_observing = False
self.files = {}
self.current_file = None
@property
def is_connected(self):
if self.client is None:
return False
else:
return self.client.is_connected
async def connect(self):
if self.client is None:
self.lib_device = await BleakScanner.find_device_by_address(
self.device_id
)
if self.lib_device is None:
raise Exception("Could not find requested device")
else:
self.client = BleakClient(self.lib_device)
if not self.is_connected:
await self.client.connect()
async def disconnect(self):
await self.client.disconnect()
def get_device(self) -> BluetoothDevice:
return BluetoothDevice.from_ble_device(self.lib_device)
async def start_observing(self):
services = await self.client.get_services()
char = services.get_characteristic(
"beb5483e-36e1-4688-b7f5-ea07361b26a8"
)
self.is_observing = True
await self.client.start_notify(
char,
self.on_char_notif
)
Et une fonction qui lit les valeurs successives de la caractéristique bluetooth :
async def on_char_notif(self, char, v):
str_value = v.decode("utf-8").split("|")
start_value = str_value[0]
actual_value = "|".join(str_value[1:])
if start_value == StartValues.NO_FILE and actual_value == "false":
await self.stop_observing()
if start_value in [
StartValues.SENDING_AVAILABLE_FILE,
StartValues.FILE_DONE_CONFIRMED
] and actual_value != "false":
if actual_value not in self.files:
self.files[actual_value] = ""
self.current_file = actual_value
if start_value not in [e.value for e in StartValues]:
if self.current_file in self.files:
self.files[self.current_file] += actual_value
Jusqu'ici on garde les fichiers en mémoire vive. On peut les écrire sur le disque dans une autre fonction.
async def stop_observing(self):
self.is_observing = False
print("Stopped observing")
await self.client.stop_notify("beb5483e-36e1-4688-b7f5-ea07361b26a8")
await self.disconnect()
if self.files:
for k, v in self.files.items():
with open(f".tmp/{k}", "w") as out_file:
print(f"{k} => {v}")
out_file.write(v)
print("Done retrieving files")
Ensuite on peut l'utiliser comme ça :
>>> from features.bluetooth.repository import BluetoothDeviceObserver
>>> observer = BluetoothDeviceObserver("666AFE17-6931-F308-9D7F-452FC325A891")
>>> loop.run_until_complete(observer.connect())
>>> loop.run_until_complete(observer.start_observing())
>>> observer.files
{'file_12202200123.csv': '1 [...] 11235,11229‘},
>>> loop.run_until_complete(observer.stop_observing())
Petit bilan
C'est pas super optimisé, ça pourrait sûrement être plus rapide et safe, mais j'ai au moins un genre de POC. Je note ça ici parce que ça pourrait toujours servir plus tard.