-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
bluetooth_builder.dart
372 lines (305 loc) · 14 KB
/
bluetooth_builder.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
/* Copyright 2024 Ryan Christopher Bahillo. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
=========================================================================*/
// ignore_for_file: non_constant_identifier_names
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:bfrbsys/shared/shared.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
abstract class GATTProtocolProfile {
/// The Generic Attribute Profile (GATT) is the architechture used
/// for bluetooth connectivity.
///
/// The length of uuids and characteristics must be the same and takes the
/// reference from the Arduino Nano 33 BLE Sense board.
/* ========================== S E R V I C E =========================== */
final String SERVICE_UUID = 'bf88b656-0000-4a61-86e0-769c741026c0';
final String TARGET_DEVICE_NAME = "BFRB Sense";
/* =========================================================================== */
/* ********************** C H A R A C T E R I S T I C S ********************** */
final String FILE_BLOCK_UUID = 'bf88b656-3000-4a61-86e0-769c741026c0';
final String FILE_LENGTH_UUID = 'bf88b656-3001-4a61-86e0-769c741026c0';
final String FILE_MAXIMUM_LENGTH_UUID = 'bf88b656-3002-4a61-86e0-769c741026c0';
final String FILE_CHECKSUM_UUID = 'bf88b656-3003-4a61-86e0-769c741026c0';
final String COMMAND_UUID = 'bf88b656-3004-4a61-86e0-769c741026c0';
final String TRANSFER_STATUS_UUID = 'bf88b656-3005-4a61-86e0-769c741026c0';
final String ERROR_MESSAGE_UUID = 'bf88b656-3006-4a61-86e0-769c741026c0';
final String ACC_DATA_UUID = 'bf88b656-3007-4a61-86e0-769c741026c0';
final String GYRO_DATA_UUID = 'bf88b656-3008-4a61-86e0-769c741026c0';
final String DIST_DATA_UUID = 'bf88b656-3009-4a61-86e0-769c741026c0';
// final String TEMP_DATA_UUID = 'bf88b656-3010-4a61-86e0-769c741026c0';
final String FILE_NAME_UUID = 'bf88b656-3011-4a61-86e0-769c741026c0';
/* =========================================================================== */
BluetoothDevice? device;
// The characteritics here must match in the peripheral
BluetoothCharacteristic? fileBlockCharacteristic;
BluetoothCharacteristic? fileLengthCharacteristic;
BluetoothCharacteristic? fileMaximumLengthCharacteristic;
BluetoothCharacteristic? fileChecksumCharacteristic;
BluetoothCharacteristic? commandCharacteristic;
BluetoothCharacteristic? transferStatusCharacteristic;
BluetoothCharacteristic? errorMessageCharacteristic;
BluetoothCharacteristic? accDataCharacteristic;
BluetoothCharacteristic? gyroDataCharacteristic;
BluetoothCharacteristic? distDataCharacteristic;
// BluetoothCharacteristic? tempDataCharacteristic;
BluetoothDevice? get serviceDevice {
return device;
}
List<BluetoothCharacteristic?> get characteristics {
return [
fileBlockCharacteristic,
fileLengthCharacteristic,
fileMaximumLengthCharacteristic,
fileChecksumCharacteristic,
commandCharacteristic,
transferStatusCharacteristic,
errorMessageCharacteristic,
accDataCharacteristic,
gyroDataCharacteristic,
distDataCharacteristic,
// tempDataCharacteristic,
];
}
}
class BluetoothBuilder extends GATTProtocolProfile {
StreamController discoverController = StreamController.broadcast();
StreamController<List> callbackController = StreamController<List>.broadcast();
bool isConnected = false;
bool isFileTransferInProgress = false;
Crc32 crc = Crc32();
int deviceMTU = 23;
Future<void> _discoverServices() async {
List<BluetoothService> services = await device!.discoverServices();
discoverController.add(false);
for (var service in services) {
if (service.uuid.toString() == SERVICE_UUID) {
for (var characteristic in service.characteristics) {
String characteristicUUID = characteristic.uuid.toString();
print('################ Searching $characteristicUUID ...');
// FILE_BLOCK_UUID : fileBlockCharacteristic
if (characteristicUUID == FILE_BLOCK_UUID) {
fileBlockCharacteristic = characteristic;
print('Connected to $FILE_BLOCK_UUID');
}
// FILE_LENGTH_UUID : fileLengthCharacteristic
else if (characteristicUUID == FILE_LENGTH_UUID) {
fileLengthCharacteristic = characteristic;
print('Connected to $FILE_LENGTH_UUID');
}
// FILE_MAXIMUM_LENGTH_UUID : fileMaximumLengthCharacteristic
else if (characteristicUUID == FILE_MAXIMUM_LENGTH_UUID) {
fileMaximumLengthCharacteristic = characteristic;
print('Connected to $FILE_MAXIMUM_LENGTH_UUID');
}
// FILE_CHECKSUM_UUID : fileChecksumCharacteristic
else if (characteristicUUID == FILE_CHECKSUM_UUID) {
fileChecksumCharacteristic = characteristic;
print('Connected to $FILE_CHECKSUM_UUID');
}
// COMMAND_UUID : commandCharacteristic
else if (characteristicUUID == COMMAND_UUID) {
commandCharacteristic = characteristic;
print('Connected to $COMMAND_UUID');
}
// TRANSFER_STATUS_UUID : transferStatusCharacteristic
else if (characteristicUUID == TRANSFER_STATUS_UUID) {
transferStatusCharacteristic = characteristic;
await Future.delayed(const Duration(milliseconds: 500));
await transferStatusCharacteristic!.setNotifyValue(true);
_onTransferStatusChanged(transferStatusCharacteristic);
print('Connected to $TRANSFER_STATUS_UUID');
}
// ERROR_MESSAGE_UUID : errorMessageCharacteristic
else if (characteristicUUID == ERROR_MESSAGE_UUID) {
errorMessageCharacteristic = characteristic;
await Future.delayed(const Duration(milliseconds: 500));
await errorMessageCharacteristic!.setNotifyValue(true);
_onErrorMessageChanged(errorMessageCharacteristic);
print('Connected to $ERROR_MESSAGE_UUID');
}
// ACC_DATA_UUID : accDataCharacteristic
else if (characteristicUUID == ACC_DATA_UUID) {
accDataCharacteristic = characteristic;
await Future.delayed(const Duration(milliseconds: 500));
await accDataCharacteristic?.setNotifyValue(true);
print('Connected to $ACC_DATA_UUID');
}
// GYRO_DATA_UUID : gyroDataCharacteristic
else if (characteristicUUID == GYRO_DATA_UUID) {
gyroDataCharacteristic = characteristic;
await Future.delayed(const Duration(milliseconds: 500));
await gyroDataCharacteristic?.setNotifyValue(true);
print('Connected to $GYRO_DATA_UUID');
}
// DIST_DATA_UUID : distDataCharacteristic
else if (characteristicUUID == DIST_DATA_UUID) {
distDataCharacteristic = characteristic;
await Future.delayed(const Duration(milliseconds: 500));
await distDataCharacteristic?.setNotifyValue(true);
print('Connected to $DIST_DATA_UUID');
}
// // TEMP_DATA_UUID : tempDataCharacteristic
// else if (characteristicUUID == TEMP_DATA_UUID) {
// tempDataCharacteristic = characteristic;
// await Future.delayed(const Duration(milliseconds: 500));
// await tempDataCharacteristic?.setNotifyValue(true);
// print('Connected to $TEMP_DATA_UUID');
// }
await Future.delayed(const Duration(milliseconds: 500));
}
// timer = Timer.periodic(const Duration(milliseconds: 100), _updateDataSource);
callbackController.add(['Connected to ${device!.localName}', 2]);
discoverController.add(true);
return;
}
}
}
void _connectToDevice() async {
await Future.delayed(const Duration(milliseconds: 500));
if (device == null) return;
await device!.connect();
// (Android Only) On iOS, MTU is negotiated automatically
if (Platform.isAndroid) await device!.requestMtu(512);
deviceMTU = await device!.mtu.first;
callbackController.add(['Getting characteristics ...', 0]);
await _discoverServices();
isConnected = true;
return;
}
///////////////////////////////////
void _onTransferStatusChanged(BluetoothCharacteristic? characteristic) {
characteristic!.onValueReceived.listen((List<int> value) {
num statusCode = bytesToInteger(value);
if (value.isEmpty) return;
if (statusCode == 0) {
_onTransferSuccess();
} else if (statusCode == 1) {
_onTransferError();
} else if (statusCode == 2) {
_onTransferInProgress();
}
});
}
/// Called when an error message is received from the device. This describes what
/// went wrong with the transfer in a user-readable form.
void _onErrorMessageChanged(BluetoothCharacteristic? characteristic) {
characteristic!.onValueReceived.listen((List<int> value) {
List<int> readData = List.from(value);
String errorMessage = String.fromCharCodes(readData);
if (readData.isNotEmpty && readData != []) {
callbackController.add(["Error message = $errorMessage", -1]);
}
});
}
void _onTransferInProgress() {
isFileTransferInProgress = true;
}
Future<void> _onTransferSuccess() async {
isFileTransferInProgress = false;
var checksumValue = await fileChecksumCharacteristic?.read();
var checksum = bytesToInteger(checksumValue!) as int;
callbackController.add(["File transfer succeeded: Checksum 0x${checksum.toRadixString(16)}", 2]);
}
void _onTransferError() {
isFileTransferInProgress = false;
callbackController.add(["File transfer error", -1]);
}
void _sendFileBlock(Uint8List fileContents) async {
if (fileContents.isEmpty) return;
int bytesAlreadySent = 0;
int bytesRemaining = fileContents.length - bytesAlreadySent;
int maxBlockLength = 128;
while (bytesRemaining > 0 && isFileTransferInProgress) {
int blockLength = min(bytesRemaining, maxBlockLength);
Uint8List blockView = Uint8List.view(fileContents.buffer, bytesAlreadySent, blockLength);
await fileBlockCharacteristic?.write(blockView).then((_) {
bytesRemaining -= blockLength;
callbackController.add([
"File block written - $bytesRemaining bytes remaining",
bytesAlreadySent / fileContents.length,
0,
]);
bytesAlreadySent += blockLength;
}).catchError((e) {
callbackController.add(["File block write error with $bytesRemaining bytes remaining", -1]);
isFileTransferInProgress = false;
});
}
}
void cancelTransfer() async {
await commandCharacteristic?.write([2]);
}
///////////////////////////////////
void connect() async {
bool isOn = await FlutterBluePlus.adapterState.first == BluetoothAdapterState.on;
bool found = false;
if (!isOn) {
callbackController.add(["Please turn on your bluetooth", 1]);
return;
}
FlutterBluePlus.isScanning.listen((scanning) {
if (!scanning && !found) callbackController.add(["Can't find your device.", 1]);
});
// Setup Listener for scan results.
Set<DeviceIdentifier> seen = {};
FlutterBluePlus.scanResults.listen((results) {
if (!found) callbackController.add(["Scanning...", 0]);
for (ScanResult r in results) {
if (seen.contains(r.device.remoteId) == false) {
print('${r.device.remoteId}: "${r.device.localName}" found! rssi: ${r.rssi}');
seen.add(r.device.remoteId);
if (r.device.localName == TARGET_DEVICE_NAME) {
callbackController.add(['Target device found. Getting primary service ...', 0]);
device = r.device;
found = true;
_connectToDevice();
}
}
}
});
// Start scanning
await FlutterBluePlus.startScan(timeout: const Duration(seconds: 10));
}
void disconnect() async {
callbackController.add(['Device ${device!.localName} disconnected', 0]);
await device!.disconnect();
isFileTransferInProgress = false;
discoverController.add(false);
device = null;
isConnected = false;
}
void transferFile(Uint8List fileContents) async {
var maximumLengthValue = await fileMaximumLengthCharacteristic?.read();
num maximumLength = bytesToInteger(maximumLengthValue!);
// var maximumLengthArray = Uint32List.fromList(maximumLengthValue!);
// ByteData byteData = maximumLengthArray.buffer.asByteData();
// int value = byteData.getUint32(0, Endian.little);
if (fileContents.length > maximumLength) {
callbackController
.add(["File length is too long: ${fileContents.length} bytes but maximum is $maximumLength", 1]);
return;
}
if (isFileTransferInProgress) {
callbackController.add(["Another file transfer is already in progress", 1]);
return;
}
var contentsLengthArray = integerToBytes(fileContents.length);
await fileLengthCharacteristic?.write(contentsLengthArray);
int fileChecksum = crc.crc32(fileContents);
var fileChecksumArray = integerToBytes(fileChecksum);
await fileChecksumCharacteristic?.write(fileChecksumArray);
await commandCharacteristic?.write([1]);
_sendFileBlock(fileContents);
}
}