Skip to content

Map ATT errors to UniversalBleErrorCode on Apple - #350

Merged
fotiDim merged 1 commit into
Navideck:mainfrom
alvynfash:apple-att-error-codes
Oct 16, 2026
Merged

fotiDim merged 1 commit into
Navideck:mainfrom
alvynfash:apple-att-error-codes

Conversation

@alvynfash

@alvynfash alvynfash commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #348.

What

On iOS and macOS, a read, write or subscription that the peripheral refuses now reports the matching UniversalBleErrorCode (insufficientEncryption, insufficientAuthentication, readNotPermitted, …), as Android already does. Until now every such refusal reached Dart as unknownError.

Why it happened

The refusal arrives as an NSError in CBATTErrorDomain. Error.toFlutterError() passed its numeric code to mapErrorCodeToEnum, which matches names only ("failed", "devicenotfound", …). A number never matches, so the result was always .unknownError.

The change

  • attErrorToUniversalBleErrorCode maps every CBATTError.Code to the same value Android's gattStatusToUniversalBleErrorCode gives the same ATT code. A refusal therefore reads the same on both platforms.
  • toFlutterError() uses that mapping only for errors in CBATTErrorDomain. Every other domain goes through mapErrorCodeToEnum as before, because CBErrorDomain reuses the same small numbers for other meanings (its 15 is encryption timed out, ATT's 15 is insufficient encryption).
  • message and details are unchanged: details still carries the numeric code.

Tests

example/ios/RunnerTests held the Flutter template test, which no longer compiles (UniversalBlePlugin has had no handle method since the move to Pigeon). It now tests this mapping:

  • a refused operation carries its ATT code (an insufficientEncryption refusal);
  • every ATT code maps as it does on Android;
  • a CBErrorDomain error with the same number is not read as an ATT code.

To run them: xcodebuild test -workspace example/ios/Runner.xcworkspace -scheme Runner -destination '<simulator>'. They pass on an iPhone 17 simulator (iOS 27). With the old toFlutterError() the first test fails.

With Xcode 27 the example needs IPHONEOS_DEPLOYMENT_TARGET=15.0 on the command line, because its 13.1 is below that SDK's minimum. This PR does not change the deployment target.

flutter analyze and flutter test pass (285 tests).

Seen on hardware

A Leica DISTO D2-2 refuses an unpaired read with "Encryption is insufficient" (ATT 15). That read is what makes iOS pair the device, and the app has to tell this refusal apart from any other failure.

A GATT operation the peripheral refuses fails with an NSError in
CBATTErrorDomain. toFlutterError() looked its numeric code up in
mapErrorCodeToEnum, which matches names only, so every refusal reached Dart
as unknownError. ATT codes now map as Android's
gattStatusToUniversalBleErrorCode maps them; other domains are unchanged.

The example's RunnerTests held a template test that no longer compiled
(UniversalBlePlugin has no handle method since the move to Pigeon); it now
tests this mapping.
@fotiDim
fotiDim merged commit 3a976ef into Navideck:main Oct 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS/macOS: a refused read or write arrives as unknownError, losing its ATT code

2 participants