Flutter ile Bluetooth Low Energy (BLE) Kullanımı

Kürşat Fevzican Şayhan
7 min readMay 27, 2024

--

Bu yazıda Flutter ile BLE kullanımını inceleyeceğiz. BLE günlük hayatta cihazların birbirleriyle iletişim kurabileceği her alanda kullanılır. Sağlık, depolama, akıllı ev teknolojileri , spor gibi IOT alanının hayatımıza girdiği her alanda kullanılır. Özellikle düşük enerji tüketiminden dolayı çokça tercih edilir.

İlk olarak BLE Server kurulumu ile başlayacağız , daha sonra ise Client kurulumuna geçeceğiz. Client-Server arasında bir chat uygulaması şeklinde ilerleyeceğiz.

BLE Server

BLE Server için ESP32 ve Arduino IDE kullandım. Donanım tarafında çok bir yetkinliğe sahip olmadığım için ESP32 gibi geliştirme kartlarının işimi daha kolay bir şekilde çözeceğini düşündüm.

İlk olarak kütüphaneleri ekleyelim.

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

BLE de Bluetooth Classic den farklı olarak iletişimlerimizi Service ve o Service yapılarına bağlı olan Characteristic ile yapacağız. Bunun için Default değerlere sahip Service ve Characteristic UUID ve onların değişkenlerini tanımlayalım.

#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

BLEServer *pServer;
BLEService *pService;
BLECharacteristic *pCharacteristic;

setup() fonksiyonu içerisinde ilk olarak BLE cihazımızın ismini tanımlıyoruz. Daha sonrasında BLE Server’ımızı oluşturuyoruz ve bu Server’a bir Service, o Service içerisine de Characteristic ekliyoruz. Hem okuma hem yazma özelliklerini kullanacağımız için iki özelliği de açıyoruz.

BLEDevice::init("KFS BLE");
pServer = BLEDevice::createServer();
pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);

BLE server içerisinde Service ve Characteristic özelliklerini en sona eklediğimiz için yine aynı şekilde mobil uygulama tarafında bağlanırken de en sondaki Service ve Characteristic’ e bağlanacağız.

Oluşturduğumuz servisleri başlatarak BLE kurulumunu bitiriyoruz.

pService->start();
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06);
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();

Client tarafından gelecek mesajlar için loop() fonksiyonu içerisinde gerekli fonksiyonu tanımlıyoruz.

std::string value = pCharacteristic->getValue();

Bu işlemi mesaj gönderme fonksiyonu ile birleştirelim. Mesaj geldiği zaman mesajın boş olmadığını kontrol edelim ve daha sonra client tarafına gelen mesajı “ Mesaj Ulasti = → mesaj“ şeklinde geri dönelim.

void loop()
{
std::string value = pCharacteristic->getValue();


if(strcmp(value.c_str(), "") != 0)
{
Serial.println(value.c_str());
Serial.println(value.length());

String message = "Mesaj Ulasti = ";
message += value.c_str();
message += "----->";
pCharacteristic->setValue(message.c_str());
pCharacteristic->notify();
}

pCharacteristic->setValue("");
delay(2000);
}

Böylelikle server tarafını tamamlamış oluyoruz. Bluetooth üzerinden mesajlarımızı gönderip alabileceğiz.

Mobil Uygulama — Client

Mobil uygulama tarafında ilk olarak izinlerimizi ekleyerek başlayalım.

İzinlerde Android tarafımızda hardware izninin yanında Android 12 ve sonrası için ayrı 11 ve öncesi için ayrı izinlerimizi Android.manifest dosyamıza ekliyoruz.

Set android:required="true" if bluetooth is necessary -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />

<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- legacy for Android 11 or lower -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>

<!-- legacy for Android 9 or lower -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />

Ios için ise gerekli BLE izinlerimizi Info.plist dosyamıza ekliyoruz.

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Need BLE permission</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Need BLE permission</string>

İzinlerimizi ekledikten sonra kütüphanelerimizi eklemeye geçebiliriz.

flutter_blue_plus: 
permission_handler:
chat_bubbles:

flutter_blue_plus BLE için kullanacağımız fonksiyonlarda gerekli kütüphanemiz. permisson_handler ise kullanıcıdan bluetooth için izin alacağımız kütüphane. chat_bubbles kütüphanesi ise opsiyonel bir kütüphane , gönderdiğimiz ve gelen verilerin daha iyi gözükmesi için kullanacağız.

Şimdi kod tarafına geçebiliriz.

İlk olarak bluetooth için izin isteyerek başlayalım.

Future getPermissions()async{
try{
await Permission.bluetooth.request();
}catch(e)
{
print(e.toString());
}
}

@override
void initState() {
// TODO: implement initState
super.initState();
getPermissions();
}

flutter_blue_plus kütüphanesini kodumuza ekliyoruz.

import 'package:flutter_blue_plus/flutter_blue_plus.dart';

Bluetooth Durumu ve Açma/Kapatma Fonksiyonları

Cihazımızdaki Bluetooth durumunu (Açık/Kapalı) öğrenmek için FlutterBluePlus.adapterState Stream özelliğini kullanacağız. Bir StreamBuilder yardımı ile durum her değiştiğinde kullanacağımız SwitchTile Widgetını güncelleyeceğiz.

Cihazın bluetooth özelliğini açmak için FlutterBluePlus.turnOn(); Cihazın bluetooth özelliğini kapatmak için FlutterBluePlus.turnOff(); fonksiyonlarını kullanacağız.

StreamBuilder(
stream: FlutterBluePlus.adapterState,
builder: (context,snapshot){
if(snapshot.data != null)
{
if(snapshot.data == BluetoothAdapterState.on){bluetoothState = true;}else if(snapshot.data == BluetoothAdapterState.off){bluetoothState = false;}
return Container(
height: 30,
child: SwitchListTile(
activeColor: Color(0xFF015164),
activeTrackColor: Color(0xFF0291B5),
inactiveTrackColor: Colors.grey,
inactiveThumbColor: Colors.white,
selectedTileColor: Colors.red,
title: Text('Bluetooth Etkinleştir',style: TextStyle(fontSize: 14),),
value: bluetoothState,
onChanged: (bool value) {
setState(() {
bluetoothState = !bluetoothState;
if (value) {
FlutterBluePlus.turnOn();
} else {
FlutterBluePlus.turnOff();
}
});
}
),
);
}else{
return Container();
}
}),

Bluetooth Cihazlarını Tarama

Cihazları taramak için ayrı bir sayfa oluşturacağız. Sayfaya gidip cihazımızı seçeceğiz ve Navigator.pop ile seçtiğimiz cihazı önceki sayfaya gönderip asıl sayfada bağlanacağız. Bu benim kurduğum bir senaryo, siz de kendinize uygun bir şekilde tasarlayabilirsiniz.

Tarama işlemini başlatmak için FlutterBluePlus.startScan ve tarama işlemini bitirmek için de FlutterBluePlus.stopScan fonksiyonunu kullanacağız.

Tarama yapıldığını tespit etmek için FlutterBluePlus.isScanning ile Stream olarak StreamBuilderdan ile FloatingActionButton dan alacağız.

floatingActionButton: StreamBuilder<bool>(
stream: FlutterBluePlus.isScanning,
initialData: false,
builder: (c, snapshot) {
if (snapshot.data!) {
return FloatingActionButton(
child: const Icon(Icons.stop,color: Colors.red,),
onPressed: () => FlutterBluePlus.stopScan(),
backgroundColor:Color(0xFFEDEDED),
);
} else {
return FloatingActionButton(
child: Icon(Icons.search,color: Colors.blue.shade300,),
backgroundColor:Color(0xFFEDEDED),
onPressed: ()=> FlutterBluePlus.startScan(timeout: const Duration(seconds: 4))
);
}
},
),

Cihazları tararken 2 tarama kullanacağız. Birincisi bağlanılmış cihazlar, ikincisi bağlanılacak cihazlar.

Bağlanılmış cihazlar için systemDevices fonksiyonunu bir stream ile çağıracağız.

Stream.periodic(const Duration(seconds:10))
.asyncMap((_) => FlutterBluePlus.systemDevices

Bağlanılacak cihazlar için , tarama fonksiyonuna tıkladığımızda scanResult streamini tetikleyecek ve biz de bununla çekeceğiz.

FlutterBluePlus.scanResults

Bu iki listeyi SingleChildScrollView içerisindeki bir Column ile kullanacağız.

Seçtiğimiz cihazı önceki sayfaya Navigator.pop ile göndereceğiz. Gönderirken cihazın bağlanılmış mı yoksa bağlanılacak cihaz mı olduğunu ise state değişkeninde belirteceğiz.

class SelectedDevice{
BluetoothDevice? device;
int? state;

SelectedDevice(this.device,this.state);
}

Cihaz Tarama Sayfası :

SingleChildScrollView(
child: Column(
children: <Widget>[
StreamBuilder<List<BluetoothDevice>>(
stream: Stream.periodic(const Duration(seconds:10))
.asyncMap((_) => FlutterBluePlus.systemDevices),
initialData: const [],
builder: (c, snapshot){
snapshot.data.toString();
return Column(
children: snapshot.data!.map((d) {
return Column(
children: [
ListTile(
title: Text(d.platformName,style: TextStyle(color: Color(0xFFEDEDED)),),
leading: Icon(Icons.devices,color: Color(0xFFEDEDED).withOpacity(0.3),),
trailing: StreamBuilder<BluetoothConnectionState>(
stream: d.connectionState,
initialData: BluetoothConnectionState.disconnected,
builder: (c, snapshot) {
bool con = snapshot.data == BluetoothConnectionState.connected;
return ElevatedButton(
child: Text('Bağlan',style: TextStyle(color: con?Colors.green:Colors.red),),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(color: con?Colors.green:Colors.red),
borderRadius: BorderRadius.all(Radius.circular(8))
)
),
onPressed: () {Navigator.of(context).pop(SelectedDevice(d,1));}
,
);
},
),
),
Divider()
],
);
})
.toList(),
);
},
),
StreamBuilder<List<ScanResult>>(
stream: FlutterBluePlus.scanResults,
initialData: const [],
builder: (c, snapshot) {
List<ScanResult> scanresults = snapshot.data!;
List<ScanResult> templist = [];
scanresults.forEach((element) {
if(element.device.platformName != "")
{
templist.add(element);
}
});

return Container(
height: 700,
child: ListView.builder(itemCount: templist.length,itemBuilder: (context,index)
{
return Column(
children: [
ListTile(
title: Text(templist[index].device.platformName,style: TextStyle(color: Color(0xFFEDEDED)),),
leading: Icon(Icons.devices,color: Color(0xFFEDEDED).withOpacity(0.3),),
trailing: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
side: BorderSide(color: Colors.orange)
)
),
onPressed: () async{
Navigator.of(context).pop(SelectedDevice(templist[index].device,0));
},child: Text("Bağlan",style: TextStyle(color: Color(0xFFEDEDED)),)),
),
Divider()
],
);
}),
);
},
),
],
),
),
Cihaz Tarama

Cihaz Bağlantısı

Cihaz seçildikten sonra geri dönüyoruz ve önceki sayfadan gönderdiğimiz veriyi yakalıyoruz.

final SelectedDevice? poppedDevice =
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return SelectBluetoothDevice();
},
),
);

Eğer veri null değilse selectedDevice değişkenine gelen cihazı ekliyoruz.

selecteddevice = poppedDevice.device;

Daha sonra ise cihazın state verisini kontrol ediyoruz. State verisini önceki sayfada cihazın bağlı veya bağlı değil durumuna göre ayarlamıştık. Eğer state 1 ise cihaz zaten bağlı demektir. Eğer state 0 ise bağlı değil ama bağlanmaya hazır anlamına gelecek. Duruma göre de ConnectionStatus değişkenimi true/false yapacağım.

if (poppedDevice != null) {
setState(() {
selecteddevice = poppedDevice.device;
print(poppedDevice.state);
if(poppedDevice.state == 1)
{
BluetoothConnectionState ? ev;
selecteddevice!.state.listen((event) {
if(ev == BluetoothConnectionState .connected)
{
setState(() {
ConnectionStatus = true;
});
}else{
ConnectionStatus = false;
}

});

}else if(poppedDevice.state == 0)
{
ConnectionStatus = false;
}
});
}

Şimdi ise bağlantıyı yapacağımız Butona geçebiliriz. selecteddevice.connect fonksiyonu ile bağlantı sağlıyoruz.

İlk olarak ConnectionStatus kontrol ediyoruz. Eğer false ise zaten bağlı olduğu için bağlantıya sokmayacağız.

await selecteddevice!.connect().then((value){


selecteddevice!.connectionState.listen((event) async{
setState(() {
if(event == BluetoothConnectionState.connected)
{
ConnectionStatus = true;
}else{
ConnectionStatus = false;
}
});
if(event == BluetoothConnectionState.disconnected){
await stream_sub!.cancel();
}
});
});

Veri Okuma

Server tarafında Service ve Characteristic özelliklerini kendimiz yazmıştık ve WRITE, READ özelliklerini açmıştık.

List<BluetoothService> services = await selecteddevice!.discoverServices();
BluetoothService lastservice = services.last;
BluetoothCharacteristic lastCharacterist = lastservice.characteristics.last;

Eğer özellikle belirli bir Service veya Characteristic değerine ulaşmak istiyorsak tarama yaparak onu bulmamız gerekiyor. Biz kendimiz eklediğimiz için en sona eklendi, bu yüzden last değerlerini aldık.

Okuma fonksiyonunu direkt çağırmak yerine StreamSubscription içerisine aldım. Bu sayede BLE cihazını disconnect durumuna getirdiğimizde cancel edebileceğiz.

StreamSubscription<List<int>>? stream_sub;


stream_sub = lastCharacterist.onValueReceived.listen((value) async{
if(value.isNotEmpty)
{
String s = new String.fromCharCodes(value);
setState(() {
buffer.add(Message(s,0));
});
}
});

Gelen mesajları bir chat şeklinde göstereceğim. Bu yüzden gelen ve gönderilen mesajların belirlenmesi için Message değişkeni altında göndereceğim.

class Message{

String? text;
int? sender;

Message(this.text,this.sender);

}

Veri Yazma

Burada da aynı şekilde kendi yazdığımız service ve characteristic değerlerini kullanacağımız için last değerlerini kullanacağız.

String text = controller.text;

List<BluetoothService> services = await selecteddevice!.discoverServices();
BluetoothService lastservice = services.last;
BluetoothCharacteristic lastCharacterist = lastservice.characteristics.last;

List<int> list = utf8.encode(text);
Uint8List bytes = Uint8List.fromList(list);
setState(() {
buffer.add(Message(text,1));
});

await lastCharacterist.setNotifyValue(true);
await lastCharacterist.write(bytes );

Chat

Okuduğum ve yazdığım verileri bir buffer listesine kaydettim. Kaydederken verilerin gönderilen ve alınan veriler mi olduğunu da kaydettim. Kütüphaneleri eklerken belirttiğimiz chat_bubbles kütüphanesini kullanarak bir chat görüntüsü oluşturacağız.

ListView.builder(
itemCount: buffer.length,
itemBuilder: (context,index){
return BubbleSpecialThree(
color: buffer[index].sender==1?Colors.white70:Colors.lightBlueAccent,
text: buffer[index].text!,
isSender: buffer[index].sender==1?true:false,
textStyle: TextStyle(color: Colors.black),
);
}),
Chat

Sonuç

ESP32 kodunu kendi uygulamanıza göre geliştirebilirsiniz. Ben şimdilik sadece gelen veriyi bize tekrar döndürecek şekilde tasarladım. BLE kütüphanesinde verileri List<Uint8> tipinde gönderiyoruz ve alıyoruz. Uygulamanın çeşitliliğine göre hex kütüphanesi gibi kütüphaneleri kullanarak verileri işleyebiliriz.

Teşekkürler.

--

--

Kürşat Fevzican Şayhan
Kürşat Fevzican Şayhan

Written by Kürşat Fevzican Şayhan

Hi, I am developing mobile and desktop applications. I use Flutter/Dart language in mobile programming and .Net/C# language in desktop programming.

No responses yet