From c5ddcf8c05bf61624e21da5807b8d40e5f4ff48e Mon Sep 17 00:00:00 2001 From: Thomas Kohler Date: Fri, 2 Jan 2026 16:16:59 +0100 Subject: [PATCH 01/10] sd: fix error in sd_ble_gap_scan_start after stop scan with nrf528xx --- gap_nrf528xx-central.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/gap_nrf528xx-central.go b/gap_nrf528xx-central.go index 2ae2c81..a3ac6bf 100644 --- a/gap_nrf528xx-central.go +++ b/gap_nrf528xx-central.go @@ -55,7 +55,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { } // Wait for received scan reports. - for a.scanning { + for { // Wait for the next advertisement packet to arrive. // TODO: use some sort of condition variable once the scheduler supports // them. @@ -69,6 +69,11 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { // Call the callback with the scan result. callback(a, globalScanResult) + if !a.scanning { + // the callback has stopped the scanning, calling "sd_ble_gap_scan_start" would be for nothing + break + } + // Restart the advertisement. This is needed, because advertisements are // automatically stopped when the first packet arrives. errCode := C.sd_ble_gap_scan_start(nil, &scanReportBufferInfo) @@ -76,7 +81,11 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { return Error(errCode) } } - return nil + + // calling "C.sd_ble_gap_scan_stop()" in "StopScan" would lead to an error in "C.sd_ble_gap_scan_start()", because + // "StopScan" is usually called in the "callback()" just before + errCode = C.sd_ble_gap_scan_stop() + return makeError(errCode) } // StopScan stops any in-progress scan. It can be called from within a Scan @@ -87,8 +96,8 @@ func (a *Adapter) StopScan() error { return errNotScanning } a.scanning = false - errCode := C.sd_ble_gap_scan_stop() - return makeError(errCode) + + return nil } // In-progress connection attempt. From cd4f1c2bd2dace5788821c8eebebdf44b8001d12 Mon Sep 17 00:00:00 2001 From: Soraya Ormazabal Date: Mon, 19 Jan 2026 09:08:59 +0100 Subject: [PATCH 02/10] Add WinRT handler for Bluetooth LE connection status changes This PR registers a `ConnectionStatusChanged` WinRT event handler on `BluetoothLEDevice` to detect abrupt disconnections. The handler resolves the current connection state and forwards it to `a.connectHandler`. The event token and handler are stored to allow proper unregistration during device teardown. --- gap_windows.go | 48 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/gap_windows.go b/gap_windows.go index 54c2676..171f96b 100644 --- a/gap_windows.go +++ b/gap_windows.go @@ -289,8 +289,10 @@ type Device struct { Address Address // the MAC address of the device - device *bluetooth.BluetoothLEDevice - session *genericattributeprofile.GattSession + device *bluetooth.BluetoothLEDevice + session *genericattributeprofile.GattSession + connectionStatusListenerToken foundation.EventRegistrationToken + connectionStatusListener *foundation.TypedEventHandler } // Connect starts a connection attempt to the given peripheral device address. @@ -367,8 +369,36 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err session: newSession, } - if a.connectHandler != nil { - a.connectHandler(device, true) + // https://learn.microsoft.com/es-es/uwp/api/windows.devices.bluetooth.bluetoothledevice.connectionstatuschanged?view=winrt-26100 + // TypedEventHandler + connectionStatusChangedGUID := winrt.ParameterizedInstanceGUID( + foundation.GUIDTypedEventHandler, + bluetooth.SignatureBluetoothLEDevice, + "cinterface(IInspectable)", // object + ) + + handler := foundation.NewTypedEventHandler(ole.NewGUID(connectionStatusChangedGUID), func(instance *foundation.TypedEventHandler, sender, arg unsafe.Pointer) { + status, err := bleDevice.GetConnectionStatus() + if err != nil { + return + } + if status == bluetooth.BluetoothConnectionStatusDisconnected { + device.Disconnect() + } + + if a.connectHandler != nil { + a.connectHandler(device, status == bluetooth.BluetoothConnectionStatusConnected) + } + }) + + token, err := device.device.AddConnectionStatusChanged(handler) + + device.connectionStatusListenerToken = token + device.connectionStatusListener = handler + + if err != nil { + _ = handler.Release() + return device, err } return device, nil @@ -379,20 +409,22 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err func (d Device) Disconnect() error { defer d.device.Release() defer d.session.Release() + if d.connectionStatusListener != nil { + defer d.connectionStatusListener.Release() + } d.cancel() if err := d.session.Close(); err != nil { return err } + + _ = d.device.RemoveConnectionStatusChanged(d.connectionStatusListenerToken) + if err := d.device.Close(); err != nil { return err } - if DefaultAdapter.connectHandler != nil { - DefaultAdapter.connectHandler(d, false) - } - return nil } From 04fbc0e7935db0147e1f49c285b3dbd87155b476 Mon Sep 17 00:00:00 2001 From: kevin fry Date: Thu, 22 Jan 2026 17:31:07 -0800 Subject: [PATCH 03/10] Fix for darwin enable-notifications API to allow disabling of notifications by passing in nil as the callback argument --- gattc_darwin.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/gattc_darwin.go b/gattc_darwin.go index 2d737da..1c72a97 100644 --- a/gattc_darwin.go +++ b/gattc_darwin.go @@ -211,14 +211,17 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) // Configuration Descriptor (CCCD). This means that most peripherals will send a // notification with a new value every time the value of the characteristic // changes. +// Users may call EnableNotifications with a nil callback to disable notifications. func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error { + // If callback is nil, disable notifications if callback == nil { - return errors.New("must provide a callback for EnableNotifications") + c.service.device.prph.SetNotify(false, c.characteristic) + c.callback = nil // Clear notification callback + } else { + // Enable notifications and set notification callback + c.callback = callback + c.service.device.prph.SetNotify(true, c.characteristic) } - - c.callback = callback - c.service.device.prph.SetNotify(true, c.characteristic) - return nil } From a48868abaf0dec102d9080beac42e0351afde269 Mon Sep 17 00:00:00 2001 From: timklge Date: Fri, 23 Jan 2026 20:40:54 +0100 Subject: [PATCH 04/10] linux: Add Write method to write characteristic value with response (#389) * linux: Add Write method to write characteristic value with response * Use short hand error check * Remove named returns --- gattc_linux.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/gattc_linux.go b/gattc_linux.go index bbf96ac..087ece2 100644 --- a/gattc_linux.go +++ b/gattc_linux.go @@ -221,9 +221,19 @@ func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteri // call will return before all data has been written. A limited number of such // writes can be in flight at any given time. This call is also known as a // "write command" (as opposed to a write request). -func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) { - err = c.characteristic.Call("org.bluez.GattCharacteristic1.WriteValue", 0, p, map[string]dbus.Variant(nil)).Err - if err != nil { +func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (int, error) { + args := map[string]any{"type": "command"} + if err := c.characteristic.Call("org.bluez.GattCharacteristic1.WriteValue", 0, p, args).Err; err != nil { + return 0, err + } + return len(p), nil +} + +// Write replaces the characteristic value with a new value. The +// call will return after all data has been written. +func (c DeviceCharacteristic) Write(p []byte) (int, error) { + args := map[string]any{"type": "request"} + if err := c.characteristic.Call("org.bluez.GattCharacteristic1.WriteValue", 0, p, args).Err; err != nil { return 0, err } return len(p), nil From 1012915659953dcefa789448fe2652926461d94e Mon Sep 17 00:00:00 2001 From: devgianlu Date: Thu, 5 Feb 2026 19:26:07 +0100 Subject: [PATCH 05/10] Remove service / stop advertising support (#407) * linux: support removing service * windows: support removing service * sd+hci: stub removing service method --- gatts.go | 1 + gatts_hci.go | 7 +++++++ gatts_linux.go | 8 ++++++++ gatts_sd.go | 10 +++++++++- gatts_windows.go | 26 ++++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/gatts.go b/gatts.go index 70e5518..e4bb6ec 100644 --- a/gatts.go +++ b/gatts.go @@ -2,6 +2,7 @@ package bluetooth // Service is a GATT service to be used in AddService. type Service struct { + id uint64 handle uint16 UUID Characteristics []CharacteristicConfig diff --git a/gatts_hci.go b/gatts_hci.go index f901990..4a4870d 100644 --- a/gatts_hci.go +++ b/gatts_hci.go @@ -2,6 +2,8 @@ package bluetooth +import "errors" + type Characteristic struct { adapter *Adapter handle uint16 @@ -79,6 +81,11 @@ func (a *Adapter) AddService(service *Service) error { return nil } +// RemoveService removes a previously added service. +func (a *Adapter) RemoveService(s *Service) error { + return errors.ErrUnsupported +} + // Write replaces the characteristic value with a new value. func (c *Characteristic) Write(p []byte) (n int, err error) { if !(c.permissions.Write() || c.permissions.WriteWithoutResponse() || diff --git a/gatts_linux.go b/gatts_linux.go index dad88b1..792ea43 100644 --- a/gatts_linux.go +++ b/gatts_linux.go @@ -76,6 +76,8 @@ func (c *bluezChar) WriteValue(value []byte, options map[string]dbus.Variant) *d func (a *Adapter) AddService(s *Service) error { // Create a unique DBus path for this service. id := atomic.AddUint64(&serviceID, 1) + s.id = id + path := dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/service%d", id)) // All objects that will be part of the ObjectManager. @@ -153,6 +155,12 @@ func (a *Adapter) AddService(s *Service) error { return a.adapter.Call("org.bluez.GattManager1.RegisterApplication", 0, path, map[string]dbus.Variant(nil)).Err } +// RemoveService removes a previously added service. +func (a *Adapter) RemoveService(s *Service) error { + path := dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/service%d", s.id)) + return a.adapter.Call("org.bluez.GattManager1.UnregisterApplication", 0, path).Err +} + // Write replaces the characteristic value with a new value. func (c *Characteristic) Write(p []byte) (n int, err error) { if len(p) == 0 { diff --git a/gatts_sd.go b/gatts_sd.go index 602aa24..ef2d259 100644 --- a/gatts_sd.go +++ b/gatts_sd.go @@ -22,7 +22,10 @@ static inline uint32_t sd_ble_gatts_value_set_noescape(uint16_t conn_handle, uin } */ import "C" -import "unsafe" +import ( + "errors" + "unsafe" +) // Characteristic is a single characteristic in a service. It has an UUID and a // value. @@ -91,6 +94,11 @@ func (a *Adapter) AddService(service *Service) error { return makeError(errCode) } +// RemoveService removes a service previously added with AddService. +func (a *Adapter) RemoveService(s *Service) error { + return errors.ErrUnsupported +} + // charWriteHandler contains a handler->callback mapping for characteristic // writes. type charWriteHandler struct { diff --git a/gatts_windows.go b/gatts_windows.go index 464926f..598bb13 100644 --- a/gatts_windows.go +++ b/gatts_windows.go @@ -240,6 +240,32 @@ func (a *Adapter) AddService(s *Service) error { return serviceProvider.StartAdvertisingWithParameters(params) } +// RemoveService stops advertising the service and removes it. +func (a *Adapter) RemoveService(s *Service) error { + gattServiceOp, err := genericattributeprofile.GattServiceProviderCreateAsync(syscallUUIDFromUUID(s.UUID)) + + if err != nil { + return err + } + + if err = awaitAsyncOperation(gattServiceOp, genericattributeprofile.SignatureGattServiceProviderResult); err != nil { + return err + } + + res, err := gattServiceOp.GetResults() + if err != nil { + return err + } + + serviceProviderResult := (*genericattributeprofile.GattServiceProviderResult)(res) + serviceProvider, err := serviceProviderResult.GetServiceProvider() + if err != nil { + return err + } + + return serviceProvider.StopAdvertising() +} + // Write replaces the characteristic value with a new value. func (c *Characteristic) Write(p []byte) (n int, err error) { length := len(p) From 53071ed6814190f779ce03fac410ef3842b17954 Mon Sep 17 00:00:00 2001 From: Dan Kortschak Date: Fri, 3 Oct 2025 06:13:42 +0930 Subject: [PATCH 06/10] make uuid api safer Unexpose the representation of the UUID data and unexport the Bytes method. Also document the byte ordering of binary marshaling and provide an exported binary round-tripper for UUID data. --- att_hci.go | 6 +- gap.go | 6 +- gap_hci.go | 2 +- gap_test.go | 2 +- gatts_hci.go | 4 +- tools/gen-characteristic-uuids/main.go | 2 +- tools/gen-service-uuids/main.go | 2 +- uuid.go | 223 +++++++++++++------------ uuid_sd.go | 6 +- uuid_test.go | 8 + 10 files changed, 141 insertions(+), 120 deletions(-) diff --git a/att_hci.go b/att_hci.go index aeaa9e4..93b86d7 100644 --- a/att_hci.go +++ b/att_hci.go @@ -118,7 +118,7 @@ func (s *rawService) Read(p []byte) (int, error) { binary.LittleEndian.PutUint16(p[4:], s.uuid.Get16Bit()) sz += 2 default: - uuid := s.uuid.Bytes() + uuid := s.uuid.bytes() copy(p[4:], uuid[:]) sz += 16 } @@ -166,7 +166,7 @@ func (c *rawCharacteristic) Read(p []byte) (int, error) { binary.LittleEndian.PutUint16(p[5:], c.uuid.Get16Bit()) sz += 2 default: - uuid := c.uuid.Bytes() + uuid := c.uuid.bytes() copy(p[5:], uuid[:]) sz += 16 } @@ -232,7 +232,7 @@ func (a *rawAttribute) Read(p []byte) (int, error) { binary.LittleEndian.PutUint16(p[sz:], a.uuid.Get16Bit()) sz += 2 default: - uuid := a.uuid.Bytes() + uuid := a.uuid.bytes() copy(p[sz:], uuid[:]) sz += 16 } diff --git a/gap.go b/gap.go index 9191f0e..e0c346a 100644 --- a/gap.go +++ b/gap.go @@ -316,7 +316,7 @@ func (buf *rawAdvertisementPayload) HasServiceUUID(uuid UUID) bool { if len(b) == 0 { b = buf.findField(0x06) // Incomplete List of 128-bit Service Class UUIDs } - uuidBuf1 := uuid.Bytes() + uuidBuf1 := uuid.bytes() for i := 0; i < len(b)/16; i++ { uuidBuf2 := b[i*16 : i*16+16] match := true @@ -519,7 +519,7 @@ func (buf *rawAdvertisementPayload) addServiceData(uuid UUID, data []byte) (ok b // Add the data. buf.data[buf.len+0] = byte(fieldLength - 1) buf.data[buf.len+1] = 0x21 - uuid_bytes := uuid.Bytes() + uuid_bytes := uuid.bytes() copy(buf.data[buf.len+2:], uuid_bytes[:]) copy(buf.data[buf.len+2+16:], data) buf.len += uint8(fieldLength) @@ -579,7 +579,7 @@ func (buf *rawAdvertisementPayload) addServiceUUID(uuid UUID) (ok bool) { } buf.data[buf.len+0] = 17 // length of field, including type buf.data[buf.len+1] = 0x07 // type, 0x07 means "Complete List of 128-bit Service Class UUIDs" - rawUUID := uuid.Bytes() + rawUUID := uuid.bytes() copy(buf.data[buf.len+2:], rawUUID[:]) buf.len += 18 return true diff --git a/gap_hci.go b/gap_hci.go index 7110dc4..59463d9 100644 --- a/gap_hci.go +++ b/gap_hci.go @@ -420,7 +420,7 @@ func (a *Advertisement) Start() error { binary.LittleEndian.PutUint16(advertisingData[5:], uuid.Get16Bit()) case uuid.Is32Bit(): sz = 6 - data := uuid.Bytes() + data := uuid.bytes() slices.Reverse(data[:]) copy(advertisingData[5:], data[:]) } diff --git a/gap_test.go b/gap_test.go index 5f77aeb..a31cadc 100644 --- a/gap_test.go +++ b/gap_test.go @@ -137,7 +137,7 @@ func TestServiceUUIDs(t *testing.T) { raw string expected []UUID } - uuidBytes := ServiceUUIDAdafruitSound.Bytes() + uuidBytes := ServiceUUIDAdafruitSound.bytes() tests := []testCase{ {}, { diff --git a/gatts_hci.go b/gatts_hci.go index 4a4870d..9f9dbb3 100644 --- a/gatts_hci.go +++ b/gatts_hci.go @@ -15,13 +15,13 @@ type Characteristic struct { // AddService creates a new service with the characteristics listed in the // Service struct. func (a *Adapter) AddService(service *Service) error { - uuid := service.UUID.Bytes() + uuid := service.UUID.bytes() serviceHandle := a.att.addLocalAttribute(attributeTypeService, 0, shortUUID(gattServiceUUID).UUID(), 0, uuid[:]) valueHandle := serviceHandle endHandle := serviceHandle for i := range service.Characteristics { - data := service.Characteristics[i].UUID.Bytes() + data := service.Characteristics[i].UUID.bytes() cuuid := append([]byte{}, data[:]...) // add characteristic declaration diff --git a/tools/gen-characteristic-uuids/main.go b/tools/gen-characteristic-uuids/main.go index 794e1fd..78f2d09 100644 --- a/tools/gen-characteristic-uuids/main.go +++ b/tools/gen-characteristic-uuids/main.go @@ -45,7 +45,7 @@ func (c Characteristic) UUIDFunc() string { if err != nil { panic(err) } - b := uuid.Bytes() + b := uuid.bytes() bs := hex.EncodeToString(b[:]) bss := "" for i := 0; i < len(bs); i += 2 { diff --git a/tools/gen-service-uuids/main.go b/tools/gen-service-uuids/main.go index cad8dcc..96b840f 100644 --- a/tools/gen-service-uuids/main.go +++ b/tools/gen-service-uuids/main.go @@ -45,7 +45,7 @@ func (s Service) UUIDFunc() string { if err != nil { panic(err) } - b := uuid.Bytes() + b := uuid.bytes() bs := hex.EncodeToString(b[:]) bss := "" for i := 0; i < len(bs); i += 2 { diff --git a/uuid.go b/uuid.go index c0ad7a1..931f1b7 100644 --- a/uuid.go +++ b/uuid.go @@ -10,18 +10,20 @@ import ( // UUID is a single UUID as used in the Bluetooth stack. It is represented as a // [4]uint32 instead of a [16]byte for efficiency. -type UUID [4]uint32 +type UUID struct { + id [4]uint32 +} var errInvalidUUID = errors.New("bluetooth: failed to parse UUID") // NewUUID returns a new UUID based on the 128-bit (or 16-byte) input. func NewUUID(uuid [16]byte) UUID { - u := UUID{} - u[0] = uint32(uuid[15]) | uint32(uuid[14])<<8 | uint32(uuid[13])<<16 | uint32(uuid[12])<<24 - u[1] = uint32(uuid[11]) | uint32(uuid[10])<<8 | uint32(uuid[9])<<16 | uint32(uuid[8])<<24 - u[2] = uint32(uuid[7]) | uint32(uuid[6])<<8 | uint32(uuid[5])<<16 | uint32(uuid[4])<<24 - u[3] = uint32(uuid[3]) | uint32(uuid[2])<<8 | uint32(uuid[1])<<16 | uint32(uuid[0])<<24 - return u + uu := UUID{} + uu.id[0] = uint32(uuid[15]) | uint32(uuid[14])<<8 | uint32(uuid[13])<<16 | uint32(uuid[12])<<24 + uu.id[1] = uint32(uuid[11]) | uint32(uuid[10])<<8 | uint32(uuid[9])<<16 | uint32(uuid[8])<<24 + uu.id[2] = uint32(uuid[7]) | uint32(uuid[6])<<8 | uint32(uuid[5])<<16 | uint32(uuid[4])<<24 + uu.id[3] = uint32(uuid[3]) | uint32(uuid[2])<<8 | uint32(uuid[1])<<16 | uint32(uuid[0])<<24 + return uu } // New16BitUUID returns a new 128-bit UUID based on a 16-bit UUID. @@ -29,13 +31,13 @@ func NewUUID(uuid [16]byte) UUID { // Note: only use registered UUIDs. See // https://www.bluetooth.com/specifications/gatt/services/ for a list. func New16BitUUID(shortUUID uint16) UUID { - // https://stackoverflow.com/questions/36212020/how-can-i-convert-a-bluetooth-16-bit-service-uuid-into-a-128-bit-uuid - var uuid UUID - uuid[0] = 0x5F9B34FB - uuid[1] = 0x80000080 - uuid[2] = 0x00001000 - uuid[3] = uint32(shortUUID) - return uuid + // https://stackoverflow.com/questions/36212020/how-can-i-convert-a-bluetooth-16-bit-service-uu-into-a-128-bit-uu + var uu UUID + uu.id[0] = 0x5F9B34FB + uu.id[1] = 0x80000080 + uu.id[2] = 0x00001000 + uu.id[3] = uint32(shortUUID) + return uu } // New32BitUUID returns a new 128-bit UUID based on a 32-bit UUID. @@ -44,12 +46,12 @@ func New16BitUUID(shortUUID uint16) UUID { // https://www.bluetooth.com/specifications/gatt/services/ for a list. func New32BitUUID(shortUUID uint32) UUID { // https://stackoverflow.com/questions/36212020/how-can-i-convert-a-bluetooth-16-bit-service-uuid-into-a-128-bit-uuid - var uuid UUID - uuid[0] = 0x5F9B34FB - uuid[1] = 0x80000080 - uuid[2] = 0x00001000 - uuid[3] = shortUUID - return uuid + var uu UUID + uu.id[0] = 0x5F9B34FB + uu.id[1] = 0x80000080 + uu.id[2] = 0x00001000 + uu.id[3] = shortUUID + return uu } // Replace16BitComponent returns a new UUID where bits 16..32 have been replaced @@ -59,19 +61,19 @@ func New32BitUUID(shortUUID uint32) UUID { // This is especially useful for the Nordic SoftDevice, because it is able to // store custom UUIDs more efficiently when only these bits vary between them. func (uuid UUID) Replace16BitComponent(component uint16) UUID { - uuid[3] &^= 0x0000ffff // clear the new component bits - uuid[3] |= uint32(component) // set the component bits + uuid.id[3] &^= 0x0000ffff // clear the new component bits + uuid.id[3] |= uint32(component) // set the component bits return uuid } // Is16Bit returns whether this UUID is a 16-bit BLE UUID. func (uuid UUID) Is16Bit() bool { - return uuid.Is32Bit() && uuid[3] == uint32(uint16(uuid[3])) + return uuid.Is32Bit() && uuid.id[3] == uint32(uint16(uuid.id[3])) } // Is32Bit returns whether this UUID is a 32-bit or 16-bit BLE UUID. func (uuid UUID) Is32Bit() bool { - return uuid[0] == 0x5F9B34FB && uuid[1] == 0x80000080 && uuid[2] == 0x00001000 + return uuid.id[0] == 0x5F9B34FB && uuid.id[1] == 0x80000080 && uuid.id[2] == 0x00001000 } // Get16Bit returns the 16-bit version of this UUID. This is only valid if it @@ -79,7 +81,7 @@ func (uuid UUID) Is32Bit() bool { func (uuid UUID) Get16Bit() uint16 { // Note: using a Get* function as a getter because method names can't start // with a number. - return uint16(uuid[3]) + return uint16(uuid.id[3]) } // Get32Bit returns the 32-bit version of this UUID. This is only valid if it @@ -87,54 +89,65 @@ func (uuid UUID) Get16Bit() uint16 { func (uuid UUID) Get32Bit() uint32 { // Note: using a Get* function as a getter because method names can't start // with a number. - return uuid[3] + return uuid.id[3] } -// Bytes returns a 16-byte array containing the raw UUID. -func (uuid UUID) Bytes() [16]byte { - buf := [16]byte{} - buf[0] = byte(uuid[0]) - buf[1] = byte(uuid[0] >> 8) - buf[2] = byte(uuid[0] >> 16) - buf[3] = byte(uuid[0] >> 24) - buf[4] = byte(uuid[1]) - buf[5] = byte(uuid[1] >> 8) - buf[6] = byte(uuid[1] >> 16) - buf[7] = byte(uuid[1] >> 24) - buf[8] = byte(uuid[2]) - buf[9] = byte(uuid[2] >> 8) - buf[10] = byte(uuid[2] >> 16) - buf[11] = byte(uuid[2] >> 24) - buf[12] = byte(uuid[3]) - buf[13] = byte(uuid[3] >> 8) - buf[14] = byte(uuid[3] >> 16) - buf[15] = byte(uuid[3] >> 24) - return buf +// BytesBytesBigEndian returns a 16-byte array containing the raw UUID in +// network order. The result from BytesBigEndian may be used as the input for +// NewUUID. +func (uuid UUID) BytesBigEndian() [16]byte { + return [16]byte{ + 0: byte(uuid.id[3] >> 24), + 1: byte(uuid.id[3] >> 16), + 2: byte(uuid.id[3] >> 8), + 3: byte(uuid.id[3]), + 4: byte(uuid.id[2] >> 24), + 5: byte(uuid.id[2] >> 16), + 6: byte(uuid.id[2] >> 8), + 7: byte(uuid.id[2]), + 8: byte(uuid.id[1] >> 24), + 9: byte(uuid.id[1] >> 16), + 10: byte(uuid.id[1] >> 8), + 11: byte(uuid.id[1]), + 12: byte(uuid.id[0] >> 24), + 13: byte(uuid.id[0] >> 16), + 14: byte(uuid.id[0] >> 8), + 15: byte(uuid.id[0]), + } +} + +// bytes returns a 16-byte array containing the raw UUID in little-endian +// order. +func (uuid UUID) bytes() [16]byte { + return [16]byte{ + 0: byte(uuid.id[0]), + 1: byte(uuid.id[0] >> 8), + 2: byte(uuid.id[0] >> 16), + 3: byte(uuid.id[0] >> 24), + 4: byte(uuid.id[1]), + 5: byte(uuid.id[1] >> 8), + 6: byte(uuid.id[1] >> 16), + 7: byte(uuid.id[1] >> 24), + 8: byte(uuid.id[2]), + 9: byte(uuid.id[2] >> 8), + 10: byte(uuid.id[2] >> 16), + 11: byte(uuid.id[2] >> 24), + 12: byte(uuid.id[3]), + 13: byte(uuid.id[3] >> 8), + 14: byte(uuid.id[3] >> 16), + 15: byte(uuid.id[3] >> 24), + } } -// AppendBinary appends the bytes of the uuid to the given byte slice b. +// AppendBinary appends the bytes of the uuid in little-endian order to the +// given byte slice b. func (uuid UUID) AppendBinary(b []byte) ([]byte, error) { - return append(b, - byte(uuid[0]), - byte(uuid[0]>>8), - byte(uuid[0]>>16), - byte(uuid[0]>>24), - byte(uuid[1]), - byte(uuid[1]>>8), - byte(uuid[1]>>16), - byte(uuid[1]>>24), - byte(uuid[2]), - byte(uuid[2]>>8), - byte(uuid[2]>>16), - byte(uuid[2]>>24), - byte(uuid[3]), - byte(uuid[3]>>8), - byte(uuid[3]>>16), - byte(uuid[3]>>24), - ), nil + id := uuid.bytes() + return append(b, id[:]...), nil } -// MarshalBinary marshals the uuid into and byte slice and returns the slice. It will not return an error +// MarshalBinary marshals the uuid into and byte slice in little-endian order +// and returns the slice. It will not return an error. func (uuid UUID) MarshalBinary() (data []byte, err error) { return uuid.AppendBinary(make([]byte, 0, 16)) } @@ -191,25 +204,25 @@ func (u *UUID) unmarshalText128(s []byte) error { if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[i] |= uint32(reverseHexTable[s[j]]) << 28 + u.id[i] |= uint32(reverseHexTable[s[j]]) << 28 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[i] |= uint32(reverseHexTable[s[j]]) << 24 + u.id[i] |= uint32(reverseHexTable[s[j]]) << 24 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[i] |= uint32(reverseHexTable[s[j]]) << 20 + u.id[i] |= uint32(reverseHexTable[s[j]]) << 20 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[i] |= uint32(reverseHexTable[s[j]]) << 16 + u.id[i] |= uint32(reverseHexTable[s[j]]) << 16 j++ // skip hypens @@ -220,25 +233,25 @@ func (u *UUID) unmarshalText128(s []byte) error { if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[i] |= uint32(reverseHexTable[s[j]]) << 12 + u.id[i] |= uint32(reverseHexTable[s[j]]) << 12 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[i] |= uint32(reverseHexTable[s[j]]) << 8 + u.id[i] |= uint32(reverseHexTable[s[j]]) << 8 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[i] |= uint32(reverseHexTable[s[j]]) << 4 + u.id[i] |= uint32(reverseHexTable[s[j]]) << 4 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[i] |= uint32(reverseHexTable[s[j]]) + u.id[i] |= uint32(reverseHexTable[s[j]]) j++ } @@ -248,58 +261,58 @@ func (u *UUID) unmarshalText128(s []byte) error { // Using the reverseHexTable rebuild the UUID from the string s represented in bytes // This implementation is the inverse of MarshalText and reaches performance pairity func (u *UUID) unmarshalText32(s []byte) error { - u[0] = 0x5F9B34FB - u[1] = 0x80000080 - u[2] = 0x00001000 + u.id[0] = 0x5F9B34FB + u.id[1] = 0x80000080 + u.id[2] = 0x00001000 var j uint8 = 0 if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) << 28 + u.id[3] |= uint32(reverseHexTable[s[j]]) << 28 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) << 24 + u.id[3] |= uint32(reverseHexTable[s[j]]) << 24 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) << 20 + u.id[3] |= uint32(reverseHexTable[s[j]]) << 20 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) << 16 + u.id[3] |= uint32(reverseHexTable[s[j]]) << 16 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) << 12 + u.id[3] |= uint32(reverseHexTable[s[j]]) << 12 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) << 8 + u.id[3] |= uint32(reverseHexTable[s[j]]) << 8 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) << 4 + u.id[3] |= uint32(reverseHexTable[s[j]]) << 4 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) + u.id[3] |= uint32(reverseHexTable[s[j]]) j++ return nil @@ -308,33 +321,33 @@ func (u *UUID) unmarshalText32(s []byte) error { // Using the reverseHexTable rebuild the UUID from the string s represented in bytes // This implementation is the inverse of MarshalText and reaches performance pairity func (u *UUID) unmarshalText16(s []byte) error { - u[0] = 0x5F9B34FB - u[1] = 0x80000080 - u[2] = 0x00001000 + u.id[0] = 0x5F9B34FB + u.id[1] = 0x80000080 + u.id[2] = 0x00001000 var j uint8 = 0 if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) << 12 + u.id[3] |= uint32(reverseHexTable[s[j]]) << 12 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) << 8 + u.id[3] |= uint32(reverseHexTable[s[j]]) << 8 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) << 4 + u.id[3] |= uint32(reverseHexTable[s[j]]) << 4 j++ if reverseHexTable[s[j]] == 255 { return errInvalidUUID } - u[3] |= uint32(reverseHexTable[s[j]]) + u.id[3] |= uint32(reverseHexTable[s[j]]) j++ return nil @@ -377,11 +390,11 @@ func (u UUID) AppendText(buf []byte) ([]byte, error) { buf = append(buf, '-') } - buf = append(buf, hexDigitLower[byte(u[i]>>24)>>4]) - buf = append(buf, hexDigitLower[byte(u[i]>>24)&0xF]) + buf = append(buf, hexDigitLower[byte(u.id[i]>>24)>>4]) + buf = append(buf, hexDigitLower[byte(u.id[i]>>24)&0xF]) - buf = append(buf, hexDigitLower[byte(u[i]>>16)>>4]) - buf = append(buf, hexDigitLower[byte(u[i]>>16)&0xF]) + buf = append(buf, hexDigitLower[byte(u.id[i]>>16)>>4]) + buf = append(buf, hexDigitLower[byte(u.id[i]>>16)&0xF]) // Insert a hyphen at the correct locations. // position 6 and 10 @@ -389,11 +402,11 @@ func (u UUID) AppendText(buf []byte) ([]byte, error) { buf = append(buf, '-') } - buf = append(buf, hexDigitLower[byte(u[i]>>8)>>4]) - buf = append(buf, hexDigitLower[byte(u[i]>>8)&0xF]) + buf = append(buf, hexDigitLower[byte(u.id[i]>>8)>>4]) + buf = append(buf, hexDigitLower[byte(u.id[i]>>8)&0xF]) - buf = append(buf, hexDigitLower[byte(u[i])>>4]) - buf = append(buf, hexDigitLower[byte(u[i])&0xF]) + buf = append(buf, hexDigitLower[byte(u.id[i])>>4]) + buf = append(buf, hexDigitLower[byte(u.id[i])&0xF]) } return buf, nil @@ -408,15 +421,15 @@ func (u UUID) MarshalText() ([]byte, error) { var ErrInvalidBinaryUUID = errors.New("bluetooth: failed to unmarshal the given binary UUID") -// UnmarshalBinary copies the given uuid bytes onto itself +// UnmarshalBinary copies the given uuid bytes in little-endian order onto itself. func (u *UUID) UnmarshalBinary(uuid []byte) error { if len(uuid) != 16 { return ErrInvalidBinaryUUID } - u[0] = uint32(uuid[0]) | uint32(uuid[1])<<8 | uint32(uuid[2])<<16 | uint32(uuid[3])<<24 - u[1] = uint32(uuid[4]) | uint32(uuid[5])<<8 | uint32(uuid[6])<<16 | uint32(uuid[7])<<24 - u[2] = uint32(uuid[8]) | uint32(uuid[9])<<8 | uint32(uuid[10])<<16 | uint32(uuid[11])<<24 - u[3] = uint32(uuid[12]) | uint32(uuid[13])<<8 | uint32(uuid[14])<<16 | uint32(uuid[15])<<24 + u.id[0] = uint32(uuid[0]) | uint32(uuid[1])<<8 | uint32(uuid[2])<<16 | uint32(uuid[3])<<24 + u.id[1] = uint32(uuid[4]) | uint32(uuid[5])<<8 | uint32(uuid[6])<<16 | uint32(uuid[7])<<24 + u.id[2] = uint32(uuid[8]) | uint32(uuid[9])<<8 | uint32(uuid[10])<<16 | uint32(uuid[11])<<24 + u.id[3] = uint32(uuid[12]) | uint32(uuid[13])<<8 | uint32(uuid[14])<<16 | uint32(uuid[15])<<24 return nil } diff --git a/uuid_sd.go b/uuid_sd.go index e9deb8b..db36099 100644 --- a/uuid_sd.go +++ b/uuid_sd.go @@ -12,12 +12,12 @@ type shortUUID C.ble_uuid_t func (uuid UUID) shortUUID() (C.ble_uuid_t, C.uint32_t) { var short C.ble_uuid_t - short.uuid = C.uint16_t(uuid[3]) + short.uuid = C.uint16_t(uuid.id[3]) if uuid.Is16Bit() { short._type = C.BLE_UUID_TYPE_BLE return short, 0 } - errCode := C.sd_ble_uuid_vs_add((*C.ble_uuid128_t)(unsafe.Pointer(&uuid[0])), &short._type) + errCode := C.sd_ble_uuid_vs_add((*C.ble_uuid128_t)(unsafe.Pointer(&uuid.id[0])), &short._type) return short, errCode } @@ -28,7 +28,7 @@ func (s shortUUID) UUID() UUID { } var outLen C.uint8_t var outUUID UUID - C.sd_ble_uuid_encode(((*C.ble_uuid_t)(unsafe.Pointer(&s))), &outLen, ((*C.uint8_t)(unsafe.Pointer(&outUUID)))) + C.sd_ble_uuid_encode(((*C.ble_uuid_t)(unsafe.Pointer(&s))), &outLen, ((*C.uint8_t)(unsafe.Pointer(&outUUID.id)))) return outUUID } diff --git a/uuid_test.go b/uuid_test.go index 93af1b2..7d827bd 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -100,6 +100,14 @@ func TestNewUUID(t *testing.T) { } } +func TestUUIDBytesRoundTrip(t *testing.T) { + uuid := NewUUID([16]byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}) + rt := NewUUID(uuid.BytesBigEndian()) + if uuid != rt { + t.Errorf("%s does not match %s", uuid, rt) + } +} + func BenchmarkUUIDToString(b *testing.B) { uuid, e := ParseUUID("00001234-0000-1000-8000-00805f9b34fb") if e != nil { From a2457af62730dbba5c037b6be3076a6b489a9581 Mon Sep 17 00:00:00 2001 From: lemmi Date: Tue, 10 Feb 2026 02:50:48 +0100 Subject: [PATCH 07/10] gap_hci: remove superflous print --- gap_hci.go | 1 - 1 file changed, 1 deletion(-) diff --git a/gap_hci.go b/gap_hci.go index 59463d9..d27e357 100644 --- a/gap_hci.go +++ b/gap_hci.go @@ -76,7 +76,6 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { rp := rawAdvertisementPayload{len: a.hci.advData.eirLength} copy(rp.data[:], a.hci.advData.eirData[:a.hci.advData.eirLength]) if rp.LocalName() != "" { - println("LocalName:", rp.LocalName()) adf.LocalName = rp.LocalName() } From a88a5c43aaa3a0dc168946af1246023b37f2c4f6 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 12 Feb 2026 12:48:27 +0100 Subject: [PATCH 08/10] sponsorship: add explicit callout/link in README to help out TinyGo Signed-off-by: deadprogram --- .github/FUNDING.yml | 3 +++ README.md | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..869e436 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +open_collective: tinygo diff --git a/README.md b/README.md index 1252c51..83b66db 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ By using [TinyGo](https://tinygo.org/), it can also be used running "bare metal" The Go Bluetooth package can be used to create both Bluetooth Low Energy Centrals as well as to create Bluetooth Low Energy Peripherals. +> [!IMPORTANT] +> You can help TinyGo with a financial contribution using OpenCollective. Please see https://opencollective.com/tinygo for more information. Thank you! + ## Bluetooth Low Energy Central A typical Bluetooth Low Energy Central would be your laptop computer or mobile phone. From 30770f14023c4d97376a405f568d0f8db17597b2 Mon Sep 17 00:00:00 2001 From: Alexis Couvreur Date: Sun, 1 Mar 2026 05:32:33 -0500 Subject: [PATCH 09/10] fix: Windows characteristic ordering in DiscoverCharacteristics (#410) * fix: Windows characteristic ordering in DiscoverCharacteristics The characteristics will now appear on the expected order * range over filterUUIDs --- gattc_windows.go | 50 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/gattc_windows.go b/gattc_windows.go index 2d8119f..f0f69ac 100644 --- a/gattc_windows.go +++ b/gattc_windows.go @@ -3,6 +3,7 @@ package bluetooth import ( "errors" "fmt" + "slices" "syscall" "unsafe" @@ -196,6 +197,13 @@ func (s DeviceService) DiscoverCharacteristics(filterUUIDs []UUID) ([]DeviceChar } var characteristics []DeviceCharacteristic + + if len(filterUUIDs) > 0 { + // The caller wants to get a list of characteristics in a specific + // order. + characteristics = make([]DeviceCharacteristic, len(filterUUIDs)) + } + for i := uint32(0); i < characteristicsSize; i++ { c, err := charVector.GetAt(i) if err != nil { @@ -217,33 +225,53 @@ func (s DeviceService) DiscoverCharacteristics(filterUUIDs []UUID) ([]DeviceChar // only include characteristics that are included in the input filter if len(filterUUIDs) > 0 { - found := false - for _, uuid := range filterUUIDs { + for j, uuid := range filterUUIDs { + if characteristics[j] != (DeviceCharacteristic{}) { + // To support multiple identical characteristics, we + // need to ignore the characteristics that are already + // found. See: + // https://github.com/tinygo-org/bluetooth/issues/131 + continue + } if characteristicUUID.String() == uuid.String() { // One of the characteristics we're looking for. - found = true + characteristics[j] = s.makeCharacteristic(characteristicUUID, characteristic, properties) break } } - if !found { - continue - } + } else { + // The caller wants to get all characteristics, in any order. + characteristics = append(characteristics, s.makeCharacteristic(characteristicUUID, characteristic, properties)) } + } - characteristics = append(characteristics, DeviceCharacteristic{ - uuidWrapper: characteristicUUID, + if slices.Contains(characteristics, (DeviceCharacteristic{})) { + return nil, errors.New("bluetooth: did not find all requested characteristic") + } + + return characteristics, nil +} + +// Small helper to create a DeviceCharacteristic object. +func (s DeviceService) makeCharacteristic(uuid UUID, characteristic *genericattributeprofile.GattCharacteristic, properties genericattributeprofile.GattCharacteristicProperties) DeviceCharacteristic { + char := DeviceCharacteristic{ + deviceCharacteristic: &deviceCharacteristic{ + uuidWrapper: uuid, service: s, characteristic: characteristic, properties: properties, - }) + }, } - - return characteristics, nil + return char } // DeviceCharacteristic is a BLE characteristic on a connected peripheral // device. type DeviceCharacteristic struct { + *deviceCharacteristic +} + +type deviceCharacteristic struct { uuidWrapper characteristic *genericattributeprofile.GattCharacteristic From 421e7a6c4669a31203a06bac56678a23169cee34 Mon Sep 17 00:00:00 2001 From: Soraya Date: Fri, 6 Mar 2026 13:03:40 +0100 Subject: [PATCH 10/10] *: detecting abrupt disconnections in Linux This PR implements persistent D-Bus signal monitoring at the adapter level to detect abrupt disconnections. The `connectHandler` callback was not being invoked when Bluetooth devices disconnected abruptly (e.g., device powered off, out of range, battery died) on Linux. It only worked for planned disconnections via `Device.Disconnect()`. This created an inconsistency with the Darwin implementation, which correctly detects all disconnections through CoreBluetooth's `DidDisconnectPeripheral` delegate method. --- adapter_linux.go | 87 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/adapter_linux.go b/adapter_linux.go index 2e49b80..ced2458 100644 --- a/adapter_linux.go +++ b/adapter_linux.go @@ -23,7 +23,10 @@ type Adapter struct { address string defaultAdvertisement *Advertisement - connectHandler func(device Device, connected bool) + connectHandler func(device Device, connected bool) + connectionMonitorChan chan *dbus.Signal + monitoringConnections bool + stopMonitorChan chan struct{} } // NewAdapter creates a new Adapter with the given ID. @@ -61,6 +64,11 @@ func (a *Adapter) Enable() (err error) { } addr.Store(&a.address) + // Start connection monitoring if connectHandler is set + if a.connectHandler != nil { + a.startConnectionMonitoring() + } + return nil } @@ -74,3 +82,80 @@ func (a *Adapter) Address() (MACAddress, error) { } return MACAddress{MAC: mac}, nil } + +func (a *Adapter) startConnectionMonitoring() error { + if a.monitoringConnections { + return nil // already monitoring + } + + a.connectionMonitorChan = make(chan *dbus.Signal, 10) + a.stopMonitorChan = make(chan struct{}) + a.bus.Signal(a.connectionMonitorChan) + + if err := a.bus.AddMatchSignal(matchOptionsPropertiesChanged...); err != nil { + return fmt.Errorf("bluetooth: add dbus match signal: %w", err) + } + + a.monitoringConnections = true + go a.handleConnectionSignals() + + return nil +} + +func (a *Adapter) handleConnectionSignals() { + for { + select { + case <-a.stopMonitorChan: + return + case sig, ok := <-a.connectionMonitorChan: + if !ok { + return // channel was closed + } + if sig.Name != dbusSignalPropertiesChanged { + continue + } + interfaceName, ok := sig.Body[0].(string) + if !ok || interfaceName != bluezDevice1Interface { + continue + } + changes, ok := sig.Body[1].(map[string]dbus.Variant) + if !ok { + continue + } + if connectedVariant, ok := changes["Connected"]; ok { + connected, ok := connectedVariant.Value().(bool) + if !ok { + continue + } + device := Device{ + device: a.bus.Object("org.bluez", sig.Path), + adapter: a, + } + var props map[string]dbus.Variant + if err := device.device.Call("org.freedesktop.DBus.Properties.GetAll", + 0, bluezDevice1Interface).Store(&props); err != nil { + continue + } + if err := device.parseProperties(&props); err != nil { + continue + } + a.connectHandler(device, connected) + } + } + } +} + +func (a *Adapter) StopConnectionMonitoring() error { + if !a.monitoringConnections { + return nil + } + // Unregister the signal channel from D-Bus before closing + a.bus.RemoveSignal(a.connectionMonitorChan) + if err := a.bus.RemoveMatchSignal(matchOptionsPropertiesChanged...); err != nil { + return fmt.Errorf("bluetooth: remove dbus match signal: %w", err) + } + close(a.stopMonitorChan) + close(a.connectionMonitorChan) + a.monitoringConnections = false + return nil +}