Skip to content

feat: add attribute protocol errors#421

Open
acouvreur wants to merge 2 commits intotinygo-org:devfrom
acouvreur:add-ble-errors-codes
Open

feat: add attribute protocol errors#421
acouvreur wants to merge 2 commits intotinygo-org:devfrom
acouvreur:add-ble-errors-codes

Conversation

@acouvreur
Copy link

This will allow to return protocol level errors

My goal is to add support for error code extraction from returned errors.

Especially in windows where you only get the async result code (aborted or failure).

I have tested the extraction of the attribute protocol error code successfully but the code is not included in this pull request because it required changes in the winrt-go library.

The errors have been implemented from https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-54/out/en/host/attribute-protocol--att-.html#UUID-eefd3e8d-9b16-3af8-1fb4-fa90f52262e8

Wether or not we actually use them in the code, I thing they are useful just like the generated services and characteristics UUIDs in this repo

This will allow to return protocol level errors
acouvreur added a commit to acouvreur/winrt-go that referenced this pull request Mar 17, 2026
This will be used to retrieve the error details from an error within a AsyncOperationCompletedHandler.

Related work: tinygo-org/bluetooth#421
@acouvreur
Copy link
Author

Note that:

errors.go Outdated
}

func (e *AttributeProtocolError) Error() string {
return fmt.Sprintf("ATT error 0x%02X (%s): %s", e.Code, e.Name, e.Description)
Copy link

@FilipVranesevic FilipVranesevic Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fmt on embedded targets can increase the binary size significantly. strings.Builder and hex.EncodeToString would be much more efficient.

Suggested change
return fmt.Sprintf("ATT error 0x%02X (%s): %s", e.Code, e.Name, e.Description)
b := strings.Builder{}
b.WriteString("ATT error 0x")
b.WriteString(hex.EncodeToString([]byte{e.Code}))
b.WriteString(" (")
b.WriteString(e.Name)
b.WriteString("): ")
b.WriteString(e.Description)
return b.String()

it does not look pretty, but the code size and allocation count difference is significant:

strings.Builder

tinygo build -print-allocs=. -size short -target nicenano
./test/main.go:24.1,24.23 1 0
./tinygo/src/runtime/string.go:76.1,76.45 1 0
./tinygo/src/runtime/baremetal.go:44.1,44.24 1 0
./tinygo/src/runtime/slice.go:39.1,39.38 1 0
./tinygo/src/internal/task/task_stack.go:43.1,43.39 1 0
./tinygo/src/internal/task/task_stack.go:75.1,75.13 1 0
   code    data     bss |   flash     ram
   7296     116    4736 |    7412    4852

fmt.Sprintf

tinygo build -print-allocs=. -size short -target nicenano
test/main.go:36.1,36.54 1 0
test/main.go:34.1,34.19 1 0
test/main.go:24.1,24.79 1 0
test/main.go:25.1,25.23 1 0
test/main.go:24.1,24.79 1 0
test/main.go:24.1,24.79 1 0
/usr/local/go/src/fmt/print.go:509.1,509.63 1 0
/usr/local/go/src/fmt/print.go:881.1,881.34 1 0
/usr/local/go/src/fmt/format.go:539.1,539.21 1 0
/usr/local/go/src/fmt/format.go:211.1,211.28 1 0
/usr/local/go/src/fmt/format.go:150.1,150.28 1 0
/usr/local/go/src/fmt/format.go:75.1,75.34 1 0
/usr/local/go/src/fmt/print.go:147.1,147.36 1 0
/usr/local/go/src/strconv/ftoa.go:287.1,287.22 1 0
/usr/local/go/src/strconv/ftoa.go:268.1,268.22 1 0
/usr/local/go/src/strconv/ftoa.go:166.1,166.18 1 0
/usr/local/go/src/strconv/ftoa.go:147.1,147.18 1 0
/usr/local/go/src/strconv/ftoa.go:119.1,119.18 1 0
/usr/local/go/src/strconv/quote.go:35.1,35.53 1 0
/usr/local/go/src/internal/fmtsort/sort.go:134.1,134.49 1 0
/usr/local/go/src/internal/fmtsort/sort.go:57.1,57.32 1 0
tinygo/src/runtime/string.go:76.1,76.45 1 0
tinygo/src/runtime/string.go:63.1,63.47 1 0
tinygo/src/runtime/hashmap.go:49.1,49.44 1 0
tinygo/src/runtime/baremetal.go:44.1,44.24 1 0
tinygo/src/runtime/slice.go:39.1,39.38 1 0
tinygo/src/internal/reflectlite/strconv.go:239.1,239.35 1 0
tinygo/src/internal/reflectlite/type.go:418.1,418.22 1 0
tinygo/src/internal/reflectlite/value.go:1751.1,1751.35 1 0
tinygo/src/internal/reflectlite/value.go:882.1,882.53 1 0
tinygo/src/internal/reflectlite/value.go:875.1,875.52 1 0
tinygo/src/internal/reflectlite/value.go:273.1,273.61 1 0
tinygo/src/internal/reflectlite/value.go:247.1,247.53 1 0
tinygo/src/internal/reflectlite/value.go:1041.1,1041.53 1 0
tinygo/src/internal/reflectlite/value.go:806.1,806.51 1 0
tinygo/src/internal/reflectlite/value.go:441.1,441.52 1 0
tinygo/src/internal/reflectlite/value.go:650.1,650.54 1 0
tinygo/src/internal/reflectlite/value.go:658.1,658.54 1 0
tinygo/src/internal/reflectlite/value.go:667.1,667.52 1 0
tinygo/src/internal/reflectlite/value.go:598.1,598.53 1 0
tinygo/src/internal/reflectlite/value.go:541.1,541.52 1 0
tinygo/src/internal/reflectlite/value.go:488.1,488.51 1 0
tinygo/src/internal/reflectlite/value.go:630.1,630.55 1 0
tinygo/src/internal/reflectlite/value.go:1163.1,1163.19 1 0
tinygo/src/internal/reflectlite/value.go:1218.1,1218.56 1 0
tinygo/src/internal/task/task_stack.go:43.1,43.39 1 0
tinygo/src/internal/task/task_stack.go:75.1,75.13 1 0
   code    data     bss |   flash     ram
  55288    1508    4744 |   56796    6252

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not know that, should we add golangci lint with a rule for fmt ?

errors.go Outdated
Comment on lines +10 to +47
// Name is a short identifier for the error.
Name string
// Description is the human-readable description from the spec.
Description string
}

func (e *AttributeProtocolError) Error() string {
return fmt.Sprintf("ATT error 0x%02X (%s): %s", e.Code, e.Name, e.Description)
}

// ATT error codes from the Bluetooth Core Specification, Table 3.4.
var (
ErrAttInvalidHandle = &AttributeProtocolError{0x01, "Invalid Handle", "The attribute handle given was not valid on this server."}
ErrAttReadNotPermitted = &AttributeProtocolError{0x02, "Read Not Permitted", "The attribute cannot be read."}
ErrAttWriteNotPermitted = &AttributeProtocolError{0x03, "Write Not Permitted", "The attribute cannot be written."}
ErrAttInvalidPDU = &AttributeProtocolError{0x04, "Invalid PDU", "The attribute PDU was invalid."}
ErrAttInsufficientAuthentication = &AttributeProtocolError{0x05, "Insufficient Authentication", "The attribute requires authentication before it can be read or written."}
ErrAttRequestNotSupported = &AttributeProtocolError{0x06, "Request Not Supported", "ATT Server does not support the request received from the client."}
ErrAttInvalidOffset = &AttributeProtocolError{0x07, "Invalid Offset", "Offset specified was past the end of the attribute."}
ErrAttInsufficientAuthorization = &AttributeProtocolError{0x08, "Insufficient Authorization", "The attribute requires authorization before it can be read or written."}
ErrAttPrepareQueueFull = &AttributeProtocolError{0x09, "Prepare Queue Full", "Too many prepare writes have been queued."}
ErrAttNotFound = &AttributeProtocolError{0x0A, "Attribute Not Found", "No attribute found within the given attribute handle range."}
ErrAttNotLong = &AttributeProtocolError{0x0B, "Attribute Not Long", "The attribute cannot be read using the ATT_READ_BLOB_REQ PDU."}
ErrAttInsufficientEncKeySize = &AttributeProtocolError{0x0C, "Encryption Key Size Too Short", "The Encryption Key Size used for encrypting this link is too short."}
ErrAttInvalidLength = &AttributeProtocolError{0x0D, "Invalid Attribute Value Length", "The attribute value length is invalid for the operation."}
ErrAttUnlikelyError = &AttributeProtocolError{0x0E, "Unlikely Error", "The attribute request that was requested has encountered an error that was unlikely, and therefore could not be completed as requested."}
ErrAttInsufficientEncryption = &AttributeProtocolError{0x0F, "Insufficient Encryption", "The attribute requires encryption before it can be read or written."}
ErrAttUnsupportedGroupType = &AttributeProtocolError{0x10, "Unsupported Group Type", "The attribute type is not a supported grouping attribute as defined by a higher layer specification."}
ErrAttInsufficientResources = &AttributeProtocolError{0x11, "Insufficient Resources", "Insufficient Resources to complete the request."}
ErrAttOutOfSync = &AttributeProtocolError{0x12, "Database Out Of Sync", "The server requests the client to rediscover the database."}
ErrAttValueNotAllowed = &AttributeProtocolError{0x13, "Value Not Allowed", "The attribute parameter value was not allowed."}
)

// attErrorsByCode maps ATT error codes to their predefined error values.
var attErrorsByCode = map[uint8]*AttributeProtocolError{
0x01: ErrAttInvalidHandle,
0x02: ErrAttReadNotPermitted,
0x03: ErrAttWriteNotPermitted,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having global exported variables like this can potentially lead to issues if for example somewhere in the user code these variables were unintentionally mutated.

Maybe the code could be the error type which could be const:

type AttributeProtocolError uint8

const (
	ErrAttInvalidHandle     = AttributeProtocolError(0x01)
	ErrAttReadNotPermitted  = AttributeProtocolError(0x02)
	ErrAttWriteNotPermitted = AttributeProtocolError(0x03)
	...
)

Then have a map from error code to error details:

type errorDetails struct {
	// name is a short identifier for the error.
	name string
	// description is the human-readable description from the spec.
	description string
}

// this map can also potentially be a slice
var attErrorDetails = map[AttributeProtocolError]errorDetails{
	ErrAttInvalidHandle:     errorDetails{"Invalid Handle", "The attribute handle given was not valid on this server."},
	ErrAttReadNotPermitted:  errorDetails{"Read Not Permitted", "The attribute cannot be read."},
	ErrAttWriteNotPermitted: errorDetails{"Write Not Permitted", "The attribute cannot be written."},
	...
}

func detailsFromCode(code AttributeProtocolError) errorDetails {
	if err, ok := attErrorDetails[code]; ok {
		return err
	}
	...
}

If Name and Description need to be exposed directly, they could be methods on AttributeProtocolError

@acouvreur
Copy link
Author

acouvreur commented Mar 17, 2026

@deadprogram @FilipVranesevic

  • I have removed all fmt usage
  • I have changed the error to a tyupe uint8
  • All protocol errors are const
  • The user can retrieve the error details using Name(), Descriptio() which calls the private function details()

I don't really like the fact that those helper methods must do a map lookup everytime, even though it is a very small map.

What do you think ? Is there some optimization to do ?

  • Cache the attErrorDetails result
  • Make the attErrorDetails a pointer to avoid recreating / copying the details everytime ?

acouvreur added a commit to acouvreur/winrt-go that referenced this pull request Mar 17, 2026
This will be used to retrieve the error details from an error within a AsyncOperationCompletedHandler.

Related work: tinygo-org/bluetooth#421
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.

4 participants