Skip to content
Merged
Changes from 1 commit
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
81 changes: 81 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package bluetooth

import "fmt"
Comment thread
acouvreur marked this conversation as resolved.
Outdated

// AttributeProtocolError represents an error from the Attribute Protocol (ATT)
// as defined in the Bluetooth Core Specification, Section 3.4.1.1 (ATT_ERROR_RSP).
type AttributeProtocolError struct {
// Code is the 1-byte ATT error code.
Code uint8
// 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)

@FilipVranesevic FilipVranesevic Mar 17, 2026

Copy link
Copy Markdown

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
Copy Markdown
Member 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 ?

}

// 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,

Copy link
Copy Markdown

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

0x04: ErrAttInvalidPDU,
0x05: ErrAttInsufficientAuthentication,
0x06: ErrAttRequestNotSupported,
0x07: ErrAttInvalidOffset,
0x08: ErrAttInsufficientAuthorization,
0x09: ErrAttPrepareQueueFull,
0x0A: ErrAttNotFound,
0x0B: ErrAttNotLong,
0x0C: ErrAttInsufficientEncKeySize,
0x0D: ErrAttInvalidLength,
0x0E: ErrAttUnlikelyError,
0x0F: ErrAttInsufficientEncryption,
0x10: ErrAttUnsupportedGroupType,
0x11: ErrAttInsufficientResources,
0x12: ErrAttOutOfSync,
0x13: ErrAttValueNotAllowed,
}

// ATTErrorFromCode returns an *AttributeProtocolError for the given ATT error code.
// For known codes it returns the predefined variable; for application errors (0x80–0x9F)
// and common profile/service errors (0xE0–0xFF) it returns a dynamically created error.
// Unknown codes return a generic ATT error.
func ATTErrorFromCode(code uint8) *AttributeProtocolError {
if err, ok := attErrorsByCode[code]; ok {
return err
}
if code >= 0x80 && code <= 0x9F {
return &AttributeProtocolError{code, "Application Error", fmt.Sprintf("Application error code defined by a higher layer specification (0x%02X).", code)}
}
if code >= 0xE0 && code <= 0xFF {
return &AttributeProtocolError{code, "Common Profile and Service Error", fmt.Sprintf("Common profile and service error code (0x%02X).", code)}
}
return &AttributeProtocolError{code, "Reserved", fmt.Sprintf("Reserved for future use (0x%02X).", code)}
}
Loading