Implement Logging (#199)

* Implement Logging

* Add timestamp in OnValueChanged

* improve web and linux logging

* Implement windows and format pigeon

* Fix windows build and unity log level

* Log commands with timestamp

* Fix code doc

* Remove unnecessary import

* Update docs

* Fix crash on windows 11 because of failed pairing

* Update changelog

* Fix native code formatting
This commit is contained in:
Rohit Sangwan
2025-12-12 18:56:43 +05:30
committed by GitHub
parent d85cbbe725
commit 89abb3bf03
32 changed files with 1016 additions and 346 deletions
+2 -2
View File
@@ -93,7 +93,7 @@ extension BleDeviceExtension on BleDevice {
///
/// [service] is the UUID of the service.
/// [preferCached] indicates whether to use cached services. If cache is empty, discoverServices() will be called.
/// might throw [NotFoundException]
/// might throw [UniversalBleException]
Future<BleService> getService(
String service, {
bool preferCached = true,
@@ -128,7 +128,7 @@ extension BleDeviceExtension on BleDevice {
/// [service] is the UUID of the service.
/// [characteristic] is the UUID of the characteristic.
/// [preferCached] indicates whether to use cached services. If cache is empty, discoverServices() will be called.
/// might throw [NotFoundException]
/// might throw [UniversalBleException]
Future<BleCharacteristic> getCharacteristic(
String characteristic, {
required String service,
+8
View File
@@ -0,0 +1,8 @@
enum BleLogLevel {
none,
error,
warning,
info,
debug,
verbose;
}
+1
View File
@@ -1,3 +1,4 @@
export 'package:universal_ble/src/models/ble_log_level.dart';
export 'package:universal_ble/src/models/manufacturer_data.dart';
export 'package:universal_ble/src/models/platform_config.dart';
export 'package:universal_ble/src/models/queue_type.dart';
+8
View File
@@ -24,6 +24,14 @@ class UniversalBle {
_bleCommandQueue.timeout = duration;
}
/// Set log level for both Dart and native implementations.
/// Only effective in debug builds.
static Future<void> setLogLevel(BleLogLevel logLevel) async {
if (!kDebugMode) return;
UniversalLogger.setLogLevel(logLevel);
await _platform.setLogLevel(logLevel);
}
/// Set how commands will be executed. By default, all commands are executed in a global queue (`QueueType.global`),
/// with each command waiting for the previous one to finish.
///
@@ -244,6 +244,10 @@ class UniversalBleLinux extends UniversalBlePlatform {
@override
Future<void> setNotifiable(String deviceId, String service,
String characteristic, BleInputProperty bleInputProperty) async {
UniversalLogger.logDebug(
"SET_NOTIFY -> $deviceId $service $characteristic input=${bleInputProperty.name}",
withTimestamp: true,
);
final char = _getCharacteristic(deviceId, service, characteristic);
String characteristicKey = "${deviceId}_${service}_$characteristic";
@@ -265,10 +269,15 @@ class UniversalBleLinux extends UniversalBlePlatform {
for (String property in properties) {
switch (property) {
case BluezProperty.value:
UniversalLogger.logVerbose(
"NOTIFY <- $deviceId $service $characteristic len=${char.value.length} data=${char.value}",
withTimestamp: true,
);
updateCharacteristicValue(
deviceId,
characteristic,
Uint8List.fromList(char.value),
DateTime.now().millisecondsSinceEpoch,
);
break;
default:
@@ -293,11 +302,19 @@ class UniversalBleLinux extends UniversalBlePlatform {
String characteristic, {
final Duration? timeout,
}) async {
UniversalLogger.logDebug(
"READ -> $deviceId $service $characteristic",
withTimestamp: true,
);
try {
final c = _getCharacteristic(deviceId, service, characteristic);
final data = await c.readValue();
return Uint8List.fromList(data);
} on BlueZFailedException catch (e) {
UniversalLogger.logError(
"READ_FAILED <- $deviceId $service $characteristic ${e.message}",
withTimestamp: true,
);
throw e.toUniversalBleException(
defaultCode: UniversalBleErrorCode.readFailed,
);
@@ -311,6 +328,10 @@ class UniversalBleLinux extends UniversalBlePlatform {
String characteristic,
Uint8List value,
BleOutputProperty bleOutputProperty) async {
UniversalLogger.logDebug(
"WRITE -> $deviceId $service $characteristic len=${value.length} property=${bleOutputProperty.name}",
withTimestamp: true,
);
try {
final c = _getCharacteristic(deviceId, service, characteristic);
if (bleOutputProperty == BleOutputProperty.withResponse) {
@@ -325,6 +346,10 @@ class UniversalBleLinux extends UniversalBlePlatform {
);
}
} on BlueZFailedException catch (e) {
UniversalLogger.logError(
"WRITE_FAILED <- $deviceId $service $characteristic ${e.message}",
withTimestamp: true,
);
throw e.toUniversalBleException(
defaultCode: UniversalBleErrorCode.writeFailed,
);
@@ -41,6 +41,15 @@ bool _deepEquals(Object? a, Object? b) {
return a == b;
}
enum UniversalBleLogLevel {
none,
error,
warning,
info,
debug,
verbose,
}
/// Unified error codes for all platforms
enum UniversalBleErrorCode {
unknownError,
@@ -478,30 +487,33 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is UniversalBleErrorCode) {
} else if (value is UniversalBleLogLevel) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else if (value is UniversalBleScanResult) {
} else if (value is UniversalBleErrorCode) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is UniversalBleService) {
writeValue(buffer, value.index);
} else if (value is UniversalBleScanResult) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else if (value is UniversalBleCharacteristic) {
} else if (value is UniversalBleService) {
buffer.putUint8(132);
writeValue(buffer, value.encode());
} else if (value is UniversalBleDescriptor) {
} else if (value is UniversalBleCharacteristic) {
buffer.putUint8(133);
writeValue(buffer, value.encode());
} else if (value is UniversalScanFilter) {
} else if (value is UniversalBleDescriptor) {
buffer.putUint8(134);
writeValue(buffer, value.encode());
} else if (value is UniversalManufacturerDataFilter) {
} else if (value is UniversalScanFilter) {
buffer.putUint8(135);
writeValue(buffer, value.encode());
} else if (value is UniversalManufacturerData) {
} else if (value is UniversalManufacturerDataFilter) {
buffer.putUint8(136);
writeValue(buffer, value.encode());
} else if (value is UniversalManufacturerData) {
buffer.putUint8(137);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@@ -512,20 +524,23 @@ class _PigeonCodec extends StandardMessageCodec {
switch (type) {
case 129:
final value = readValue(buffer) as int?;
return value == null ? null : UniversalBleErrorCode.values[value];
return value == null ? null : UniversalBleLogLevel.values[value];
case 130:
return UniversalBleScanResult.decode(readValue(buffer)!);
final value = readValue(buffer) as int?;
return value == null ? null : UniversalBleErrorCode.values[value];
case 131:
return UniversalBleService.decode(readValue(buffer)!);
return UniversalBleScanResult.decode(readValue(buffer)!);
case 132:
return UniversalBleCharacteristic.decode(readValue(buffer)!);
return UniversalBleService.decode(readValue(buffer)!);
case 133:
return UniversalBleDescriptor.decode(readValue(buffer)!);
return UniversalBleCharacteristic.decode(readValue(buffer)!);
case 134:
return UniversalScanFilter.decode(readValue(buffer)!);
return UniversalBleDescriptor.decode(readValue(buffer)!);
case 135:
return UniversalManufacturerDataFilter.decode(readValue(buffer)!);
return UniversalScanFilter.decode(readValue(buffer)!);
case 136:
return UniversalManufacturerDataFilter.decode(readValue(buffer)!);
case 137:
return UniversalManufacturerData.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
@@ -1061,6 +1076,30 @@ class UniversalBlePlatformChannel {
return (pigeonVar_replyList[0] as int?)!;
}
}
Future<void> setLogLevel(UniversalBleLogLevel logLevel) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setLogLevel$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture =
pigeonVar_channel.send(<Object?>[logLevel]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
}
/// Native -> Flutter
@@ -1073,8 +1112,8 @@ abstract class UniversalBleCallbackChannel {
void onScanResult(UniversalBleScanResult result);
void onValueChanged(
String deviceId, String characteristicId, Uint8List value);
void onValueChanged(String deviceId, String characteristicId, Uint8List value,
int? timestamp);
void onConnectionChanged(String deviceId, bool connected, String? error);
@@ -1192,9 +1231,10 @@ abstract class UniversalBleCallbackChannel {
final Uint8List? arg_value = (args[2] as Uint8List?);
assert(arg_value != null,
'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged was null, expected non-null Uint8List.');
final int? arg_timestamp = (args[3] as int?);
try {
api.onValueChanged(
arg_deviceId!, arg_characteristicId!, arg_value!);
api.onValueChanged(arg_deviceId!, arg_characteristicId!, arg_value!,
arg_timestamp);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
@@ -164,6 +164,10 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
);
}
@override
Future<void> setLogLevel(BleLogLevel logLevel) => _executeWithErrorHandling(
() => _channel.setLogLevel(logLevel.toUniversalBleLogLevel()));
/// To set listeners
void _setupListeners() {
UniversalBleCallbackChannel.setUp(_UniversalBleCallbackHandler(
@@ -253,8 +257,12 @@ class _UniversalBleCallbackHandler extends UniversalBleCallbackChannel {
@override
void onValueChanged(
String deviceId, String characteristicId, Uint8List value) =>
valueChanged(deviceId, characteristicId, value);
String deviceId,
String characteristicId,
Uint8List value,
int? timestamp,
) =>
valueChanged(deviceId, characteristicId, value, timestamp);
@override
void onPairStateChange(String deviceId, bool isPaired, String? error) =>
@@ -298,3 +306,14 @@ extension _ScanFilterExtension on ScanFilter? {
);
}
}
extension _BleLogLevelExtension on BleLogLevel {
UniversalBleLogLevel toUniversalBleLogLevel() => switch (this) {
BleLogLevel.none => UniversalBleLogLevel.none,
BleLogLevel.error => UniversalBleLogLevel.error,
BleLogLevel.warning => UniversalBleLogLevel.warning,
BleLogLevel.info => UniversalBleLogLevel.info,
BleLogLevel.debug => UniversalBleLogLevel.debug,
BleLogLevel.verbose => UniversalBleLogLevel.verbose,
};
}
+15 -3
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:universal_ble/src/utils/cache_handler.dart';
import 'package:universal_ble/src/utils/universal_ble_stream_controller.dart';
import 'package:universal_ble/src/utils/universal_logger.dart';
import 'package:universal_ble/universal_ble.dart';
abstract class UniversalBlePlatform {
@@ -85,6 +86,9 @@ abstract class UniversalBlePlatform {
List<String>? withServices,
);
Future<void> setLogLevel(BleLogLevel logLevel) async =>
UniversalLogger.setLogLevel(logLevel);
bool receivesAdvertisements(String deviceId) => true;
/// Streams
@@ -142,6 +146,7 @@ abstract class UniversalBlePlatform {
String deviceId,
String characteristicId,
Uint8List value,
int? timestamp,
) {
characteristicId = BleUuidParser.string(characteristicId);
_valueStreamController.add((
@@ -150,7 +155,7 @@ abstract class UniversalBlePlatform {
value: value,
));
try {
onValueChange?.call(deviceId, characteristicId, value);
onValueChange?.call(deviceId, characteristicId, value, timestamp);
} catch (_) {}
}
@@ -176,10 +181,17 @@ abstract class UniversalBlePlatform {
// Callback types
typedef OnConnectionChange = void Function(
String deviceId, bool isConnected, String? error);
String deviceId,
bool isConnected,
String? error,
);
typedef OnValueChange = void Function(
String deviceId, String characteristicId, Uint8List value);
String deviceId,
String characteristicId,
Uint8List value,
int? timestamp,
);
typedef OnScanResult = void Function(BleDevice scanResult);
@@ -162,6 +162,10 @@ class UniversalBleWeb extends UniversalBlePlatform {
String characteristic,
BleInputProperty bleInputProperty,
) async {
UniversalLogger.logDebug(
"SET_NOTIFY -> $deviceId $service $characteristic input=${bleInputProperty.name}",
withTimestamp: true,
);
final bleCharacteristic = await _getBleCharacteristic(
deviceId: deviceId,
serviceId: service,
@@ -185,10 +189,20 @@ class UniversalBleWeb extends UniversalBlePlatform {
await bleCharacteristic.startNotifications();
_characteristicStreamList[characteristicKey] =
bleCharacteristic.value.listen((ByteData event) {
final preview = event.buffer
.asUint8List()
.take(8)
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();
UniversalLogger.logVerbose(
"NOTIFY <- $deviceId $characteristic len=${event.lengthInBytes} data=$preview",
withTimestamp: true,
);
updateCharacteristicValue(
deviceId,
characteristic,
event.buffer.asUint8List(),
DateTime.now().millisecondsSinceEpoch,
);
});
} else {
@@ -205,6 +219,10 @@ class UniversalBleWeb extends UniversalBlePlatform {
Uint8List value,
BleOutputProperty bleOutputProperty,
) async {
UniversalLogger.logDebug(
"WRITE -> $deviceId $service $characteristic len=${value.length} property=${bleOutputProperty.name}",
withTimestamp: true,
);
final bleCharacteristic = await _getBleCharacteristic(
deviceId: deviceId,
serviceId: service,
@@ -234,6 +252,10 @@ class UniversalBleWeb extends UniversalBlePlatform {
String characteristic, {
final Duration? timeout,
}) async {
UniversalLogger.logDebug(
"READ -> $deviceId $service $characteristic",
withTimestamp: true,
);
var bleCharacteristic = await _getBleCharacteristic(
deviceId: deviceId,
serviceId: service,
+63 -7
View File
@@ -1,24 +1,80 @@
import 'dart:developer';
import 'package:universal_ble/src/models/ble_log_level.dart';
class UniversalLogger {
static void logInfo(String message) {
static BleLogLevel _currentLogLevel = BleLogLevel.none;
static BleLogLevel get currentLogLevel => _currentLogLevel;
static void setLogLevel(BleLogLevel logLevel) {
_currentLogLevel = logLevel;
}
static void logError(
String message, {
bool withTimestamp = false,
}) {
if (!_allows(BleLogLevel.error)) return;
if (withTimestamp) {
final ts = DateTime.now().toIso8601String();
message = "[$ts] $message";
}
log(
'\x1B[31m$message\x1B[0m',
name: 'UniversalBle:ERROR',
);
}
static void logWarning(String message, {bool withTimestamp = false}) {
if (!_allows(BleLogLevel.warning)) return;
if (withTimestamp) {
final ts = DateTime.now().toIso8601String();
message = "[$ts] $message";
}
log(
'\x1B[33m$message\x1B[0m',
name: 'UniversalBle:WARN',
);
}
static void logInfo(String message, {bool withTimestamp = false}) {
if (!_allows(BleLogLevel.info)) return;
if (withTimestamp) {
final ts = DateTime.now().toIso8601String();
message = "[$ts] $message";
}
log(
message.toString(),
name: 'UniversalBle:INFO',
);
}
static void logError(String message) {
static void logDebug(String message, {bool withTimestamp = false}) {
if (!_allows(BleLogLevel.debug)) return;
if (withTimestamp) {
final ts = DateTime.now().toIso8601String();
message = "[$ts] $message";
}
log(
'\x1B[31m$message\x1B[31m',
name: 'UniversalBle:ERROR',
message.toString(),
name: 'UniversalBle:DEBUG',
);
}
static void logWarning(String message) {
static void logVerbose(String message, {bool withTimestamp = false}) {
if (!_allows(BleLogLevel.verbose)) return;
if (withTimestamp) {
final ts = DateTime.now().toIso8601String();
message = "[$ts] $message";
}
log(
'\x1B[33m$message\x1B[33m',
name: 'UniversalBle:WARN',
message.toString(),
name: 'UniversalBle:VERBOSE',
);
}
static bool _allows(BleLogLevel level) {
return level.index <= _currentLogLevel.index &&
_currentLogLevel != BleLogLevel.none;
}
}