ble

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: BSD-3-Clause Imports: 17 Imported by: 0

README

ble

ble is a Go Bluetooth Low Energy package for Linux (raw HCI sockets) and macOS (CoreBluetooth via cbgo).

This is a maintained fork of the dormant go-ble/ble (merge base 8c5522f). The module is renamed github.com/bdstark/ble and the API diverges deliberately — it will not be upstreamed. It exists to run an unattended BLE telemetry gateway, so the changes are biased toward one goal: the stack must never wedge, and every failure must surface as a typed, bounded error.

Linux is the production target. The macOS backend is for development; several of the guarantees below (bounded waits, connection-parameter control) apply only to the Linux stack.

  • Examples: bdstark/ble-examples
  • Test coverage of the fork's changes: COVERAGE.md (diff coverage of added lines: 91.7% overall, 99.6% excluding darwin/)
  • CI: GitHub Actions runs build/vet/test (plus -race and a repeated-run pass over the HCI/ATT/GATT packages) on Linux and macOS

Requires Go 1.26+. On Linux the process needs CAP_NET_ADMIN (or root) to open the HCI socket, and the interface must be free of a competing host stack (stop or mask bluetoothd, or run on a dedicated controller).

go get github.com/bdstark/ble

What changed relative to upstream

API
  • context.Context threads through the entire ble.Client request API. Every GATT operation — discovery, reads, writes, subscribe/unsubscribe, ExchangeMTU, ReadRSSI — takes a ctx that bounds the caller's wait. On expiry you get ctx.Err() (possibly wrapped; errors.Is against context.Canceled / context.DeadlineExceeded holds). Cancellation is best-effort at the transport layer: an in-flight ACL write finishes, but it is independently bounded by hci.ACLWriteTimeout. A request abandoned after it reached the wire still owns the ATT bearer — the next request first waits out that transaction (late response, or its 30 s spec deadline, which closes the bearer), so a canceled request can never cause a later one to consume a stale response. Teardown (CancelConnection) deliberately takes no context so a client can always be torn down.
  • Typed sentinel errors, stdlib wrapping. pkg/errors is gone. Failures wrap sentinels you can test with errors.Is: ble.ErrNotImplemented, ble.ErrInvalidConnParams, ble.ErrInvalidDataLength, and on Linux hci.ErrClosed (wraps io.ErrClosedPipe), hci.ErrCreditTimeout, hci.ErrCommandTimeout, hci.ErrConnUpdateTimeout, att.ErrSeqProtoTimeout, plus the spec-defined ble.ATTError and hci.ErrCommand code types.
  • Conn.UpdateParams(ctx, ble.ConnParams{...}) — LE Connection Update on a live central link, expressed in time.Duration units, validated against the spec ranges before touching the controller, blocking until the controller reports completion. Peer-requested (L2CAP) updates and local requests share the per-connection update slot, so they cannot misattribute each other's completion events.
  • Conn.SetDataLength(ctx, txOctets, txTime) — LE Data Length Extension request, validated by ble.ValidateDataLength; the negotiated values are readable via the Linux conn's DataLength() getter as LE Data Length Change events arrive.
  • API repairs: ReadRSSI performs a real HCI Read RSSI exchange (it returned a fabricated 0 upstream), DiscoverIncludedServices is implemented (upstream returned nil, nil), and Client.Name() reads the GAP Device Name characteristic, caching deterministic outcomes rather than re-querying on every call.
  • ble.Connect cancellation race fixed. Upstream decided "found" by which error the scan returned; a parent-context expiry could race the match, and Connect would block forever on a channel nobody would send to. Found-ness is now decided solely by draining a buffered found channel. This wedge was hit in production.
  • Options propagate setter errors instead of silently discarding them, and device init failures fail NewDevice/Init instead of returning a booby-trapped half-initialized HCI.
  • Notification handler contract: the []byte passed to a NotificationHandler is backed by a pooled buffer on Linux and is valid only for the duration of the call — copy it if you keep it.
Reliability (Linux HCI/ATT)
  • Every indefinite channel wait in the stack is bounded. Command responses, ACL buffer credits, connection updates, disconnects, connection cancels — all have timeouts, hoisted into package variables (the exported hci.ACLWriteTimeout is the public knob; the rest exist so tests can shrink them), so a dead controller or peer produces a typed error instead of a parked goroutine.
  • Controller-state reconciliation after abandoned commands. When a context expiry abandons an HCI command that the controller later executes anyway (the classic "Command Disallowed on every subsequent scan" wedge), the stack reconciles its scan/dial state instead of desyncing permanently.
  • sktLoop wedge classes closed. Socket death tears down all connections; reads/writes on a dying transport return hci.ErrClosed rather than stalling; Socket.Close is idempotent and no longer depends on the controller being alive to complete.
  • Connection lifecycle fixes: a lost Disconnect command no longer leaves a zombie connection; a failed connect no longer leaks its conn; one connection's wait for ACL credits no longer stalls writes on every other connection; mid-reassembly disconnects no longer panic.
  • ATT bearer discipline per spec: an ATT transaction timeout or an unconfirmed indication poisons the bearer and closes it (Vol 3, Part F), rather than leaving a half-dead exchange to corrupt the next request.
Protocol correctness
  • Client RX paths hardened against runt, malformed, stale, and hostile PDUs — a misbehaving peer gets an error or a disconnect, not a panic.
  • ATT server: nine inherited bugs fixed, among them three remotely triggerable panics, an ExecuteWrite panic, and PrepareWrite dropping the queued value. ExchangeMTU handling conforms to Vol 3, Part F 3.4.2.2.
  • GATT server subscription state records only peer-acknowledged CCCD writes, so notification state cannot outrun the peer.
  • Wire-format fixes: CommandReject.Marshal produced unsendable frames; SMP frames parsed the opcode from the wrong offset with an incorrect data length. Both fixed.
Performance
  • Advertising delivery runs through a bounded dispatcher with pooled buffers instead of upstream's goroutine-per-advertisement; the parsed advertisement packet is cached across field accesses.
  • Receive-path buffers are pooled where lifetimes provably allow it; ACL packet headers are built without reflection. Micro-benchmarks cover the hot paths.
Observability & housekeeping
  • Logging is stdlib log/slog via ble.SetLogger / ble.Logger() (logxi is gone), with atomic access so the logger can be swapped safely while a device runs. Hot-path debug sites check Enabled first, so debug-off costs nothing.
  • examples/ moved to bdstark/ble-examples; dead pre-cbgo darwin XPC code deleted; modern idiomatic Go throughout (any, min, slices, …).
  • The darwin backend's ctx.Done abandon paths can no longer deadlock the CoreBluetooth dispatch thread.
  • Diff coverage of every fork change is tracked in COVERAGE.md and measured with tools/diffcover.py.

Usage

Device setup
import (
    "github.com/bdstark/ble"
    "github.com/bdstark/ble/linux"
)

d, err := linux.NewDevice()           // opens and initializes the default HCI device
if err != nil {
    log.Fatalf("can't open BLE device: %v", err)
}
ble.SetDefaultDevice(d)

Options are validated and their errors surface:

d, err := linux.NewDevice(
    ble.OptDeviceID(1),               // hci1 instead of hci0
    ble.OptCentralRole(),
)
Scanning
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

err := ble.Scan(ctx, false, func(a ble.Advertisement) {
    fmt.Printf("%s %s rssi=%d\n", a.Addr(), a.LocalName(), a.RSSI())
}, nil)
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
    log.Printf("scan failed: %v", err)
}

The handler runs on the bounded advertising dispatcher — keep it fast, and copy anything you retain from the advertisement.

Connecting and discovering
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Connect scans until an advertisement matches, then dials it.
cln, err := ble.Connect(ctx, func(a ble.Advertisement) bool {
    return a.LocalName() == "my-sensor"
})
if err != nil {
    log.Fatalf("connect: %v", err)   // errors.Is(err, context.DeadlineExceeded) on timeout
}
defer cln.CancelConnection()

p, err := cln.DiscoverProfile(ctx, true)
if err != nil {
    log.Fatalf("discover: %v", err)
}

If you already know the address, dial directly: ble.Dial(ctx, ble.NewAddr("aa:bb:cc:dd:ee:ff")).

Reading and writing characteristics
char := p.FindCharacteristic(ble.NewCharacteristic(ble.MustParse("2a19"))) // Battery Level
if char == nil {
    log.Fatal("characteristic not found")
}

opCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

val, err := cln.ReadCharacteristic(opCtx, char)
if err != nil {
    log.Fatalf("read: %v", err)
}

err = cln.WriteCharacteristic(opCtx, char, []byte{0x01}, false /* with response */)
Notifications and indications
err := cln.Subscribe(ctx, char, false /* notify, true = indicate */, func(data []byte) {
    // data is only valid inside this call — it is backed by a pooled
    // buffer. Copy before sending it anywhere.
    buf := make([]byte, len(data))
    copy(buf, data)
    readings <- buf
})
if err != nil {
    log.Fatalf("subscribe: %v", err)
}

// Block until the peer drops or you decide to leave.
select {
case <-cln.Disconnected():
    log.Print("peer disconnected")
case <-ctx.Done():
    _ = cln.CancelConnection()
}
Connection parameters and data length (Linux, central role)
// Relax the connection interval on a live link, e.g. to share radio time
// across several concurrent connections.
err := cln.Conn().UpdateParams(ctx, ble.ConnParams{
    IntervalMin: 30 * time.Millisecond,
    IntervalMax: 50 * time.Millisecond,
    Latency:     0,
    Timeout:     4 * time.Second,
})
if errors.Is(err, ble.ErrInvalidConnParams) {
    // rejected locally before touching the controller
}

// Ask for the controller's maximum LE packet length (fewer, larger packets).
err = cln.Conn().SetDataLength(ctx, ble.DataLengthMaxTxOctets, ble.DataLengthMaxTxTime)

UpdateParams blocks until the controller reports the update complete. SetDataLength returns once the controller accepts the command; the negotiated values arrive asynchronously (readable on the Linux conn via DataLength()). On backends that manage these themselves (CoreBluetooth), both return a wrapped ble.ErrNotImplemented.

Error handling

Wrapped sentinels make failure modes distinguishable:

_, err := cln.ReadCharacteristic(ctx, char)
switch {
case errors.Is(err, context.DeadlineExceeded):
    // this call's ctx expired; the link may still be fine
case errors.Is(err, att.ErrSeqProtoTimeout):
    // ATT transaction timed out; the bearer is poisoned, reconnect
case errors.Is(err, hci.ErrClosed): // also matches io.ErrClosedPipe
    // transport is gone
}

var attErr ble.ATTError
if errors.As(err, &attErr) && attErr == ble.ErrReadNotPerm {
    // peer refused the read at the protocol level
}
Tuning timeouts

The bounded waits use package variables; adjust them before opening a device if your controller or peers need different ceilings:

hci.ACLWriteTimeout = 30 * time.Second  // wait for ACL buffer credits
Logging
ble.SetLogger(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
    Level: slog.LevelDebug,             // debug enables packet-level dumps
})))

ble.SetLogger is safe to call at any time — access is atomic, so reconfiguring logging on a running device doesn't race the stack's log sites. ble.Logger() returns the current logger (or slog.Default() if none was set).

Peripheral role (advertising a GATT server)
d, _ := linux.NewDevice(ble.OptPeripheralRole())
ble.SetDefaultDevice(d)

svc := ble.NewService(ble.MustParse("19fad5e6-0000-4a86-9e44-95b9cee0f730"))
svc.NewCharacteristic(ble.MustParse("19fad5e6-0001-4a86-9e44-95b9cee0f730")).
    HandleRead(ble.ReadHandlerFunc(func(req ble.Request, rsp ble.ResponseWriter) {
        rsp.Write([]byte("hello"))
    }))
if err := ble.AddService(svc); err != nil {
    log.Fatal(err)
}

ctx := ble.WithSigHandler(context.WithCancel(context.Background()))
log.Fatal(ble.AdvertiseNameAndServices(ctx, "my-device", svc.UUID))

Testing

The linux backend is pure Go above the socket layer, so linux/hci and linux/att build and test on any platform:

go build ./...
go vet ./...
go test ./...

Diff coverage of the fork's changes (see COVERAGE.md):

go test -count=1 -coverprofile=/tmp/cover.out -coverpkg=./...,github.com/bdstark/ble/... ./...
python3 tools/diffcover.py . 8c5522f..HEAD /tmp/cover.out

License

BSD-3-Clause, inherited from upstream — see LICENSE.

Documentation

Index

Constants

View Source
const (
	DataLengthMinTxOctets = 27    // minimum supported PDU payload
	DataLengthMaxTxOctets = 251   // maximum supported PDU payload
	DataLengthMinTxTime   = 328   // air time for a 27-octet PDU (µs)
	DataLengthMaxTxTime   = 17040 // air time for a 251-octet coded-PHY PDU (µs)
)

LE Data Length Extension permitted ranges for the host's preferred maximum transmission [Vol 6, Part B, 4.5.10]. TxOctets is a link-layer payload size in octets; TxTime is the corresponding air time in microseconds. The controller clamps the request to what it and the peer support, then reports the negotiated maximums in an LE Data Length Change event.

View Source
const DefaultMTU = 23

DefaultMTU defines the default MTU of ATT protocol including 3 bytes of ATT header.

View Source
const MaxMTU = 512 + 3

MaxMTU is maximum of ATT_MTU, which is 512 bytes of value length, plus 3 bytes of ATT header. The maximum length of an attribute value shall be 512 octets [Vol 3, Part F, 3.2.9]

Variables

View Source
var (
	GAPUUID         = UUID16(0x1800) // Generic Access
	GATTUUID        = UUID16(0x1801) // Generic Attribute
	CurrentTimeUUID = UUID16(0x1805) // Current Time Service
	DeviceInfoUUID  = UUID16(0x180A) // Device Information
	BatteryUUID     = UUID16(0x180F) // Battery Service
	HIDUUID         = UUID16(0x1812) // Human Interface Device

	PrimaryServiceUUID   = UUID16(0x2800)
	SecondaryServiceUUID = UUID16(0x2801)
	IncludeUUID          = UUID16(0x2802)
	CharacteristicUUID   = UUID16(0x2803)

	ClientCharacteristicConfigUUID = UUID16(0x2902)
	ServerCharacteristicConfigUUID = UUID16(0x2903)

	DeviceNameUUID        = UUID16(0x2A00)
	AppearanceUUID        = UUID16(0x2A01)
	PeripheralPrivacyUUID = UUID16(0x2A02)
	ReconnectionAddrUUID  = UUID16(0x2A03)
	PeferredParamsUUID    = UUID16(0x2A04)
	ServiceChangedUUID    = UUID16(0x2A05)
)

UUIDs ...

View Source
var (
	// ContextKeySig for SigHandler context
	ContextKeySig = ContextKey("sig")
	// ContextKeyCCC for per connection contexts
	ContextKeyCCC = ContextKey("ccc")
)
View Source
var ErrDefaultDevice = errors.New("default device is not set")

ErrDefaultDevice ...

View Source
var ErrEIRPacketTooLong = errors.New("max packet length is 31")

ErrEIRPacketTooLong is the error returned when an AdvertisingPacket or ScanResponsePacket is too long.

View Source
var ErrInvalidConnParams = errors.New("invalid connection parameters")

ErrInvalidConnParams is returned by ConnParams.Encode (and, wrapped, by Conn.UpdateParams) when a requested connection parameter falls outside the range the Bluetooth spec permits, or the fields are mutually inconsistent.

View Source
var ErrInvalidDataLength = errors.New("invalid data length parameters")

ErrInvalidDataLength is returned by ValidateDataLength (and, wrapped, by Conn.SetDataLength) when a requested LE data-length parameter falls outside the range the Bluetooth spec permits.

View Source
var ErrNotImplemented = errors.New("not implemented")

ErrNotImplemented means the functionality is not implemented.

View Source
var ErrUnsupportedOption = errors.New("unsupported option")

ErrUnsupportedOption is returned (wrapped with the option name) by a backend's setter when it cannot honor the option — e.g. setting a linux-only HCI option on the darwin backend. Match with errors.Is(err, ble.ErrUnsupportedOption).

Functions

func AddService

func AddService(svc *Service) error

AddService adds a service to database.

func AdvertiseIBeacon

func AdvertiseIBeacon(ctx context.Context, u UUID, major, minor uint16, pwr int8) error

AdvertiseIBeacon advertises iBeacon with specified parameters.

func AdvertiseIBeaconData

func AdvertiseIBeaconData(ctx context.Context, b []byte) error

AdvertiseIBeaconData advertise iBeacon with given manufacturer data.

func AdvertiseNameAndServices

func AdvertiseNameAndServices(ctx context.Context, name string, uuids ...UUID) error

AdvertiseNameAndServices advertises device name, and specified service UUIDs. It tres to fit the UUIDs in the advertising packet as much as possi If name doesn't fit in the advertising packet, it will be put in scan response.

func ApplyOptions

func ApplyOptions(dev DeviceOption, opts ...Option) error

ApplyOptions applies every option to dev and returns the collected errors joined with errors.Join — it does not stop at the first failure, so a caller passing several options learns about every one that was rejected, and errors.Is still matches each individual cause. Backends' Option methods and device constructors should apply user options through this helper instead of hand-rolled loops that keep only the last error.

func Contains

func Contains(s []UUID, u UUID) bool

Contains returns a boolean reporting whether u is in the slice s.

This is a plain membership test: a nil (or empty) slice contains nothing and yields false. Earlier versions returned true for a nil slice — "no filter matches everything" semantics — which made Contains a footgun for its obvious use. Call sites that want filter semantics must check for the nil filter themselves (as the discovery code in linux/gatt does).

func Logger

func Logger() *slog.Logger

Logger returns the logger in use: the one set by SetLogger, or slog.Default() if none was. Hot-path debug sites call Logger().Enabled first, so no formatting cost is paid while debug logging is off.

func Name

func Name(u UUID) string

Name returns name of know services, characteristics, or descriptors.

func RemoveAllServices

func RemoveAllServices() error

RemoveAllServices removes all services that are currently in the database.

func Reverse

func Reverse(u []byte) []byte

Reverse returns a reversed copy of u.

func Scan

func Scan(ctx context.Context, allowDup bool, h AdvHandler, f AdvFilter) error

Scan starts scanning. Duplicated advertisements will be filtered out if allowDup is set to false.

func SetDefaultDevice

func SetDefaultDevice(d Device)

SetDefaultDevice returns the default HCI device.

func SetLogger added in v0.2.0

func SetLogger(l *slog.Logger)

SetLogger routes this package's logs — and its subpackages' — to l, or to slog.Default() when l is nil. Safe to call at any time, including concurrently with active logging: the previous plain package variable raced every log site the moment it was reassigned after a device opened.

func SetServices

func SetServices(svcs []*Service) error

SetServices set the specified service to the database. It removes all currently added services, if any.

func Stop

func Stop() error

Stop detatch the GATT server from a peripheral device.

func ValidateDataLength

func ValidateDataLength(txOctets, txTime uint16) error

ValidateDataLength reports whether txOctets and txTime are within the ranges [Vol 6, Part B, 4.5.10] accepts for LE Set Data Length: TxOctets in [27, 251], TxTime in [328, 17040] µs. An out-of-range field returns ErrInvalidDataLength (wrapped, with detail); nil means the pair is valid. Pass DataLengthMaxTxOctets / DataLengthMaxTxTime (251 / 17040) to request the controller's ceiling.

func WithSigHandler

func WithSigHandler(ctx context.Context, cancel func()) context.Context

WithSigHandler ...

Types

type ATTError

type ATTError byte

ATTError is the error code of Attribute Protocol [Vol 3, Part F, 3.4.1.1].

const (
	ErrSuccess           ATTError = 0x00 // ErrSuccess measn the operation is success.
	ErrInvalidHandle     ATTError = 0x01 // ErrInvalidHandle means the attribute handle given was not valid on this server.
	ErrReadNotPerm       ATTError = 0x02 // ErrReadNotPerm eans the attribute cannot be read.
	ErrWriteNotPerm      ATTError = 0x03 // ErrWriteNotPerm eans the attribute cannot be written.
	ErrInvalidPDU        ATTError = 0x04 // ErrInvalidPDU means the attribute PDU was invalid.
	ErrAuthentication    ATTError = 0x05 // ErrAuthentication means the attribute requires authentication before it can be read or written.
	ErrReqNotSupp        ATTError = 0x06 // ErrReqNotSupp means the attribute server does not support the request received from the client.
	ErrInvalidOffset     ATTError = 0x07 // ErrInvalidOffset means the specified was past the end of the attribute.
	ErrAuthorization     ATTError = 0x08 // ErrAuthorization means the attribute requires authorization before it can be read or written.
	ErrPrepQueueFull     ATTError = 0x09 // ErrPrepQueueFull means too many prepare writes have been queued.
	ErrAttrNotFound      ATTError = 0x0a // ErrAttrNotFound means no attribute found within the given attribute handle range.
	ErrAttrNotLong       ATTError = 0x0b // ErrAttrNotLong means the attribute cannot be read or written using the Read Blob Request.
	ErrInsuffEncrKeySize ATTError = 0x0c // ErrInsuffEncrKeySize means the Encryption Key Size used for encrypting this link is insufficient.
	ErrInvalAttrValueLen ATTError = 0x0d // ErrInvalAttrValueLen means the attribute value length is invalid for the operation.
	ErrUnlikely          ATTError = 0x0e // ErrUnlikely means the attribute request that was requested has encountered an error that was unlikely, and therefore could not be completed as requested.
	ErrInsuffEnc         ATTError = 0x0f // ErrInsuffEnc means the attribute requires encryption before it can be read or written.
	ErrUnsuppGrpType     ATTError = 0x10 // ErrUnsuppGrpType means the attribute type is not a supported grouping attribute as defined by a higher layer specification.
	ErrInsuffResources   ATTError = 0x11 // ErrInsuffResources means insufficient resources to complete the request.
)

ATTError is the error code of Attribute Protocol [Vol 3, Part F, 3.4.1.1].

func (ATTError) Error

func (e ATTError) Error() string

type Addr

type Addr interface {
	String() string
}

Addr represents a network end point address. It's MAC address on Linux or Device UUID on OS X.

func NewAddr

func NewAddr(s string) Addr

NewAddr creates an Addr from string

type AdvFilter

type AdvFilter func(a Advertisement) bool

AdvFilter returns true if the advertisement matches specified condition.

type AdvHandler

type AdvHandler func(a Advertisement)

AdvHandler handles advertisement.

type Advertisement interface {
	LocalName() string
	ManufacturerData() []byte
	ServiceData() []ServiceData
	Services() []UUID
	OverflowService() []UUID
	TxPowerLevel() int
	Connectable() bool
	SolicitedService() []UUID

	RSSI() int
	Addr() Addr
}

Advertisement ...

func Find

func Find(ctx context.Context, allowDup bool, f AdvFilter) ([]Advertisement, error)

Find ...

type Characteristic

type Characteristic struct {
	UUID        UUID
	Property    Property
	Secure      Property // FIXME
	Descriptors []*Descriptor
	CCCD        *Descriptor

	Value []byte

	ReadHandler     ReadHandler
	WriteHandler    WriteHandler
	NotifyHandler   NotifyHandler
	IndicateHandler NotifyHandler

	Handle      uint16
	ValueHandle uint16
	EndHandle   uint16
}

A Characteristic is a BLE characteristic.

func NewCharacteristic

func NewCharacteristic(u UUID) *Characteristic

NewCharacteristic creates and returns a Characteristic.

func (*Characteristic) AddDescriptor

func (c *Characteristic) AddDescriptor(d *Descriptor) *Descriptor

AddDescriptor adds a descriptor to a characteristic. AddDescriptor panics if the characteristic already contains another descriptor with the same UUID.

func (*Characteristic) HandleIndicate

func (c *Characteristic) HandleIndicate(h NotifyHandler)

HandleIndicate makes the characteristic support indicate requests, and routes notification requests to h. HandleIndicate must be called before the containing service is added to a server.

func (*Characteristic) HandleNotify

func (c *Characteristic) HandleNotify(h NotifyHandler)

HandleNotify makes the characteristic support notify requests, and routes notification requests to h. HandleNotify must be called before the containing service is added to a server.

func (*Characteristic) HandleRead

func (c *Characteristic) HandleRead(h ReadHandler)

HandleRead makes the characteristic support read requests, and routes read requests to h. HandleRead must be called before the containing service is added to a server. HandleRead panics if the characteristic has been configured with a static value.

func (*Characteristic) HandleWrite

func (c *Characteristic) HandleWrite(h WriteHandler)

HandleWrite makes the characteristic support write and write-no-response requests, and routes write requests to h. The WriteHandler does not differentiate between write and write-no-response requests; it is handled automatically. HandleWrite must be called before the containing service is added to a server.

func (*Characteristic) NewDescriptor

func (c *Characteristic) NewDescriptor(u UUID) *Descriptor

NewDescriptor adds a descriptor to a characteristic. NewDescriptor panics if the characteristic already contains another descriptor with the same UUID.

func (*Characteristic) SetValue

func (c *Characteristic) SetValue(b []byte)

SetValue makes the characteristic support read requests, and returns a static value. SetValue must be called before the containing service is added to a server. SetValue panics if the characteristic has been configured with a ReadHandler.

type Client

type Client interface {
	// Addr returns platform specific unique ID of the remote peripheral, e.g. MAC on Linux, Client UUID on OS X.
	Addr() Addr

	// Name returns the name of the remote peripheral.
	// This can be the advertised name, if exists, or the GAP device name, which takes priority.
	Name() string

	// Profile returns discovered profile.
	Profile() *Profile

	// DiscoverProfile discovers the whole hierarchy of a server.
	DiscoverProfile(ctx context.Context, force bool) (*Profile, error)

	// DiscoverServices finds all the primary services on a server. [Vol 3, Part G, 4.4.1]
	// If filter is specified, only filtered services are returned.
	DiscoverServices(ctx context.Context, filter []UUID) ([]*Service, error)

	// DiscoverIncludedServices finds the included services of a service. [Vol 3, Part G, 4.5.1]
	// If filter is specified, only filtered services are returned.
	DiscoverIncludedServices(ctx context.Context, filter []UUID, s *Service) ([]*Service, error)

	// DiscoverCharacteristics finds all the characteristics within a service. [Vol 3, Part G, 4.6.1]
	// If filter is specified, only filtered characteristics are returned.
	DiscoverCharacteristics(ctx context.Context, filter []UUID, s *Service) ([]*Characteristic, error)

	// DiscoverDescriptors finds all the descriptors within a characteristic. [Vol 3, Part G, 4.7.1]
	// If filter is specified, only filtered descriptors are returned.
	DiscoverDescriptors(ctx context.Context, filter []UUID, c *Characteristic) ([]*Descriptor, error)

	// ReadCharacteristic reads a characteristic value from a server. [Vol 3, Part G, 4.8.1]
	ReadCharacteristic(ctx context.Context, c *Characteristic) ([]byte, error)

	// ReadLongCharacteristic reads a characteristic value which is longer than the MTU. [Vol 3, Part G, 4.8.3]
	ReadLongCharacteristic(ctx context.Context, c *Characteristic) ([]byte, error)

	// WriteCharacteristic writes a characteristic value to a server. [Vol 3, Part G, 4.9.3]
	WriteCharacteristic(ctx context.Context, c *Characteristic, value []byte, noRsp bool) error

	// ReadDescriptor reads a characteristic descriptor from a server. [Vol 3, Part G, 4.12.1]
	ReadDescriptor(ctx context.Context, d *Descriptor) ([]byte, error)

	// WriteDescriptor writes a characteristic descriptor to a server. [Vol 3, Part G, 4.12.3]
	WriteDescriptor(ctx context.Context, d *Descriptor, v []byte) error

	// ReadRSSI retrieves the current RSSI value of the remote peripheral, in
	// dBm. [Vol 2, Part E, 7.5.4] The underlying command exchange is bounded
	// by the backend's own internal timeout and cannot be interrupted
	// mid-flight; per the contract above, ctx bounds only this caller's wait
	// — on expiry ctx.Err() is returned and the exchange's eventual result
	// is discarded.
	ReadRSSI(ctx context.Context) (int, error)

	// ExchangeMTU set the ATT_MTU to the maximum possible value that can be supported by both devices [Vol 3, Part G, 4.3.1]
	ExchangeMTU(ctx context.Context, rxMTU int) (txMTU int, err error)

	// Subscribe subscribes to indication (if ind is set true), or notification of a characteristic value. [Vol 3, Part G, 4.10 & 4.11]
	Subscribe(ctx context.Context, c *Characteristic, ind bool, h NotificationHandler) error

	// Unsubscribe unsubscribes to indication (if ind is set true), or notification of a specified characteristic value. [Vol 3, Part G, 4.10 & 4.11]
	Unsubscribe(ctx context.Context, c *Characteristic, ind bool) error

	// ClearSubscriptions clears all subscriptions to notifications and indications.
	ClearSubscriptions(ctx context.Context) error

	// CancelConnection disconnects the connection.
	CancelConnection() error

	// Disconnected returns a receiving channel, which is closed when the client disconnects.
	Disconnected() <-chan struct{}

	// Conn returns the client's current connection.
	Conn() Conn
}

A Client is a GATT client.

Methods that take a context.Context use it to bound their waits: the request is abandoned and ctx.Err() is returned (unwrapped or wrapped, so errors.Is(err, context.Canceled) / context.DeadlineExceeded hold) when the context is canceled or its deadline passes. Cancellation is best-effort at the transport layer: an in-flight ACL write cannot be interrupted mid-write; on the Linux stack it is independently bounded by hci.ACLWriteTimeout.

A request abandoned after it reached the wire still owns the ATT bearer (ATT is a sequential protocol). On the Linux stack the next request first resolves that transaction — waiting for its late response, or for its 30-second spec deadline and closing the bearer on expiry — so cancellation never causes a later request to consume a stale response.

Teardown paths (CancelConnection) deliberately take no context so that a client can always be torn down, even when no request context is available.

func Connect

func Connect(ctx context.Context, f AdvFilter) (Client, error)

Connect searches for and connects to a Peripheral which matches specified condition.

Whether a device was found is decided solely by draining the buffered found channel after Scan returns — never by which cancellation error Scan reported. The previous implementation cancelled the scan context from the advertisement handler and then treated Scan's context.Canceled as "match": when the parent ctx expired, its watcher could cancel the scan first, Scan returned context.Canceled with no match, and Connect blocked forever on an unbuffered channel nobody would ever send to. This library drives an unattended RV BLE gateway; that wedge was hit live.

func Dial

func Dial(ctx context.Context, a Addr) (Client, error)

Dial ...

type Conn

type Conn interface {
	io.ReadWriteCloser

	// Context returns the context that is used by this Conn.
	Context() context.Context

	// SetContext sets the context that is used by this Conn.
	SetContext(ctx context.Context)

	// LocalAddr returns local device's address.
	LocalAddr() Addr

	// RemoteAddr returns remote device's address.
	RemoteAddr() Addr

	// RxMTU returns the ATT_MTU which the local device is capable of accepting.
	RxMTU() int

	// SetRxMTU sets the ATT_MTU which the local device is capable of accepting.
	SetRxMTU(mtu int)

	// TxMTU returns the ATT_MTU which the remote device is capable of accepting.
	TxMTU() int

	// SetTxMTU sets the ATT_MTU which the remote device is capable of accepting.
	SetTxMTU(mtu int)

	// ReadRSSI retrieves the current RSSI value of the remote peripheral, in
	// dBm. [Vol 2, Part E, 7.5.4] Any transport or command failure is
	// reported as an error rather than a fabricated zero reading. The
	// exchange is bounded by the backend's own command timeout.
	ReadRSSI() (int, error)

	// UpdateParams issues an LE Connection Update on a live central link and
	// blocks until the controller reports it complete. p is validated and
	// converted with ConnParams.Encode; an out-of-range field returns
	// ErrInvalidConnParams (wrapped) without touching the controller. The
	// wait is bounded by ctx, by the backend's own update timeout, and by
	// connection teardown. Backends with no central-side update API (e.g.
	// CoreBluetooth, which manages parameters itself) return a wrapped
	// ErrNotImplemented.
	UpdateParams(ctx context.Context, p ConnParams) error

	// SetDataLength requests LE Data Length Extension on a live central link:
	// it asks the controller to use up to txOctets-octet link-layer payloads
	// (and txTime µs of air time) for this connection, cutting the packet
	// count of large GATT operations and thus radio airtime. txOctets and
	// txTime are validated by ValidateDataLength; an out-of-range value returns
	// ErrInvalidDataLength (wrapped) without touching the controller. Pass
	// DataLengthMaxTxOctets / DataLengthMaxTxTime (251 / 17040) for "the
	// controller's maximum".
	//
	// Unlike UpdateParams, this returns as soon as the controller accepts or
	// rejects the command (a Command Complete with a status): the actual
	// negotiated length arrives asynchronously — if at all — as an LE Data
	// Length Change event and may also be driven by the peer, so it is not
	// correlated 1:1 with this call. A non-zero command status is returned as
	// an error. Backends that manage data length themselves (e.g.
	// CoreBluetooth) return a wrapped ErrNotImplemented.
	SetDataLength(ctx context.Context, txOctets, txTime uint16) error

	// Disconnected returns a receiving channel, which is closed when the connection disconnects.
	Disconnected() <-chan struct{}
}

Conn implements a L2CAP connection.

type ConnParams

type ConnParams struct {
	IntervalMin time.Duration
	IntervalMax time.Duration
	Latency     int
	Timeout     time.Duration
}

ConnParams describes a requested LE connection-parameter update in human units. It is converted to the controller's integer units by Encode.

Valid ranges [Vol 6, Part B, 4.5]:

  • IntervalMin / IntervalMax: 7.5 ms to 4 s (encoded in 1.25 ms steps), with IntervalMin <= IntervalMax.
  • Latency: 0 to 499 connection events the peripheral may skip.
  • Timeout: 100 ms to 32 s (encoded in 10 ms steps), and large enough that Timeout > (1 + Latency) * IntervalMax * 2.

Durations are rounded to the nearest encoding step.

func (ConnParams) Encode

func (p ConnParams) Encode() (intervalMin, intervalMax, latency, timeout uint16, err error)

Encode validates p against the permitted ranges and converts it to the controller's integer units: connection intervals in 1.25 ms steps, timeout in 10 ms steps, latency as a raw connection-event count. Any violation returns ErrInvalidConnParams (wrapped, with detail) and zero values.

type ContextKey

type ContextKey string

ContextKey is a type used for keys of a context

type Descriptor

type Descriptor struct {
	UUID     UUID
	Property Property

	Handle uint16
	Value  []byte

	ReadHandler  ReadHandler
	WriteHandler WriteHandler
}

Descriptor is a BLE descriptor

func NewDescriptor

func NewDescriptor(u UUID) *Descriptor

NewDescriptor creates and returns a Descriptor.

func (*Descriptor) HandleRead

func (d *Descriptor) HandleRead(h ReadHandler)

HandleRead makes the descriptor support read requests, and routes read requests to h. HandleRead must be called before the containing service is added to a server. HandleRead panics if the descriptor has been configured with a static value.

func (*Descriptor) HandleWrite

func (d *Descriptor) HandleWrite(h WriteHandler)

HandleWrite makes the descriptor support write and write-no-response requests, and routes write requests to h. The WriteHandler does not differentiate between write and write-no-response requests; it is handled automatically. HandleWrite must be called before the containing service is added to a server.

func (*Descriptor) SetValue

func (d *Descriptor) SetValue(b []byte)

SetValue makes the descriptor support read requests, and returns a static value. SetValue must be called before the containing service is added to a server. SetValue panics if the descriptor has already configured with a ReadHandler.

type Device

type Device interface {
	// AddService adds a service to database.
	AddService(svc *Service) error

	// RemoveAllServices removes all services that are currently in the database.
	RemoveAllServices() error

	// SetServices set the specified service to the database.
	// It removes all currently added services, if any.
	SetServices(svcs []*Service) error

	// Stop detatch the GATT server from a peripheral device.
	Stop() error

	// Advertise advertises a given Advertisement
	Advertise(ctx context.Context, adv Advertisement) error

	// AdvertiseNameAndServices advertises device name, and specified service UUIDs.
	// It tres to fit the UUIDs in the advertising packet as much as possi
	// If name doesn't fit in the advertising packet, it will be put in scan response.
	AdvertiseNameAndServices(ctx context.Context, name string, uuids ...UUID) error

	// AdvertiseMfgData avertises the given manufacturer data.
	AdvertiseMfgData(ctx context.Context, id uint16, b []byte) error

	// AdvertiseServiceData16 advertises data associated with a 16bit service uuid
	AdvertiseServiceData16(ctx context.Context, id uint16, b []byte) error

	// AdvertiseIBeaconData advertise iBeacon with given manufacturer data.
	AdvertiseIBeaconData(ctx context.Context, b []byte) error

	// AdvertiseIBeacon advertises iBeacon with specified parameters.
	AdvertiseIBeacon(ctx context.Context, u UUID, major, minor uint16, pwr int8) error

	// Scan starts scanning. Duplicated advertisements will be filtered out if allowDup is set to false.
	Scan(ctx context.Context, allowDup bool, h AdvHandler) error

	// Dial ...
	Dial(ctx context.Context, a Addr) (Client, error)
}

Device ...

type DeviceOption

type DeviceOption interface {
	SetDeviceID(int) error
	SetDialerTimeout(time.Duration) error
	SetListenerTimeout(time.Duration) error
	SetConnParams(cmd.LECreateConnection) error
	SetScanParams(cmd.LESetScanParameters) error
	SetAdvParams(cmd.LESetAdvertisingParameters) error
	SetConnectedHandler(f func(evt.LEConnectionComplete)) error
	SetDisconnectedHandler(f func(evt.DisconnectionComplete)) error
	SetPeripheralRole() error
	SetCentralRole() error
}

DeviceOption is an interface which the device should implement to allow using configuration options

type NotificationHandler

type NotificationHandler func(req []byte)

A NotificationHandler handles notification or indication from a server.

The req slice is valid only for the duration of the call: on the linux stack it is backed by a pooled buffer that is reused for later notifications as soon as the handler returns. A handler that retains the data past its return must copy it.

type Notifier

type Notifier interface {
	// Context sends data to the central.
	Context() context.Context

	// Write sends data to the central.
	Write(b []byte) (int, error)

	// Close ...
	Close() error

	// Cap returns the maximum number of bytes that may be sent in a single notification.
	Cap() int
}

Notifier ...

func NewNotifier

func NewNotifier(send func([]byte) (int, error)) Notifier

NewNotifier ...

type NotifyHandler

type NotifyHandler interface {
	ServeNotify(req Request, n Notifier)
}

A NotifyHandler handles GATT requests.

type NotifyHandlerFunc

type NotifyHandlerFunc func(req Request, n Notifier)

NotifyHandlerFunc is an adapter to allow the use of ordinary functions as Handlers.

func (NotifyHandlerFunc) ServeNotify

func (f NotifyHandlerFunc) ServeNotify(req Request, n Notifier)

ServeNotify returns f(r, maxlen, offset).

type Option

type Option func(DeviceOption) error

An Option is a configuration function, which configures the device.

func OptAdvParams

func OptAdvParams(param cmd.LESetAdvertisingParameters) Option

OptAdvParams overrides default advertising parameters.

func OptCentralRole

func OptCentralRole() Option

OptCentralRole configures the device to perform Central tasks.

func OptConnParams

func OptConnParams(param cmd.LECreateConnection) Option

OptConnParams overrides default connection parameters.

func OptConnectHandler

func OptConnectHandler(f func(evt.LEConnectionComplete)) Option

func OptDeviceID

func OptDeviceID(id int) Option

OptDeviceID sets HCI device ID.

func OptDialerTimeout

func OptDialerTimeout(d time.Duration) Option

OptDialerTimeout sets dialing timeout for Dialer.

func OptDisconnectHandler

func OptDisconnectHandler(f func(evt.DisconnectionComplete)) Option

func OptListenerTimeout

func OptListenerTimeout(d time.Duration) Option

OptListenerTimeout sets dialing timeout for Listener.

func OptPeripheralRole

func OptPeripheralRole() Option

OptPeripheralRole configures the device to perform Peripheral tasks.

func OptScanParams

func OptScanParams(param cmd.LESetScanParameters) Option

OptScanParams overrides default scanning parameters.

type Profile

type Profile struct {
	Services []*Service
}

A Profile is composed of one or more services necessary to fulfill a use case.

func (*Profile) Find

func (p *Profile) Find(target any) any

Find searches the discovered profile for a match to target by type and UUID. target must be a *Service, *Characteristic, or *Descriptor; the result is the matching one, or an untyped nil when nothing matches or target is some other type.

The no-match case returns a genuine nil interface, not a typed nil: the concrete FindX helpers return a nil *Service/*Characteristic/*Descriptor, and returning that directly would box it into a non-nil any, so `p.Find(x) == nil` never fired. Callers must still type-assert a non-nil result to the concrete type.

func (*Profile) FindCharacteristic

func (p *Profile) FindCharacteristic(char *Characteristic) *Characteristic

FindCharacteristic searches discoverd profile for the specified characteristic and UUID

func (*Profile) FindDescriptor

func (p *Profile) FindDescriptor(desc *Descriptor) *Descriptor

FindDescriptor searches discoverd profile for the specified descriptor and UUID

func (*Profile) FindService

func (p *Profile) FindService(service *Service) *Service

FindService searches discoverd profile for the specified service and UUID

type Property

type Property int

Property ...

const (
	CharBroadcast   Property = 0x01 // may be brocasted
	CharRead        Property = 0x02 // may be read
	CharWriteNR     Property = 0x04 // may be written to, with no reply
	CharWrite       Property = 0x08 // may be written to, with a reply
	CharNotify      Property = 0x10 // supports notifications
	CharIndicate    Property = 0x20 // supports Indications
	CharSignedWrite Property = 0x40 // supports signed write
	CharExtended    Property = 0x80 // supports extended properties
)

Characteristic property flags (spec 3.3.3.1)

type ReadHandler

type ReadHandler interface {
	ServeRead(req Request, rsp ResponseWriter)
}

A ReadHandler handles GATT requests.

type ReadHandlerFunc

type ReadHandlerFunc func(req Request, rsp ResponseWriter)

ReadHandlerFunc is an adapter to allow the use of ordinary functions as Handlers.

func (ReadHandlerFunc) ServeRead

func (f ReadHandlerFunc) ServeRead(req Request, rsp ResponseWriter)

ServeRead returns f(r, maxlen, offset).

type Request

type Request interface {
	Conn() Conn
	Data() []byte
	Offset() int
}

Request ...

func NewRequest

func NewRequest(conn Conn, data []byte, offset int) Request

NewRequest returns a default implementation of Request.

type ResponseWriter

type ResponseWriter interface {
	// Write writes data to return as the characteristic value.
	Write(b []byte) (int, error)

	// Status reports the result of the request.
	Status() ATTError

	// SetStatus reports the result of the request.
	SetStatus(status ATTError)

	// Len ...
	Len() int

	// Cap ...
	Cap() int
}

ResponseWriter ...

func NewResponseWriter

func NewResponseWriter(buf *bytes.Buffer) ResponseWriter

NewResponseWriter ...

type Service

type Service struct {
	UUID            UUID
	Characteristics []*Characteristic

	// Includes holds the services discovered by
	// Client.DiscoverIncludedServices for this service.
	Includes []*Service

	Handle    uint16
	EndHandle uint16
}

A Service is a BLE service.

func NewService

func NewService(u UUID) *Service

NewService creates and initialize a new Service using u as it's UUID.

func (*Service) AddCharacteristic

func (s *Service) AddCharacteristic(c *Characteristic) *Characteristic

AddCharacteristic adds a characteristic to a service. AddCharacteristic panics if the service already contains another characteristic with the same UUID.

func (*Service) NewCharacteristic

func (s *Service) NewCharacteristic(u UUID) *Characteristic

NewCharacteristic adds a characteristic to a service. NewCharacteristic panics if the service already contains another characteristic with the same UUID.

type ServiceData

type ServiceData struct {
	UUID UUID
	Data []byte
}

ServiceData ...

type UUID

type UUID []byte

A UUID is a BLE UUID.

func MustParse

func MustParse(s string) UUID

MustParse parses a standard-format UUID string, like Parse, but panics in case of error.

func Parse

func Parse(s string) (UUID, error)

Parse parses a standard-format UUID string, such as "1800" or "34DA3AD1-7110-41A1-B1EF-4430F509CDE7".

func UUID16

func UUID16(i uint16) UUID

UUID16 converts a uint16 (such as 0x1800) to a UUID.

func (UUID) Equal

func (u UUID) Equal(v UUID) bool

Equal returns a boolean reporting whether v represent the same UUID as u.

func (UUID) Len

func (u UUID) Len() int

Len returns the length of the UUID, in bytes. BLE UUIDs are either 2 or 16 bytes.

func (UUID) String

func (u UUID) String() string

String hex-encodes a UUID.

type WriteHandler

type WriteHandler interface {
	ServeWrite(req Request, rsp ResponseWriter)
}

A WriteHandler handles GATT requests.

type WriteHandlerFunc

type WriteHandlerFunc func(req Request, rsp ResponseWriter)

WriteHandlerFunc is an adapter to allow the use of ordinary functions as Handlers.

func (WriteHandlerFunc) ServeWrite

func (f WriteHandlerFunc) ServeWrite(req Request, rsp ResponseWriter)

ServeWrite returns f(r, maxlen, offset).

Directories

Path Synopsis
adv
att
hci
tools/codegen command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL