Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# These are supported funding model platforms

open_collective: tinygo
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
87 changes: 86 additions & 1 deletion adapter_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}
6 changes: 3 additions & 3 deletions att_hci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions gap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions gap_hci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -420,7 +419,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[:])
}
Expand Down
17 changes: 13 additions & 4 deletions gap_nrf528xx-central.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -69,14 +69,23 @@ 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)
if errCode != 0 {
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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion gap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func TestServiceUUIDs(t *testing.T) {
raw string
expected []UUID
}
uuidBytes := ServiceUUIDAdafruitSound.Bytes()
uuidBytes := ServiceUUIDAdafruitSound.bytes()
tests := []testCase{
{},
{
Expand Down
48 changes: 40 additions & 8 deletions gap_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<BluetoothLEDevice,object>
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
Expand All @@ -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
}

Expand Down
13 changes: 8 additions & 5 deletions gattc_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
16 changes: 13 additions & 3 deletions gattc_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading