Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 3.1.1
* Apple: a read, write or subscription the peripheral refuses now reports its ATT error as the matching `UniversalBleErrorCode` (`insufficientEncryption`, `insufficientAuthentication`, `readNotPermitted`, ...), as Android does, instead of `unknownError`. The error code is still in `details`. Codes in `CBErrorDomain` are unchanged: its numbers overlap ATT's with other meanings (#348).
* Windows: a read, write or subscription the peripheral refuses now reports its ATT error as the matching `UniversalBleErrorCode` (`insufficientEncryption`, `insufficientAuthentication`, `readNotPermitted`, ...) instead of the generic `failed`, as Android already does. Characteristic/descriptor reads and writes and `SetNotifiable` are covered; service/characteristic discovery at connect time is unchanged. Application-defined codes `0x80`-`0xFF` now report `unknownError`. The raw ATT error code is still in `details`.
* Linux: take the ATT error code from the BlueZ failure message only when it follows `ATT error:`, so an unrelated hex value in the message is no longer mistaken for the code.
* Bump `bluez` to `^0.9.0`.
* Bump the `pigeon` dev dependency to `^29.0.0`.
* Regenerate the Pigeon bindings with Pigeon 29. The async platform channels now use `@asyncCallback`, so the existing callback-based native implementations (Swift completion handlers, Kotlin callbacks) are kept as-is rather than migrated to native `async`/`suspend`. No public API change.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,8 @@ try {

The error parser automatically converts platform-specific error formats (strings, numeric codes, PlatformExceptions) into the unified `UniversalBleErrorCode` enum, ensuring consistent error handling across all platforms.

When a peripheral refuses a characteristic or descriptor read/write, or a subscription, Android, Apple and Windows report the ATT error as the matching `UniversalBleErrorCode` (`insufficientEncryption`, `insufficientAuthentication`, `readNotPermitted`, ...) instead of a generic `failed`/`unknownError`. The raw native code is available in `UniversalBleException.details`. Service/characteristic discovery at connect time, and the Linux and Web backends, may still report a generic code.

## Peripheral Mode

`universal_ble` provides peripheral mode through `UniversalBlePeripheral`, so your app can advertise as a peripheral "server" in addition to client mode.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.navideck.universal_ble

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

/**
* Drives [gattStatusToUniversalBleErrorCode] from the shared conformance fixture
* (`test/fixtures/att_error_cases.tsv`, exposed as a test resource by
* `android/build.gradle`), so the ATT error mapping stays identical to the Dart
* and Windows implementations.
*/
internal class UniversalBleAttErrorConformanceTest {

@Test
fun matchesTheSharedConformanceFixture() {
val stream = javaClass.getResourceAsStream("/att_error_cases.tsv")
?: error("Fixture not found on the test classpath")
val rows = stream.bufferedReader().readLines()
.filter { it.isNotBlank() && !it.startsWith("#") }
.map { it.split("|") }
.drop(1) // header row
assertTrue(rows.isNotEmpty(), "Fixture should contain cases")

for (columns in rows) {
assertEquals(3, columns.size, "Bad fixture row: ${columns.joinToString("|")}")
val attCode = columns[0].removePrefix("0x").toInt(16)
val expected = columns[1].toInt()
assertEquals(
expected,
gattStatusToUniversalBleErrorCode(attCode).raw,
"ATT ${columns[0]} (${columns[2]})",
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,14 @@ func attErrorToUniversalBleErrorCode(_ code: Int) -> UniversalBleErrorCode {
}
}

/// Creates a PigeonError with the error code enum in details
/// Creates a PigeonError carrying the error code enum's raw value
func createFlutterError(
code: UniversalBleErrorCode,
message: String? = nil,
details: String? = nil
) -> PigeonError {
// Pass the enum's rawValue (Int) in code as string, and enum name in details
// `code` and `details` are both numeric strings: the enum's raw value, unless
// the caller passes a native code (e.g. the ATT error) in details.
return PigeonError(
code: code.rawValue.description,
message: message,
Expand Down
18 changes: 4 additions & 14 deletions lib/src/universal_ble_linux/universal_ble_linux.dart
Original file line number Diff line number Diff line change
Expand Up @@ -798,20 +798,10 @@ extension on BlueZFailedException {
);
}

/// Extract error code from message and parse into decimal
/// example: 'Operation failed with ATT error: 0x90' => 144
String? get errorCode {
try {
RegExp regExp = RegExp(r'0x\w+');
Match? match = regExp.firstMatch(message);
String? code = match?.group(0);
if (code == null) return null;
int? decimalValue = int.tryParse(code.replaceFirst('0x', ''), radix: 16);
return decimalValue?.toString() ?? code;
} catch (e) {
return null;
}
}
/// The ATT error code parsed out of the BlueZ failure message as a decimal
/// string, e.g. 'Operation failed with ATT error: 0x90' => '144'.
String? get errorCode =>
UniversalBleErrorParser.extractBlueZAttErrorCode(message);
}

extension BlueZDeviceExtension on BlueZDevice {
Expand Down
22 changes: 22 additions & 0 deletions lib/src/utils/universal_ble_error_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ class UniversalBleErrorParser {
return UniversalBleErrorCode.unknownError;
}

/// Extracts the ATT error code from a BlueZ GATT failure message as a decimal
/// string, e.g. `'Operation failed with ATT error: 0x90'` => `'144'`, or
/// `null` when the message carries no code.
///
/// BlueZ reports a GATT protocol error as `org.bluez.Error.Failed` with an
/// "ATT error: 0xNN" message. The phrase is matched first (case-insensitively),
/// so an unrelated hex value elsewhere in the message is not mistaken for the
/// ATT code. Only when the phrase is absent is the first `0x`-prefixed hex token
/// (lower-case prefix) used as a best-effort fallback; a message with no such
/// token returns `null`.
static String? extractBlueZAttErrorCode(String message) {
final attCode = RegExp(
r'ATT error:\s*0x([0-9a-fA-F]+)',
caseSensitive: false,
).firstMatch(message)?.group(1);
final hex =
attCode ?? RegExp(r'0x([0-9a-fA-F]+)').firstMatch(message)?.group(1);
if (hex == null) return null;
final decimalValue = int.tryParse(hex, radix: 16);
return decimalValue?.toString() ?? '0x$hex';
}

static UniversalBleErrorCode? _parseStringErrorCode(String code) {
switch (code.toLowerCase()) {
case 'failed to get device':
Expand Down
52 changes: 52 additions & 0 deletions test/att_error_conformance_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Drives the Dart ATT error mapping from the shared conformance fixture
// (test/fixtures/att_error_cases.tsv), so its semantics stay identical to the
// Android and Windows implementations.
//
// Skipped on the web test run: the fixture is read from disk.
@TestOn('vm')
library;

import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:universal_ble/src/utils/universal_ble_error_parser.dart';

const _fixturePath = 'test/fixtures/att_error_cases.tsv';

void main() {
final file = File(_fixturePath);
if (!file.existsSync()) {
throw StateError(
'Fixture not found at $_fixturePath (run tests from the package root)',
);
}

final rows = file
.readAsLinesSync()
.where((line) => line.isNotEmpty && !line.startsWith('#'))
.toList();
rows.removeAt(0); // header

test('maps every shared ATT error code like the other platforms', () {
expect(rows, isNotEmpty);

for (final row in rows) {
final columns = row.split('|');
expect(
columns.length,
3,
reason: 'Fixture row "$row" must have 3 columns',
);

final attCode = int.parse(columns[0].replaceFirst('0x', ''), radix: 16);
final expected = int.parse(columns[1]);

final code = UniversalBleErrorParser.getCode(attCode);
expect(
code.index,
expected,
reason: 'ATT ${columns[0]} (${columns[2]})',
);
}
});
}
42 changes: 42 additions & 0 deletions test/fixtures/att_error_cases.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Shared ATT error-mapping conformance cases.
#
# Consumed by:
# * test/att_error_conformance_test.dart (Dart)
# * android/src/test/.../UniversalBleAttErrorConformanceTest.kt (Kotlin)
# * windows/test/att_error_mapper_test.cpp (C++)
# so the Android, Dart and Windows ATT mappings are held to one table instead of
# drifting apart. Apple's `attErrorToUniversalBleErrorCode` maps the same way but
# its XCTest (example/ios/RunnerTests) still asserts a hand-written list; it can
# be pointed at this fixture too.
#
# Columns, '|'-separated (exactly two separators per row):
# att_code | expected | expected_name
# att_code ATT error code in hex, as the platform delivers it
# expected the UniversalBleErrorCode raw value the code maps to
# expected_name the UniversalBleErrorCode name (documentation only)
#
# Codes 0x80-0xFF are application-defined: they are not protocol errors, so the
# mappings driven by this fixture (Dart, Android, Windows) report them as
# unknownError. Linux is not driven by this fixture: its wrapper falls back to the
# caller's default code (readFailed / writeFailed / ...) whenever the ATT code
# maps to unknownError.
att_code|expected|expected_name
0x01|26|invalidHandle
0x02|28|readNotPermitted
0x03|30|writeNotPermitted
0x04|25|invalidPdu
0x05|50|insufficientAuthentication
0x06|33|operationNotSupported
0x07|23|invalidOffset
0x08|51|insufficientAuthorization
0x09|36|operationInProgress
0x0A|19|serviceNotFound
0x0B|24|invalidAttributeLength
0x0C|53|insufficientKeySize
0x0D|24|invalidAttributeLength
0x0E|1|failed
0x0F|52|insufficientEncryption
0x10|33|operationNotSupported
0x11|1|failed
0x80|0|unknownError
0xFF|0|unknownError
35 changes: 35 additions & 0 deletions test/universal_ble_error_parser_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,40 @@ void main() {
);
});
});

group('extractBlueZAttErrorCode', () {
test('reads the ATT code that follows the phrase', () {
expect(
UniversalBleErrorParser.extractBlueZAttErrorCode(
'GDBus.Error:org.bluez.Error.Failed: Operation failed with ATT '
'error: 0x0F',
),
'15',
);
});

test('prefers the ATT code over an unrelated hex value', () {
expect(
UniversalBleErrorParser.extractBlueZAttErrorCode(
'write failed (handle 0xdead) with ATT error: 0x0f',
),
'15',
);
});

test('falls back to a bare hex value when the phrase is absent', () {
expect(
UniversalBleErrorParser.extractBlueZAttErrorCode('error code 0x1A'),
'26',
);
});

test('returns null when the message has no code', () {
expect(
UniversalBleErrorParser.extractBlueZAttErrorCode('not connected'),
isNull,
);
});
});
});
}
2 changes: 2 additions & 0 deletions windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ list(APPEND PLUGIN_SOURCES
"src/scan_result_merge.h"
"src/scan_filter_matcher.cpp"
"src/scan_filter_matcher.h"
"src/att_error_mapper.cpp"
"src/att_error_mapper.h"
"src/universal_ble_filter_util.cpp"
"src/universal_ble_filter_util.h"
"src/universal_ble_thread_safe.h"
Expand Down
62 changes: 62 additions & 0 deletions windows/src/att_error_mapper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#include "att_error_mapper.h"

namespace universal_ble {

// ATT error codes (Bluetooth Core Spec, Vol 3, Part F, 3.4.1.1).
//
// The right-hand values are the `UniversalBleErrorCode` integer values from
// windows/src/generated/universal_ble.g.h. They are pinned there (the enum uses
// explicit `= N` values) and shared with the Dart and Android layers, so they
// do not shift. Kept in lockstep with Android's
// `gattStatusToUniversalBleErrorCode` and Apple's
// `attErrorToUniversalBleErrorCode`:
//
// 1 kFailed 19 kServiceNotFound
// 23 kInvalidOffset 24 kInvalidAttributeLength
// 25 kInvalidPdu 26 kInvalidHandle
// 28 kReadNotPermitted 30 kWriteNotPermitted
// 33 kOperationNotSupported 36 kOperationInProgress
// 50 kInsufficientAuthentication 51 kInsufficientAuthorization
// 52 kInsufficientEncryption 53 kInsufficientKeySize
int AttErrorToUniversalBleErrorCodeValue(uint8_t att_error) {
switch (att_error) {
case 0x01:
return 26; // kInvalidHandle
case 0x02:
return 28; // kReadNotPermitted
case 0x03:
return 30; // kWriteNotPermitted
case 0x04:
return 25; // kInvalidPdu
case 0x05:
return 50; // kInsufficientAuthentication
case 0x06:
return 33; // kOperationNotSupported (request not supported)
case 0x07:
return 23; // kInvalidOffset
case 0x08:
return 51; // kInsufficientAuthorization
case 0x09:
return 36; // kOperationInProgress (prepare queue full)
case 0x0A:
return 19; // kServiceNotFound (attribute not found)
case 0x0B:
return 24; // kInvalidAttributeLength (attribute not long)
case 0x0C:
return 53; // kInsufficientKeySize
case 0x0D:
return 24; // kInvalidAttributeLength
case 0x0E:
return 1; // kFailed (unlikely error)
case 0x0F:
return 52; // kInsufficientEncryption
case 0x10:
return 33; // kOperationNotSupported (unsupported group type)
case 0x11:
return 1; // kFailed (insufficient resources)
default:
return 0; // kUnknownError (incl. application-defined 0x80-0xFF)
}
}

} // namespace universal_ble
21 changes: 21 additions & 0 deletions windows/src/att_error_mapper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include <cstdint>

namespace universal_ble {

/// Maps a Bluetooth ATT error code (the `ProtocolError` byte of a GATT read,
/// write or descriptor-discovery result) to the raw value of the matching
/// `UniversalBleErrorCode`.
///
/// Mirrors Android's `gattStatusToUniversalBleErrorCode` and Apple's
/// `attErrorToUniversalBleErrorCode`, so a peripheral refusing a read, write or
/// subscription reports the same code on every platform. The result is the
/// `UniversalBleErrorCode` integer rather than the enum itself, which keeps
/// this free of flutter/winrt types so it can be unit tested without a Flutter
/// engine (see windows/test/att_error_mapper_test.cpp). Application-defined
/// codes (`0x80`-`0xFF`) carry no protocol meaning and map to `0`
/// (`kUnknownError`).
int AttErrorToUniversalBleErrorCodeValue(uint8_t att_error);

} // namespace universal_ble
Loading
Loading