From f639d800125d343c0b326b2898ee7b04d67de3d0 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 6 Jan 2024 19:07:18 +0100 Subject: [PATCH 01/10] ninafw: add support for software RTS/CTS flow control for boards where hardware support is not available Signed-off-by: deadprogram --- adapter_ninafw.go | 27 +++++++++++++++++++++------ hci_ninafw.go | 46 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/adapter_ninafw.go b/adapter_ninafw.go index 7ff3c052..c246b2c9 100644 --- a/adapter_ninafw.go +++ b/adapter_ninafw.go @@ -41,7 +41,6 @@ var DefaultAdapter = &Adapter{ func (a *Adapter) Enable() error { // reset the NINA in BLE mode machine.NINA_CS.Configure(machine.PinConfig{Mode: machine.PinOutput}) - machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput}) machine.NINA_CS.Low() if machine.NINA_RESET_INVERTED { @@ -51,16 +50,28 @@ func (a *Adapter) Enable() error { } // serial port for nina chip - uart := machine.UART1 - uart.Configure(machine.UARTConfig{ + uart := machine.UART_NINA + cfg := machine.UARTConfig{ TX: machine.NINA_TX, RX: machine.NINA_RX, BaudRate: machine.NINA_BAUDRATE, - CTS: machine.NINA_CTS, - RTS: machine.NINA_RTS, - }) + } + if !machine.NINA_SOFT_FLOWCONTROL { + cfg.CTS = machine.NINA_CTS + cfg.RTS = machine.NINA_RTS + } + + uart.Configure(cfg) a.hci, a.att = newBLEStack(uart) + if machine.NINA_SOFT_FLOWCONTROL { + a.hci.softRTS = machine.NINA_RTS + a.hci.softRTS.Configure(machine.PinConfig{Mode: machine.PinOutput}) + a.hci.softRTS.High() + + a.hci.softCTS = machine.NINA_CTS + machine.NINA_CTS.Configure(machine.PinConfig{Mode: machine.PinInput}) + } a.hci.start() @@ -122,6 +133,8 @@ func makeNINAAddress(mac MAC) [6]uint8 { } func resetNINA() { + machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput}) + machine.NINA_RESETN.High() time.Sleep(100 * time.Millisecond) machine.NINA_RESETN.Low() @@ -129,6 +142,8 @@ func resetNINA() { } func resetNINAInverted() { + machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput}) + machine.NINA_RESETN.Low() time.Sleep(100 * time.Millisecond) machine.NINA_RESETN.High() diff --git a/hci_ninafw.go b/hci_ninafw.go index 4b1db956..7952e8a2 100644 --- a/hci_ninafw.go +++ b/hci_ninafw.go @@ -117,6 +117,8 @@ type leConnectData struct { type hci struct { uart *machine.UART + softCTS machine.Pin + softRTS machine.Pin att *att buf []byte address [6]byte @@ -129,12 +131,21 @@ type hci struct { } func newHCI(uart *machine.UART) *hci { - return &hci{uart: uart, - buf: make([]byte, 256), + return &hci{ + uart: uart, + softCTS: machine.NoPin, + softRTS: machine.NoPin, + buf: make([]byte, 256), } } func (h *hci) start() error { + if h.softRTS != machine.NoPin { + h.softRTS.Low() + + defer h.softRTS.High() + } + for h.uart.Buffered() > 0 { h.uart.ReadByte() } @@ -151,6 +162,12 @@ func (h *hci) reset() error { } func (h *hci) poll() error { + if h.softRTS != machine.NoPin { + h.softRTS.Low() + + defer h.softRTS.High() + } + i := 0 for h.uart.Buffered() > 0 { data, _ := h.uart.ReadByte() @@ -322,7 +339,7 @@ func (h *hci) sendCommandWithParams(opcode uint16, params []byte) error { h.buf[3] = byte(len(params)) copy(h.buf[4:], params) - if _, err := h.uart.Write(h.buf[:4+len(params)]); err != nil { + if _, err := h.write(h.buf[:4+len(params)]); err != nil { return err } @@ -356,13 +373,34 @@ func (h *hci) sendAclPkt(handle uint16, cid uint8, data []byte) error { println("hci send acl data", handle, cid, hex.EncodeToString(h.buf[:9+len(data)])) } - if _, err := h.uart.Write(h.buf[:9+len(data)]); err != nil { + if _, err := h.write(h.buf[:9+len(data)]); err != nil { return err } return nil } +const writeAttempts = 200 + +func (h *hci) write(buf []byte) (int, error) { + if h.softCTS != machine.NoPin { + retries := writeAttempts + for h.softCTS.Get() { + retries-- + if retries == 0 { + return 0, ErrHCITimeout + } + } + } + + n, err := h.uart.Write(buf) + if err != nil { + return 0, err + } + + return n, nil +} + type aclDataHeader struct { handle uint16 dlen uint16 From 56e56f36473207e481da964246a01e57e72da9dc Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 6 Jan 2024 19:19:58 +0100 Subject: [PATCH 02/10] build: add arduino-nano33 and pyportal to smoke tests Signed-off-by: deadprogram --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fa065766..efa73b26 100644 --- a/Makefile +++ b/Makefile @@ -32,10 +32,12 @@ smoketest-tinygo: @md5sum test.hex $(TINYGO) build -o test.hex -size=short -target=microbit-v2-s113v7 ./examples/nusserver @md5sum test.hex - $(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/scanner - @md5sum test.hex $(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/discover @md5sum test.hex + $(TINYGO) build -o test.uf2 -size=short -target=arduino-nano33 ./examples/discover + @md5sum test.hex + $(TINYGO) build -o test.uf2 -size=short -target=pyportal ./examples/discover + @md5sum test.hex smoketest-linux: # Test on Linux. From 735333aa1a379b33318aa4db8a19914491b63add Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sun, 24 Dec 2023 19:08:52 +0100 Subject: [PATCH 03/10] softdevice: print connection parameters when debug is enabled This is very useful for debugging, though we should probably expose this in some way to users of the bluetooth package without changing a constant. --- adapter_nrf528xx-full.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/adapter_nrf528xx-full.go b/adapter_nrf528xx-full.go index 6ce048b3..f772863b 100644 --- a/adapter_nrf528xx-full.go +++ b/adapter_nrf528xx-full.go @@ -62,6 +62,14 @@ func handleEvent() { C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT) } DefaultAdapter.connectHandler(Address{}, false) + case C.BLE_GAP_EVT_CONN_PARAM_UPDATE: + if debug { + // Print connection parameters for easy debugging. + params := gapEvent.params.unionfield_conn_param_update().conn_params + interval_ms := params.min_conn_interval * 125 / 100 // min and max are the same here + print("conn param update interval=", interval_ms, "ms latency=", params.slave_latency, " timeout=", params.conn_sup_timeout*10, "ms") + println() + } case C.BLE_GAP_EVT_ADV_REPORT: advReport := gapEvent.params.unionfield_adv_report() if debug && &scanReportBuffer.data[0] != (*byte)(unsafe.Pointer(advReport.data.p_data)) { From 6e0df0ec3cfcce2690a426d5c98375a735749da3 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 25 Dec 2023 14:19:23 +0100 Subject: [PATCH 04/10] softdevice: add address of connecting device I thought it wasn't available, but in fact it is. So let's make it available in the connect handler. --- adapter_nrf51.go | 12 +++++++++++- adapter_nrf528xx-full.go | 8 ++++---- adapter_nrf528xx-peripheral.go | 3 ++- adapter_nrf528xx.go | 8 ++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/adapter_nrf51.go b/adapter_nrf51.go index 2b98cc19..9f38dcc1 100644 --- a/adapter_nrf51.go +++ b/adapter_nrf51.go @@ -43,7 +43,9 @@ func handleEvent() { switch id { case C.BLE_GAP_EVT_CONNECTED: currentConnection.handle.Reg = uint16(gapEvent.conn_handle) - DefaultAdapter.connectHandler(Address{}, true) + connectEvent := gapEvent.params.unionfield_connected() + address := Address{makeMACAddress(connectEvent.peer_addr)} + DefaultAdapter.connectHandler(address, true) case C.BLE_GAP_EVT_DISCONNECTED: if defaultAdvertisement.isAdvertising.Get() != 0 { // The advertisement was running but was automatically stopped @@ -111,3 +113,11 @@ func (a *Adapter) Address() (MACAddress, error) { } return MACAddress{MAC: makeAddress(addr.addr)}, nil } + +// Convert a C.ble_gap_addr_t to a MACAddress struct. +func makeMACAddress(addr C.ble_gap_addr_t) MACAddress { + return MACAddress{ + MAC: makeAddress(addr.addr), + isRandom: addr.addr_type != 0, + } +} diff --git a/adapter_nrf528xx-full.go b/adapter_nrf528xx-full.go index f772863b..bd4f4c18 100644 --- a/adapter_nrf528xx-full.go +++ b/adapter_nrf528xx-full.go @@ -25,20 +25,21 @@ func handleEvent() { switch id { case C.BLE_GAP_EVT_CONNECTED: connectEvent := gapEvent.params.unionfield_connected() + address := Address{makeMACAddress(connectEvent.peer_addr)} switch connectEvent.role { case C.BLE_GAP_ROLE_PERIPH: if debug { println("evt: connected in peripheral role") } currentConnection.handle.Reg = uint16(gapEvent.conn_handle) - DefaultAdapter.connectHandler(Address{}, true) + DefaultAdapter.connectHandler(address, true) case C.BLE_GAP_ROLE_CENTRAL: if debug { println("evt: connected in central role") } connectionAttempt.connectionHandle = gapEvent.conn_handle connectionAttempt.state.Set(2) // connection was successful - DefaultAdapter.connectHandler(Address{}, true) + DefaultAdapter.connectHandler(address, true) } case C.BLE_GAP_EVT_DISCONNECTED: if debug { @@ -81,8 +82,7 @@ func handleEvent() { scanReportBuffer.len = byte(advReport.data.len) globalScanResult.RSSI = int16(advReport.rssi) globalScanResult.Address = Address{ - MACAddress{MAC: makeAddress(advReport.peer_addr.addr), - isRandom: advReport.peer_addr.bitfield_addr_type() != 0}, + makeMACAddress(advReport.peer_addr), } globalScanResult.AdvertisementPayload = &scanReportBuffer // Signal to the main thread that there was a scan report. diff --git a/adapter_nrf528xx-peripheral.go b/adapter_nrf528xx-peripheral.go index 5e9f181a..d26d3921 100644 --- a/adapter_nrf528xx-peripheral.go +++ b/adapter_nrf528xx-peripheral.go @@ -28,7 +28,8 @@ func handleEvent() { println("evt: connected in peripheral role") } currentConnection.handle.Reg = uint16(gapEvent.conn_handle) - DefaultAdapter.connectHandler(Address{}, true) + connectEvent := gapEvent.params.unionfield_connected() + DefaultAdapter.connectHandler(Address{makeMACAddress(connectEvent.peer_addr)}, true) case C.BLE_GAP_EVT_DISCONNECTED: if debug { println("evt: disconnected") diff --git a/adapter_nrf528xx.go b/adapter_nrf528xx.go index d05ab027..557699d8 100644 --- a/adapter_nrf528xx.go +++ b/adapter_nrf528xx.go @@ -59,3 +59,11 @@ func (a *Adapter) Address() (MACAddress, error) { } return MACAddress{MAC: makeAddress(addr.addr)}, nil } + +// Convert a C.ble_gap_addr_t to a MACAddress struct. +func makeMACAddress(addr C.ble_gap_addr_t) MACAddress { + return MACAddress{ + MAC: makeAddress(addr.addr), + isRandom: addr.bitfield_addr_type() != 0, + } +} From c9eafaff20dd7ba6f377e8403d29260c0e3d2235 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 25 Dec 2023 14:28:56 +0100 Subject: [PATCH 05/10] all: make Device a value instead of a pointer This is a refactor that is necessary to make it easier to work with connected central devices on a SoftDevice. --- adapter_ninafw.go | 10 ++++---- examples/discover/main.go | 2 +- examples/heartrate-monitor/main.go | 2 +- gap_darwin.go | 28 +++++++++++++--------- gap_linux.go | 10 ++++---- gap_ninafw.go | 37 ++++++++++++++++++------------ gap_nrf528xx-central.go | 10 ++++---- gap_windows.go | 24 +++++++++---------- gattc_darwin.go | 4 ++-- gattc_ninafw.go | 4 ++-- gattc_sd.go | 2 +- gattc_windows.go | 4 ++-- 12 files changed, 75 insertions(+), 62 deletions(-) diff --git a/adapter_ninafw.go b/adapter_ninafw.go index c246b2c9..5ae4eec8 100644 --- a/adapter_ninafw.go +++ b/adapter_ninafw.go @@ -21,7 +21,7 @@ type Adapter struct { connectHandler func(device Address, connected bool) - connectedDevices []*Device + connectedDevices []Device notificationsStarted bool } @@ -33,7 +33,7 @@ var DefaultAdapter = &Adapter{ connectHandler: func(device Address, connected bool) { return }, - connectedDevices: make([]*Device, 0, maxConnections), + connectedDevices: make([]Device, 0, maxConnections), } // Enable configures the BLE stack. It must be called before any @@ -185,7 +185,7 @@ func (a *Adapter) startNotifications() { } d := a.findDevice(not.connectionHandle) - if d == nil { + if d.deviceInternal == nil { if _debug { println("no device found for handle", not.connectionHandle) } @@ -212,7 +212,7 @@ func (a *Adapter) startNotifications() { }() } -func (a *Adapter) findDevice(handle uint16) *Device { +func (a *Adapter) findDevice(handle uint16) Device { for _, d := range a.connectedDevices { if d.handle == handle { if _debug { @@ -223,5 +223,5 @@ func (a *Adapter) findDevice(handle uint16) *Device { } } - return nil + return Device{} } diff --git a/examples/discover/main.go b/examples/discover/main.go index e4142e61..1b9be0ea 100644 --- a/examples/discover/main.go +++ b/examples/discover/main.go @@ -43,7 +43,7 @@ func main() { } }) - var device *bluetooth.Device + var device bluetooth.Device select { case result := <-ch: device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{}) diff --git a/examples/heartrate-monitor/main.go b/examples/heartrate-monitor/main.go index 85cd3e9b..210be9c5 100644 --- a/examples/heartrate-monitor/main.go +++ b/examples/heartrate-monitor/main.go @@ -52,7 +52,7 @@ func main() { } }) - var device *bluetooth.Device + var device bluetooth.Device select { case result := <-ch: device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{}) diff --git a/gap_darwin.go b/gap_darwin.go index b9f5dee6..5b7fd5d1 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -85,6 +85,10 @@ func (a *Adapter) StopScan() error { // Device is a connection to a remote peripheral. type Device struct { + *deviceInternal +} + +type deviceInternal struct { delegate *peripheralDelegate cm cbgo.CentralManager @@ -97,14 +101,14 @@ type Device struct { } // Connect starts a connection attempt to the given peripheral device address. -func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, error) { +func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) { uuid, err := cbgo.ParseUUID(address.UUID.String()) if err != nil { - return nil, err + return Device{}, err } prphs := a.cm.RetrievePeripheralsWithIdentifiers([]cbgo.UUID{uuid}) if len(prphs) == 0 { - return nil, fmt.Errorf("Connect failed: no peer with address: %s", address.UUID.String()) + return Device{}, fmt.Errorf("Connect failed: no peer with address: %s", address.UUID.String()) } timeout := defaultConnectionTimeout @@ -129,14 +133,16 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er // check if we have received a disconnected peripheral if p.State() == cbgo.PeripheralStateDisconnected { - return nil, connectionError + return Device{}, connectionError } - d := &Device{ - cm: a.cm, - prph: p, - servicesChan: make(chan error), - charsChan: make(chan error), + d := Device{ + &deviceInternal{ + cm: a.cm, + prph: p, + servicesChan: make(chan error), + charsChan: make(chan error), + }, } d.delegate = &peripheralDelegate{d: d} @@ -162,7 +168,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er // Disconnect from the BLE device. This method is non-blocking and does not // wait until the connection is fully gone. -func (d *Device) Disconnect() error { +func (d Device) Disconnect() error { d.cm.CancelConnect(d.prph) return nil } @@ -172,7 +178,7 @@ func (d *Device) Disconnect() error { type peripheralDelegate struct { cbgo.PeripheralDelegateBase - d *Device + d Device } // DidDiscoverServices is called when the services for a Peripheral diff --git a/gap_linux.go b/gap_linux.go index 7bd2973e..c4697080 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -300,9 +300,9 @@ type Device struct { // Connect starts a connection attempt to the given peripheral device address. // // On Linux and Windows, the IsRandom part of the address is ignored. -func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, error) { +func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) { devicePath := dbus.ObjectPath(string(a.adapter.Path()) + "/dev_" + strings.Replace(address.MAC.String(), ":", "_", -1)) - device := &Device{ + device := Device{ device: a.bus.Object("org.bluez", devicePath), adapter: a, address: address, @@ -321,7 +321,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er // Read whether this device is already connected. connected, err := device.device.GetProperty("org.bluez.Device1.Connected") if err != nil { - return nil, err + return Device{}, err } // Connect to the device, if not already connected. @@ -329,7 +329,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er // Start connecting (async). err := device.device.Call("org.bluez.Device1.Connect", 0).Err if err != nil { - return nil, fmt.Errorf("bluetooth: failed to connect: %w", err) + return Device{}, fmt.Errorf("bluetooth: failed to connect: %w", err) } // Wait until the device has connected. @@ -360,7 +360,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er // Disconnect from the BLE device. This method is non-blocking and does not // wait until the connection is fully gone. -func (d *Device) Disconnect() error { +func (d Device) Disconnect() error { // we don't call our cancel function here, instead we wait for the // property change in `watchForConnect` and cancel things then return d.device.Call("org.bluez.Device1.Disconnect", 0).Err diff --git a/gap_ninafw.go b/gap_ninafw.go index 7ea9aefa..51fbbf5b 100644 --- a/gap_ninafw.go +++ b/gap_ninafw.go @@ -133,7 +133,7 @@ type Address struct { } // Connect starts a connection attempt to the given peripheral device address. -func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, error) { +func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) { if _debug { println("Connect") } @@ -145,14 +145,14 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er if err := a.hci.leCreateConn(0x0060, 0x0030, 0x00, random, makeNINAAddress(address.MAC), 0x00, 0x0006, 0x000c, 0x0000, 0x00c8, 0x0004, 0x0006); err != nil { - return nil, err + return Device{}, err } // are we connected? start := time.Now().UnixNano() for { if err := a.hci.poll(); err != nil { - return nil, err + return Device{}, err } if a.hci.connectData.connected { @@ -163,15 +163,18 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er random = true } - d := &Device{adapter: a, - handle: a.hci.connectData.handle, + d := Device{ Address: Address{ MACAddress{ MAC: makeAddress(a.hci.connectData.peerBdaddr), isRandom: random}, }, - mtu: defaultMTU, - notificationRegistrations: make([]notificationRegistration, 0), + deviceInternal: &deviceInternal{ + adapter: a, + handle: a.hci.connectData.handle, + mtu: defaultMTU, + notificationRegistrations: make([]notificationRegistration, 0), + }, } a.connectedDevices = append(a.connectedDevices, d) @@ -189,10 +192,10 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er // cancel connection attempt that failed if err := a.hci.leCancelConn(); err != nil { - return nil, err + return Device{}, err } - return nil, ErrConnect + return Device{}, ErrConnect } type notificationRegistration struct { @@ -202,8 +205,12 @@ type notificationRegistration struct { // Device is a connection to a remote peripheral. type Device struct { - adapter *Adapter Address Address + *deviceInternal +} + +type deviceInternal struct { + adapter *Adapter handle uint16 mtu uint16 @@ -211,7 +218,7 @@ type Device struct { } // Disconnect from the BLE device. -func (d *Device) Disconnect() error { +func (d Device) Disconnect() error { if _debug { println("Disconnect") } @@ -219,11 +226,11 @@ func (d *Device) Disconnect() error { return err } - d.adapter.connectedDevices = []*Device{} + d.adapter.connectedDevices = []Device{} return nil } -func (d *Device) findNotificationRegistration(handle uint16) *notificationRegistration { +func (d Device) findNotificationRegistration(handle uint16) *notificationRegistration { for _, n := range d.notificationRegistrations { if n.handle == handle { return &n @@ -233,7 +240,7 @@ func (d *Device) findNotificationRegistration(handle uint16) *notificationRegist return nil } -func (d *Device) addNotificationRegistration(handle uint16, callback func([]byte)) { +func (d Device) addNotificationRegistration(handle uint16, callback func([]byte)) { d.notificationRegistrations = append(d.notificationRegistrations, notificationRegistration{ handle: handle, @@ -241,6 +248,6 @@ func (d *Device) addNotificationRegistration(handle uint16, callback func([]byte }) } -func (d *Device) startNotifications() { +func (d Device) startNotifications() { d.adapter.startNotifications() } diff --git a/gap_nrf528xx-central.go b/gap_nrf528xx-central.go index 44fe683f..148a2cba 100644 --- a/gap_nrf528xx-central.go +++ b/gap_nrf528xx-central.go @@ -109,7 +109,7 @@ var connectionAttempt struct { // connection attempt at once and that the address parameter must have the // IsRandom bit set correctly. This bit is set correctly for scan results, so // you can reuse that address directly. -func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, error) { +func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) { // Construct an address object as used in the SoftDevice. var addr C.ble_gap_addr_t addr.addr = makeSDAddress(address.MAC) @@ -158,7 +158,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er // This should be safe as long as Connect is not called concurrently. And // even then, it should catch most such race conditions. if connectionAttempt.state.Get() != 0 { - return nil, errAlreadyConnecting + return Device{}, errAlreadyConnecting } connectionAttempt.state.Set(1) @@ -166,7 +166,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er errCode := C.sd_ble_gap_connect(&addr, &scanParams, &connectionParams, C.BLE_CONN_CFG_TAG_DEFAULT) if errCode != 0 { connectionAttempt.state.Set(0) - return nil, Error(errCode) + return Device{}, Error(errCode) } // Wait until the connection is established. @@ -179,13 +179,13 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er connectionAttempt.state.Set(0) // Connection has been established. - return &Device{ + return Device{ connectionHandle: connectionHandle, }, nil } // Disconnect from the BLE device. -func (d *Device) Disconnect() error { +func (d Device) Disconnect() error { errCode := C.sd_ble_gap_disconnect(d.connectionHandle, C.BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION) if errCode != 0 { return Error(errCode) diff --git a/gap_windows.go b/gap_windows.go index 8b66cb68..5ae0f972 100644 --- a/gap_windows.go +++ b/gap_windows.go @@ -170,7 +170,7 @@ type Device struct { // Connect starts a connection attempt to the given peripheral device address. // // On Linux and Windows, the IsRandom part of the address is ignored. -func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, error) { +func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) { var winAddr uint64 for i := range address.MAC { winAddr += uint64(address.MAC[i]) << (8 * i) @@ -179,23 +179,23 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er // IAsyncOperation bleDeviceOp, err := bluetooth.FromBluetoothAddressAsync(winAddr) if err != nil { - return nil, err + return Device{}, err } // We need to pass the signature of the parameter returned by the async operation: // IAsyncOperation if err := awaitAsyncOperation(bleDeviceOp, bluetooth.SignatureBluetoothLEDevice); err != nil { - return nil, fmt.Errorf("error connecting to device: %w", err) + return Device{}, fmt.Errorf("error connecting to device: %w", err) } res, err := bleDeviceOp.GetResults() if err != nil { - return nil, err + return Device{}, err } // The returned BluetoothLEDevice is set to null if FromBluetoothAddressAsync can't find the device identified by bluetoothAddress if uintptr(res) == 0x0 { - return nil, fmt.Errorf("device with the given address was not found") + return Device{}, fmt.Errorf("device with the given address was not found") } bleDevice := (*bluetooth.BluetoothLEDevice)(res) @@ -204,7 +204,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er // To initiate a connection, we need to set GattSession.MaintainConnection to true. dID, err := bleDevice.GetBluetoothDeviceId() if err != nil { - return nil, err + return Device{}, err } // Windows does not support explicitly connecting to a device. @@ -212,29 +212,29 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er // by the calling program. gattSessionOp, err := genericattributeprofile.FromDeviceIdAsync(dID) // IAsyncOperation if err != nil { - return nil, err + return Device{}, err } if err := awaitAsyncOperation(gattSessionOp, genericattributeprofile.SignatureGattSession); err != nil { - return nil, fmt.Errorf("error getting gatt session: %w", err) + return Device{}, fmt.Errorf("error getting gatt session: %w", err) } gattRes, err := gattSessionOp.GetResults() if err != nil { - return nil, err + return Device{}, err } newSession := (*genericattributeprofile.GattSession)(gattRes) // This keeps the device connected until we set maintain_connection = False. if err := newSession.SetMaintainConnection(true); err != nil { - return nil, err + return Device{}, err } - return &Device{bleDevice, newSession}, nil + return Device{bleDevice, newSession}, nil } // Disconnect from the BLE device. This method is non-blocking and does not // wait until the connection is fully gone. -func (d *Device) Disconnect() error { +func (d Device) Disconnect() error { defer d.device.Release() defer d.session.Release() diff --git a/gattc_darwin.go b/gattc_darwin.go index c4abf72f..2d737dad 100644 --- a/gattc_darwin.go +++ b/gattc_darwin.go @@ -14,7 +14,7 @@ import ( // // Passing a nil slice of UUIDs will return a complete list of // services. -func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { +func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { d.prph.DiscoverServices([]cbgo.UUID{}) // clear cache of services @@ -69,7 +69,7 @@ type DeviceService struct { type deviceService struct { uuidWrapper - device *Device + device Device service cbgo.Service characteristics []DeviceCharacteristic diff --git a/gattc_ninafw.go b/gattc_ninafw.go index 00f1fb7b..9ff0e5f6 100644 --- a/gattc_ninafw.go +++ b/gattc_ninafw.go @@ -35,7 +35,7 @@ const ( type DeviceService struct { uuid UUID - device *Device + device Device startHandle, endHandle uint16 } @@ -51,7 +51,7 @@ func (s DeviceService) UUID() UUID { // // Passing a nil slice of UUIDs will return a complete list of // services. -func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { +func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { if _debug { println("DiscoverServices") } diff --git a/gattc_sd.go b/gattc_sd.go index e0fe8c77..5b3029b9 100644 --- a/gattc_sd.go +++ b/gattc_sd.go @@ -59,7 +59,7 @@ func (s DeviceService) UUID() UUID { // // On the Nordic SoftDevice, only one service discovery procedure may be done at // a time. -func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { +func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { if discoveringService.state.Get() != 0 { // Not concurrency safe, but should catch most concurrency misuses. return nil, errAlreadyDiscovering diff --git a/gattc_windows.go b/gattc_windows.go index 4735edda..3877df37 100644 --- a/gattc_windows.go +++ b/gattc_windows.go @@ -30,7 +30,7 @@ var ( // // Passing a nil slice of UUIDs will return a complete list of // services. -func (d *Device) DiscoverServices(filterUUIDs []UUID) ([]DeviceService, error) { +func (d Device) DiscoverServices(filterUUIDs []UUID) ([]DeviceService, error) { // IAsyncOperation getServicesOperation, err := d.device.GetGattServicesWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached) if err != nil { @@ -133,7 +133,7 @@ type DeviceService struct { uuidWrapper service *genericattributeprofile.GattDeviceService - device *Device + device Device } // UUID returns the UUID for this DeviceService. From 5d805a929cf384d59e49c54dbc209fa1e5e31b09 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 25 Dec 2023 14:52:10 +0100 Subject: [PATCH 06/10] all: use Device instead of Address in SetConnectHandler This makes it possible to discover services on a connected central while in peripheral mode, for example. --- adapter.go | 2 +- adapter_darwin.go | 6 +++--- adapter_linux.go | 4 ++-- adapter_ninafw.go | 4 ++-- adapter_nrf51.go | 12 +++++++++--- adapter_nrf528xx-full.go | 14 ++++++++++---- adapter_nrf528xx-peripheral.go | 11 +++++++++-- adapter_sd.go | 4 ++-- adapter_windows.go | 4 ++-- examples/circuitplay/main.go | 2 +- examples/stop-advertisement/main.go | 2 +- gap_darwin.go | 7 +++++-- gap_linux.go | 5 +++-- gap_nrf528xx-central.go | 5 ----- gap_sd.go | 15 +++++++++++++++ 15 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 gap_sd.go diff --git a/adapter.go b/adapter.go index f3f4528c..b18aa35d 100644 --- a/adapter.go +++ b/adapter.go @@ -6,6 +6,6 @@ const debug = false // SetConnectHandler sets a handler function to be called whenever the adaptor connects // or disconnects. You must call this before you call adaptor.Connect() for centrals // or adaptor.Start() for peripherals in order for it to work. -func (a *Adapter) SetConnectHandler(c func(device Address, connected bool)) { +func (a *Adapter) SetConnectHandler(c func(device Device, connected bool)) { a.connectHandler = c } diff --git a/adapter_darwin.go b/adapter_darwin.go index 06c7ffc9..292d1680 100644 --- a/adapter_darwin.go +++ b/adapter_darwin.go @@ -24,7 +24,7 @@ type Adapter struct { // used to allow multiple callers to call Connect concurrently. connectMap sync.Map - connectHandler func(device Address, connected bool) + connectHandler func(device Device, connected bool) } // DefaultAdapter is the default adapter on the system. @@ -35,7 +35,7 @@ var DefaultAdapter = &Adapter{ pm: cbgo.NewPeripheralManager(nil), connectMap: sync.Map{}, - connectHandler: func(device Address, connected bool) { + connectHandler: func(device Device, connected bool) { return }, } @@ -106,7 +106,7 @@ func (cmd *centralManagerDelegate) DidDisconnectPeripheral(cmgr cbgo.CentralMana addr := Address{} uuid, _ := ParseUUID(id) addr.UUID = uuid - cmd.a.connectHandler(addr, false) + cmd.a.connectHandler(Device{Address: addr}, false) // like with DidConnectPeripheral, check if we have a chan allocated for this and send through the peripheral // this will only be true if the receiving side is still waiting for a connection to complete diff --git a/adapter_linux.go b/adapter_linux.go index 0ea9b452..8eac7083 100644 --- a/adapter_linux.go +++ b/adapter_linux.go @@ -23,7 +23,7 @@ type Adapter struct { address string defaultAdvertisement *Advertisement - connectHandler func(device Address, connected bool) + connectHandler func(device Device, connected bool) } // DefaultAdapter is the default adapter on the system. On Linux, it is the @@ -32,7 +32,7 @@ type Adapter struct { // Make sure to call Enable() before using it to initialize the adapter. var DefaultAdapter = &Adapter{ id: defaultAdapter, - connectHandler: func(device Address, connected bool) { + connectHandler: func(device Device, connected bool) { }, } diff --git a/adapter_ninafw.go b/adapter_ninafw.go index 5ae4eec8..c55e6c2a 100644 --- a/adapter_ninafw.go +++ b/adapter_ninafw.go @@ -19,7 +19,7 @@ type Adapter struct { isDefault bool scanning bool - connectHandler func(device Address, connected bool) + connectHandler func(device Device, connected bool) connectedDevices []Device notificationsStarted bool @@ -30,7 +30,7 @@ type Adapter struct { // Make sure to call Enable() before using it to initialize the adapter. var DefaultAdapter = &Adapter{ isDefault: true, - connectHandler: func(device Address, connected bool) { + connectHandler: func(device Device, connected bool) { return }, connectedDevices: make([]Device, 0, maxConnections), diff --git a/adapter_nrf51.go b/adapter_nrf51.go index 9f38dcc1..2208ecc2 100644 --- a/adapter_nrf51.go +++ b/adapter_nrf51.go @@ -44,8 +44,11 @@ func handleEvent() { case C.BLE_GAP_EVT_CONNECTED: currentConnection.handle.Reg = uint16(gapEvent.conn_handle) connectEvent := gapEvent.params.unionfield_connected() - address := Address{makeMACAddress(connectEvent.peer_addr)} - DefaultAdapter.connectHandler(address, true) + device := Device{ + Address: Address{makeMACAddress(connectEvent.peer_addr)}, + connectionHandle: gapEvent.conn_handle, + } + DefaultAdapter.connectHandler(device, true) case C.BLE_GAP_EVT_DISCONNECTED: if defaultAdvertisement.isAdvertising.Get() != 0 { // The advertisement was running but was automatically stopped @@ -57,7 +60,10 @@ func handleEvent() { defaultAdvertisement.start() } currentConnection.handle.Reg = C.BLE_CONN_HANDLE_INVALID - DefaultAdapter.connectHandler(Address{}, false) + device := Device{ + connectionHandle: gapEvent.conn_handle, + } + DefaultAdapter.connectHandler(device, false) case C.BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST: // Respond with the default PPCP connection parameters by passing // nil: diff --git a/adapter_nrf528xx-full.go b/adapter_nrf528xx-full.go index bd4f4c18..73b01718 100644 --- a/adapter_nrf528xx-full.go +++ b/adapter_nrf528xx-full.go @@ -25,21 +25,24 @@ func handleEvent() { switch id { case C.BLE_GAP_EVT_CONNECTED: connectEvent := gapEvent.params.unionfield_connected() - address := Address{makeMACAddress(connectEvent.peer_addr)} + device := Device{ + Address: Address{makeMACAddress(connectEvent.peer_addr)}, + connectionHandle: gapEvent.conn_handle, + } switch connectEvent.role { case C.BLE_GAP_ROLE_PERIPH: if debug { println("evt: connected in peripheral role") } currentConnection.handle.Reg = uint16(gapEvent.conn_handle) - DefaultAdapter.connectHandler(address, true) + DefaultAdapter.connectHandler(device, true) case C.BLE_GAP_ROLE_CENTRAL: if debug { println("evt: connected in central role") } connectionAttempt.connectionHandle = gapEvent.conn_handle connectionAttempt.state.Set(2) // connection was successful - DefaultAdapter.connectHandler(address, true) + DefaultAdapter.connectHandler(device, true) } case C.BLE_GAP_EVT_DISCONNECTED: if debug { @@ -62,7 +65,10 @@ func handleEvent() { // necessary. C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT) } - DefaultAdapter.connectHandler(Address{}, false) + device := Device{ + connectionHandle: gapEvent.conn_handle, + } + DefaultAdapter.connectHandler(device, false) case C.BLE_GAP_EVT_CONN_PARAM_UPDATE: if debug { // Print connection parameters for easy debugging. diff --git a/adapter_nrf528xx-peripheral.go b/adapter_nrf528xx-peripheral.go index d26d3921..00860df6 100644 --- a/adapter_nrf528xx-peripheral.go +++ b/adapter_nrf528xx-peripheral.go @@ -29,7 +29,11 @@ func handleEvent() { } currentConnection.handle.Reg = uint16(gapEvent.conn_handle) connectEvent := gapEvent.params.unionfield_connected() - DefaultAdapter.connectHandler(Address{makeMACAddress(connectEvent.peer_addr)}, true) + device := Device{ + Address: Address{makeMACAddress(connectEvent.peer_addr)}, + connectionHandle: gapEvent.conn_handle, + } + DefaultAdapter.connectHandler(device, true) case C.BLE_GAP_EVT_DISCONNECTED: if debug { println("evt: disconnected") @@ -45,7 +49,10 @@ func handleEvent() { // necessary. C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT) } - DefaultAdapter.connectHandler(Address{}, false) + device := Device{ + connectionHandle: gapEvent.conn_handle, + } + DefaultAdapter.connectHandler(device, false) case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST: // We need to respond with sd_ble_gap_data_length_update. Setting // both parameters to nil will make sure we send the default values. diff --git a/adapter_sd.go b/adapter_sd.go index 44590ab6..4037ed9b 100644 --- a/adapter_sd.go +++ b/adapter_sd.go @@ -48,7 +48,7 @@ type Adapter struct { scanning bool charWriteHandlers []charWriteHandler - connectHandler func(device Address, connected bool) + connectHandler func(device Device, connected bool) } // DefaultAdapter is the default adapter on the current system. On Nordic chips, @@ -56,7 +56,7 @@ type Adapter struct { // // Make sure to call Enable() before using it to initialize the adapter. var DefaultAdapter = &Adapter{isDefault: true, - connectHandler: func(device Address, connected bool) { + connectHandler: func(device Device, connected bool) { return }} diff --git a/adapter_windows.go b/adapter_windows.go index 2211cab4..747c9731 100644 --- a/adapter_windows.go +++ b/adapter_windows.go @@ -12,14 +12,14 @@ import ( type Adapter struct { watcher *advertisement.BluetoothLEAdvertisementWatcher - connectHandler func(device Address, connected bool) + connectHandler func(device Device, connected bool) } // DefaultAdapter is the default adapter on the system. // // Make sure to call Enable() before using it to initialize the adapter. var DefaultAdapter = &Adapter{ - connectHandler: func(device Address, connected bool) { + connectHandler: func(device Device, connected bool) { return }, } diff --git a/examples/circuitplay/main.go b/examples/circuitplay/main.go index 213e6766..a299fde6 100644 --- a/examples/circuitplay/main.go +++ b/examples/circuitplay/main.go @@ -38,7 +38,7 @@ func main() { neo.Configure(machine.PinConfig{Mode: machine.PinOutput}) ws = ws2812.New(neo) - adapter.SetConnectHandler(func(d bluetooth.Address, c bool) { + adapter.SetConnectHandler(func(d bluetooth.Device, c bool) { connected = c if !connected && !disconnected { diff --git a/examples/stop-advertisement/main.go b/examples/stop-advertisement/main.go index ea60c6e5..d5c5a9b5 100644 --- a/examples/stop-advertisement/main.go +++ b/examples/stop-advertisement/main.go @@ -21,7 +21,7 @@ func main() { must("config adv", adv.Configure(bluetooth.AdvertisementOptions{ LocalName: "Go Bluetooth", })) - adapter.SetConnectHandler(func(device bluetooth.Address, connected bool) { + adapter.SetConnectHandler(func(device bluetooth.Device, connected bool) { if connected { println("connected, not advertising...") advState = false diff --git a/gap_darwin.go b/gap_darwin.go index 5b7fd5d1..c87db057 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -85,6 +85,8 @@ func (a *Adapter) StopScan() error { // Device is a connection to a remote peripheral. type Device struct { + Address Address + *deviceInternal } @@ -137,7 +139,8 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err } d := Device{ - &deviceInternal{ + Address: address, + deviceInternal: &deviceInternal{ cm: a.cm, prph: p, servicesChan: make(chan error), @@ -148,7 +151,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err d.delegate = &peripheralDelegate{d: d} p.SetDelegate(d.delegate) - a.connectHandler(address, true) + a.connectHandler(d, true) return d, nil diff --git a/gap_linux.go b/gap_linux.go index c4697080..2c77c36a 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -292,9 +292,10 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult { // Device is a connection to a remote peripheral. type Device struct { + Address Address // the MAC address of the device + device dbus.BusObject // bluez device interface adapter *Adapter // the adapter that was used to form this device connection - address Address // the address of the device } // Connect starts a connection attempt to the given peripheral device address. @@ -303,9 +304,9 @@ type Device struct { func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) { devicePath := dbus.ObjectPath(string(a.adapter.Path()) + "/dev_" + strings.Replace(address.MAC.String(), ":", "_", -1)) device := Device{ + Address: address, device: a.bus.Object("org.bluez", devicePath), adapter: a, - address: address, } // Already start watching for property changes. We do this before reading diff --git a/gap_nrf528xx-central.go b/gap_nrf528xx-central.go index 148a2cba..b451573b 100644 --- a/gap_nrf528xx-central.go +++ b/gap_nrf528xx-central.go @@ -92,11 +92,6 @@ func (a *Adapter) StopScan() error { return nil } -// Device is a connection to a remote peripheral. -type Device struct { - connectionHandle C.uint16_t -} - // In-progress connection attempt. var connectionAttempt struct { state volatile.Register8 // 0 means unused, 1 means connecting, 2 means ready (connected or timeout) diff --git a/gap_sd.go b/gap_sd.go new file mode 100644 index 00000000..2147db42 --- /dev/null +++ b/gap_sd.go @@ -0,0 +1,15 @@ +//go:build softdevice + +package bluetooth + +/* +#include "ble_gap.h" +*/ +import "C" + +// Device is a connection to a remote peripheral or central. +type Device struct { + Address Address + + connectionHandle C.uint16_t +} From d74f6a1009ac7d65dac1f52e2d520af46aeecb77 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 25 Dec 2023 15:29:49 +0100 Subject: [PATCH 07/10] all: add RequestConnectionParams to request new connection parameters This allows changing the connection latency, slave latency, and connection timeout of an active connection - whether in the central or peripheral role. This is especially helpful on battery operated BLE devices that don't have a lot of power and need to lower the connection latency for improved speed. It might also be useful for devices that need high speed, as the defaults might be too low. --- Makefile | 3 ++ examples/connparams/main.go | 84 +++++++++++++++++++++++++++++++++++++ gap.go | 8 +++- gap_darwin.go | 12 ++++++ gap_linux.go | 11 +++++ gap_ninafw.go | 10 +++++ gap_nrf528xx-central.go | 31 ++++++++++++++ gap_windows.go | 12 ++++++ 8 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 examples/connparams/main.go diff --git a/Makefile b/Makefile index efa73b26..28da8159 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ smoketest-tinygo: @md5sum test.hex $(TINYGO) build -o test.uf2 -size=short -target=circuitplay-bluefruit ./examples/circuitplay @md5sum test.hex + $(TINYGO) build -o test.hex -size=short -target=circuitplay-bluefruit ./examples/connparams + @md5sum test.hex $(TINYGO) build -o test.uf2 -size=short -target=circuitplay-bluefruit ./examples/discover @md5sum test.hex $(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/heartrate @@ -42,6 +44,7 @@ smoketest-tinygo: smoketest-linux: # Test on Linux. GOOS=linux go build -o /tmp/go-build-discard ./examples/advertisement + GOOS=linux go build -o /tmp/go-build-discard ./examples/connparams GOOS=linux go build -o /tmp/go-build-discard ./examples/heartrate GOOS=linux go build -o /tmp/go-build-discard ./examples/heartrate-monitor GOOS=linux go build -o /tmp/go-build-discard ./examples/nusserver diff --git a/examples/connparams/main.go b/examples/connparams/main.go new file mode 100644 index 00000000..762cbb92 --- /dev/null +++ b/examples/connparams/main.go @@ -0,0 +1,84 @@ +// Test for setting connection parameters. +// +// To test this feature, run this either on a desktop OS or by flashing it to a +// device with TinyGo. Then connect to it from a BLE connection debugger, for +// example nRF Connect on Android. After a second, you should see in the log of +// the BLE app that the connection latency has been updated. It might look +// something like this: +// +// Connection parameters updated (interval: 510.0ms, latency: 0, timeout: 10000ms) +package main + +import ( + "time" + + "tinygo.org/x/bluetooth" +) + +var ( + adapter = bluetooth.DefaultAdapter + newDevice chan bluetooth.Device +) + +func main() { + must("enable BLE stack", adapter.Enable()) + + newDevice = make(chan bluetooth.Device, 1) + adapter.SetConnectHandler(func(device bluetooth.Device, connected bool) { + // If this is a new device, signal it to the separate goroutine. + if connected { + select { + case newDevice <- device: + default: + } + } + }) + + // Start advertising, so we can be found. + const name = "Go BLE test" + adv := adapter.DefaultAdvertisement() + adv.Configure(bluetooth.AdvertisementOptions{ + LocalName: name, + }) + adv.Start() + println("advertising:", name) + + for device := range newDevice { + println("connection from device:", device.Address.String()) + + // Discover services and characteristics. + svcs, err := device.DiscoverServices(nil) + if err != nil { + println(" failed to resolve services:", err) + } + for _, svc := range svcs { + println(" service:", svc.UUID().String()) + chars, err := svc.DiscoverCharacteristics(nil) + if err != nil { + println(" failed to resolve characteristics:", err) + } + for _, char := range chars { + println(" characteristic:", char.UUID().String()) + } + } + + // Update connection parameters (as a test). + time.Sleep(time.Second) + err = device.RequestConnectionParams(bluetooth.ConnectionParams{ + MinInterval: bluetooth.NewDuration(495 * time.Millisecond), + MaxInterval: bluetooth.NewDuration(510 * time.Millisecond), + Timeout: bluetooth.NewDuration(10 * time.Second), + }) + if err != nil { + println(" failed to update connection parameters:", err) + continue + } + println(" updated connection parameters") + } +} + +func must(action string, err error) { + if err != nil { + panic("failed to " + action + ": " + err.Error()) + } +} diff --git a/gap.go b/gap.go index 17dddfd6..f6a418c8 100644 --- a/gap.go +++ b/gap.go @@ -391,7 +391,8 @@ func (buf *rawAdvertisementPayload) addServiceUUID(uuid UUID) (ok bool) { } } -// ConnectionParams are used when connecting to a peripherals. +// ConnectionParams are used when connecting to a peripherals or when changing +// the parameters of an active connection. type ConnectionParams struct { // The timeout for the connection attempt. Not used during the rest of the // connection. If no duration is specified, a default timeout will be used. @@ -403,4 +404,9 @@ type ConnectionParams struct { // will be used. MinInterval Duration MaxInterval Duration + + // Connection Supervision Timeout. After this time has passed with no + // communication, the connection is considered lost. If no timeout is + // specified, the timeout will be unchanged. + Timeout Duration } diff --git a/gap_darwin.go b/gap_darwin.go index c87db057..542535c4 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -176,6 +176,18 @@ func (d Device) Disconnect() error { return nil } +// RequestConnectionParams requests a different connection latency and timeout +// of the given device connection. Fields that are unset will be left alone. +// Whether or not the device will actually honor this, depends on the device and +// on the specific parameters. +// +// This call has not yet been implemented on macOS. +func (d Device) RequestConnectionParams(params ConnectionParams) error { + // TODO: implement this using setDesiredConnectionLatency, see: + // https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393277-setdesiredconnectionlatency + return nil +} + // Peripheral delegate functions type peripheralDelegate struct { diff --git a/gap_linux.go b/gap_linux.go index 2c77c36a..80ecf1a1 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -366,3 +366,14 @@ func (d Device) Disconnect() error { // property change in `watchForConnect` and cancel things then return d.device.Call("org.bluez.Device1.Disconnect", 0).Err } + +// RequestConnectionParams requests a different connection latency and timeout +// of the given device connection. Fields that are unset will be left alone. +// Whether or not the device will actually honor this, depends on the device and +// on the specific parameters. +// +// On Linux, this call doesn't do anything because BlueZ doesn't support +// changing the connection latency. +func (d Device) RequestConnectionParams(params ConnectionParams) error { + return nil +} diff --git a/gap_ninafw.go b/gap_ninafw.go index 51fbbf5b..5cf78402 100644 --- a/gap_ninafw.go +++ b/gap_ninafw.go @@ -230,6 +230,16 @@ func (d Device) Disconnect() error { return nil } +// RequestConnectionParams requests a different connection latency and timeout +// of the given device connection. Fields that are unset will be left alone. +// Whether or not the device will actually honor this, depends on the device and +// on the specific parameters. +// +// On NINA, this call hasn't been implemented yet. +func (d Device) RequestConnectionParams(params ConnectionParams) error { + return nil +} + func (d Device) findNotificationRegistration(handle uint16) *notificationRegistration { for _, n := range d.notificationRegistrations { if n.handle == handle { diff --git a/gap_nrf528xx-central.go b/gap_nrf528xx-central.go index b451573b..085e7d02 100644 --- a/gap_nrf528xx-central.go +++ b/gap_nrf528xx-central.go @@ -188,3 +188,34 @@ func (d Device) Disconnect() error { return nil } + +// RequestConnectionParams requests a different connection latency and timeout +// of the given device connection. Fields that are unset will be left alone. +// Whether or not the device will actually honor this, depends on the device and +// on the specific parameters. +// +// On the Nordic SoftDevice, this call will also set the slave latency to 0. +func (d Device) RequestConnectionParams(params ConnectionParams) error { + // The default parameters if no specific parameters are picked. + connParams := C.ble_gap_conn_params_t{ + min_conn_interval: C.BLE_GAP_CP_MIN_CONN_INTVL_NONE, + max_conn_interval: C.BLE_GAP_CP_MAX_CONN_INTVL_NONE, + slave_latency: 0, + conn_sup_timeout: C.BLE_GAP_CP_CONN_SUP_TIMEOUT_NONE, + } + + // Use specified parameters if available. + if params.MinInterval != 0 { + connParams.min_conn_interval = C.uint16_t(params.MinInterval) / 2 + } + if params.MaxInterval != 0 { + connParams.max_conn_interval = C.uint16_t(params.MaxInterval) / 2 + } + if params.Timeout != 0 { + connParams.conn_sup_timeout = C.uint16_t(params.Timeout) / 16 + } + + // Send them to peer device. + errCode := C.sd_ble_gap_conn_param_update(d.connectionHandle, &connParams) + return makeError(errCode) +} diff --git a/gap_windows.go b/gap_windows.go index 5ae0f972..c800c253 100644 --- a/gap_windows.go +++ b/gap_windows.go @@ -247,3 +247,15 @@ func (d Device) Disconnect() error { return nil } + +// RequestConnectionParams requests a different connection latency and timeout +// of the given device connection. Fields that are unset will be left alone. +// Whether or not the device will actually honor this, depends on the device and +// on the specific parameters. +// +// On Windows, this call doesn't do anything. +func (d Device) RequestConnectionParams(params ConnectionParams) error { + // TODO: implement this using + // BluetoothLEDevice.RequestPreferredConnectionParameters. + return nil +} From 3d478bb32d016ccce4391a99720a90f53b2ecc49 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 13 Jan 2024 15:38:46 +0100 Subject: [PATCH 08/10] ninafw: implement BLE peripheral support Signed-off-by: deadprogram --- att_ninafw.go | 672 +++++++++++++++++++++++++++++++++++++++++++----- gap_ninafw.go | 136 ++++++++++ gatts.go | 5 + gatts_ninafw.go | 102 ++++++++ hci_ninafw.go | 76 +++++- 5 files changed, 912 insertions(+), 79 deletions(-) diff --git a/att_ninafw.go b/att_ninafw.go index 70b85913..ad29cd38 100644 --- a/att_ninafw.go +++ b/att_ninafw.go @@ -62,10 +62,11 @@ const ( attErrorUnsupportedGroupType = 0x10 attErrorInsufficientResources = 0x11 - gattUnknownUUID = 0x0000 - gattServiceUUID = 0x2800 - gattCharacteristicUUID = 0x2803 - gattDescriptorUUID = 0x2900 + gattUnknownUUID = 0x0000 + gattServiceUUID = 0x2800 + gattCharacteristicUUID = 0x2803 + gattDescriptorUUID = 0x2900 + gattClientCharacteristicConfigUUID = 0x2902 ) var ( @@ -81,16 +82,109 @@ type rawService struct { uuid UUID } +func (s *rawService) Write(buf []byte) (int, error) { + s.startHandle = binary.LittleEndian.Uint16(buf[0:]) + s.endHandle = binary.LittleEndian.Uint16(buf[2:]) + + sz := 4 + switch len(buf) - 4 { + case 2: + s.uuid = New16BitUUID(binary.LittleEndian.Uint16(buf[4:])) + sz += 2 + case 16: + var uuid [16]byte + copy(uuid[:], buf[4:]) + slices.Reverse(uuid[:]) + s.uuid = NewUUID(uuid) + sz += 16 + } + + return sz, nil +} + +func (s *rawService) Read(p []byte) (int, error) { + binary.LittleEndian.PutUint16(p[0:], s.startHandle) + binary.LittleEndian.PutUint16(p[2:], s.endHandle) + + sz := 4 + switch { + case s.uuid.Is16Bit(): + binary.LittleEndian.PutUint16(p[4:], s.uuid.Get16Bit()) + sz += 2 + default: + uuid := s.uuid.Bytes() + copy(p[4:], uuid[:]) + sz += 16 + } + + return sz, nil +} + type rawCharacteristic struct { startHandle uint16 properties uint8 valueHandle uint16 uuid UUID + chr *Characteristic +} + +func (c *rawCharacteristic) Write(buf []byte) (int, error) { + c.startHandle = binary.LittleEndian.Uint16(buf[0:]) + c.properties = buf[2] + c.valueHandle = binary.LittleEndian.Uint16(buf[3:]) + + sz := 5 + switch len(buf) - 5 { + case 2: + c.uuid = New16BitUUID(binary.LittleEndian.Uint16(buf[5:])) + sz += 2 + case 16: + var uuid [16]byte + copy(uuid[:], buf[5:]) + slices.Reverse(uuid[:]) + c.uuid = NewUUID(uuid) + sz += 16 + } + + return sz, nil +} + +func (c *rawCharacteristic) Read(p []byte) (int, error) { + binary.LittleEndian.PutUint16(p[0:], c.startHandle) + p[2] = c.properties + binary.LittleEndian.PutUint16(p[3:], c.valueHandle) + + sz := 5 + switch { + case c.uuid.Is16Bit(): + binary.LittleEndian.PutUint16(p[5:], c.uuid.Get16Bit()) + sz += 2 + default: + uuid := c.uuid.Bytes() + copy(p[5:], uuid[:]) + sz += 16 + } + + return sz, nil } type rawDescriptor struct { handle uint16 - uuid UUID + data []byte +} + +func (d *rawDescriptor) Write(buf []byte) (int, error) { + d.handle = binary.LittleEndian.Uint16(buf[0:]) + d.data = append(d.data, buf[2:]...) + + return len(d.data) + 2, nil +} + +func (d *rawDescriptor) Read(p []byte) (int, error) { + binary.LittleEndian.PutUint16(p[0:], d.handle) + copy(p[2:], d.data) + + return len(d.data) + 2, nil } type rawNotification struct { @@ -99,6 +193,65 @@ type rawNotification struct { data []byte } +type attributeType int + +const ( + attributeTypeService attributeType = iota + attributeTypeCharacteristic + attributeTypeCharacteristicValue + attributeTypeDescriptor +) + +type rawAttribute struct { + typ attributeType + parent uint16 + handle uint16 + uuid UUID + permissions CharacteristicPermissions + value []byte +} + +func (a *rawAttribute) Write(buf []byte) (int, error) { + return 0, errNotYetImplemented +} + +func (a *rawAttribute) Read(p []byte) (int, error) { + binary.LittleEndian.PutUint16(p[0:], a.handle) + sz := 2 + + switch a.typ { + case attributeTypeCharacteristicValue, attributeTypeDescriptor: + switch { + case a.uuid.Is16Bit(): + binary.LittleEndian.PutUint16(p[sz:], a.uuid.Get16Bit()) + sz += 2 + default: + uuid := a.uuid.Bytes() + copy(p[sz:], uuid[:]) + sz += 16 + } + default: + copy(p[sz:], a.value) + sz += len(a.value) + } + + return sz, nil +} + +func (a *rawAttribute) length() int { + switch a.typ { + case attributeTypeCharacteristicValue, attributeTypeDescriptor: + switch { + case a.uuid.Is16Bit(): + return 2 + default: + return 16 + } + default: + return len(a.value) + } +} + type att struct { hci *hci busy sync.Mutex @@ -113,6 +266,11 @@ type att struct { descriptors []rawDescriptor value []byte notifications chan rawNotification + + connections []uint16 + lastHandle uint16 + attributes []rawAttribute + localServices []rawService } func newATT(hci *hci) *att { @@ -122,6 +280,10 @@ func newATT(hci *hci) *att { characteristics: []rawCharacteristic{}, value: []byte{}, notifications: make(chan rawNotification, 32), + connections: []uint16{}, + lastHandle: 0x0001, + attributes: []rawAttribute{}, + localServices: []rawService{}, } } @@ -277,6 +439,51 @@ func (a *att) sendReq(handle uint16, data []byte) error { return nil } +func (a *att) sendNotification(handle uint16, data []byte) error { + if _debug { + println("att.sendNotifications:", handle, "data:", hex.EncodeToString(data)) + } + + a.busy.Lock() + defer a.busy.Unlock() + + var b [3]byte + b[0] = attOpHandleNotify + binary.LittleEndian.PutUint16(b[1:], handle) + + for connection := range a.connections { + if _debug { + println("att.sendNotifications: sending to", connection) + } + + if err := a.hci.sendAclPkt(uint16(connection), attCID, append(b[:], data...)); err != nil { + return err + } + } + + return nil +} + +func (a *att) sendError(handle uint16, opcode uint8, hdl uint16, code uint8) error { + a.clearResponse() + + if _debug { + println("att.sendError:", handle, "data:", opcode, hdl, code) + } + + var b [5]byte + b[0] = attOpError + b[1] = opcode + binary.LittleEndian.PutUint16(b[2:], hdl) + b[4] = code + + if err := a.hci.sendAclPkt(handle, attCID, b[:]); err != nil { + return err + } + + return nil +} + func (a *att) handleData(handle uint16, buf []byte) error { if _debug { println("att.handleData:", handle, "data:", hex.EncodeToString(buf)) @@ -299,6 +506,11 @@ func (a *att) handleData(handle uint16, buf []byte) error { if _debug { println("att.handleData: attOpMTUReq") } + a.mtu = binary.LittleEndian.Uint16(buf[1:]) + response := [3]byte{attOpMTUResponse, buf[1], buf[2]} + if err := a.hci.sendAclPkt(handle, attCID, response[:]); err != nil { + return err + } case attOpMTUResponse: if _debug { @@ -312,6 +524,11 @@ func (a *att) handleData(handle uint16, buf []byte) error { println("att.handleData: attOpFindInfoReq") } + startHandle := binary.LittleEndian.Uint16(buf[1:]) + endHandle := binary.LittleEndian.Uint16(buf[3:]) + + return a.handleFindInfoReq(handle, startHandle, endHandle) + case attOpFindInfoResponse: if _debug { println("att.handleData: attOpFindInfoResponse") @@ -319,23 +536,13 @@ func (a *att) handleData(handle uint16, buf []byte) error { a.responded = true lengthPerDescriptor := int(buf[1]) - var uuid [16]byte for i := 2; i < len(buf); i += lengthPerDescriptor { - d := rawDescriptor{ - handle: binary.LittleEndian.Uint16(buf[i:]), - } - switch lengthPerDescriptor - 2 { - case 2: - d.uuid = New16BitUUID(binary.LittleEndian.Uint16(buf[i+2:])) - case 16: - copy(uuid[:], buf[i+2:]) - slices.Reverse(uuid[:]) - d.uuid = NewUUID(uuid) - } + d := rawDescriptor{} + d.Write(buf[i : i+lengthPerDescriptor]) if _debug { - println("att.handleData: descriptor", d.handle, d.uuid.String()) + println("att.handleData: descriptor", d.handle, hex.EncodeToString(d.data)) } a.descriptors = append(a.descriptors, d) @@ -351,6 +558,12 @@ func (a *att) handleData(handle uint16, buf []byte) error { println("att.handleData: attOpReadByTypeReq") } + startHandle := binary.LittleEndian.Uint16(buf[1:]) + endHandle := binary.LittleEndian.Uint16(buf[3:]) + uuid := shortUUID(binary.LittleEndian.Uint16(buf[5:])) + + return a.handleReadByTypeReq(handle, startHandle, endHandle, uuid) + case attOpReadByTypeResponse: if _debug { println("att.handleData: attOpReadByTypeResponse") @@ -358,22 +571,10 @@ func (a *att) handleData(handle uint16, buf []byte) error { a.responded = true lengthPerCharacteristic := int(buf[1]) - var uuid [16]byte for i := 2; i < len(buf); i += lengthPerCharacteristic { - c := rawCharacteristic{ - startHandle: binary.LittleEndian.Uint16(buf[i:]), - properties: buf[i+2], - valueHandle: binary.LittleEndian.Uint16(buf[i+3:]), - } - switch lengthPerCharacteristic - 5 { - case 2: - c.uuid = New16BitUUID(binary.LittleEndian.Uint16(buf[i+5:])) - case 16: - copy(uuid[:], buf[i+5:]) - slices.Reverse(uuid[:]) - c.uuid = NewUUID(uuid) - } + c := rawCharacteristic{} + c.Write(buf[i : i+lengthPerCharacteristic]) if _debug { println("att.handleData: characteristic", c.startHandle, c.properties, c.valueHandle, c.uuid.String()) @@ -389,32 +590,11 @@ func (a *att) handleData(handle uint16, buf []byte) error { println("att.handleData: attOpReadByGroupReq") } - // return generic services - var response [14]byte - response[0] = attOpReadByGroupResponse - response[1] = 0x06 // length per service + startHandle := binary.LittleEndian.Uint16(buf[1:]) + endHandle := binary.LittleEndian.Uint16(buf[3:]) + uuid := shortUUID(binary.LittleEndian.Uint16(buf[5:])) - genericAccessService := rawService{ - startHandle: 0, - endHandle: 1, - uuid: ServiceUUIDGenericAccess, - } - binary.LittleEndian.PutUint16(response[2:], genericAccessService.startHandle) - binary.LittleEndian.PutUint16(response[4:], genericAccessService.endHandle) - binary.LittleEndian.PutUint16(response[6:], genericAccessService.uuid.Get16Bit()) - - genericAttributeService := rawService{ - startHandle: 2, - endHandle: 5, - uuid: ServiceUUIDGenericAttribute, - } - binary.LittleEndian.PutUint16(response[8:], genericAttributeService.startHandle) - binary.LittleEndian.PutUint16(response[10:], genericAttributeService.endHandle) - binary.LittleEndian.PutUint16(response[12:], genericAttributeService.uuid.Get16Bit()) - - if err := a.hci.sendAclPkt(handle, attCID, response[:]); err != nil { - return err - } + return a.handleReadByGroupReq(handle, startHandle, endHandle, uuid) case attOpReadByGroupResponse: if _debug { @@ -423,21 +603,10 @@ func (a *att) handleData(handle uint16, buf []byte) error { a.responded = true lengthPerService := int(buf[1]) - var uuid [16]byte for i := 2; i < len(buf); i += lengthPerService { - service := rawService{ - startHandle: binary.LittleEndian.Uint16(buf[i:]), - endHandle: binary.LittleEndian.Uint16(buf[i+2:]), - } - switch lengthPerService - 4 { - case 2: - service.uuid = New16BitUUID(binary.LittleEndian.Uint16(buf[i+4:])) - case 16: - copy(uuid[:], buf[i+4:]) - slices.Reverse(uuid[:]) - service.uuid = NewUUID(uuid) - } + service := rawService{} + service.Write(buf[i : i+lengthPerService]) if _debug { println("att.handleData: service", service.startHandle, service.endHandle, service.uuid.String()) @@ -453,6 +622,9 @@ func (a *att) handleData(handle uint16, buf []byte) error { println("att.handleData: attOpReadReq") } + attrHandle := binary.LittleEndian.Uint16(buf[1:]) + return a.handleReadReq(handle, attrHandle) + case attOpReadBlobReq: if _debug { println("att.handleData: attOpReadBlobReq") @@ -470,6 +642,9 @@ func (a *att) handleData(handle uint16, buf []byte) error { println("att.handleData: attOpWriteReq") } + attrHandle := binary.LittleEndian.Uint16(buf[1:]) + return a.handleWriteReq(handle, attrHandle, buf[3:]) + case attOpWriteCmd: if _debug { println("att.handleData: attOpWriteCmd") @@ -538,6 +713,295 @@ func (a *att) handleData(handle uint16, buf []byte) error { return nil } +func (a *att) handleReadByGroupReq(handle, start, end uint16, uuid shortUUID) error { + var response [64]byte + response[0] = attOpReadByGroupResponse + response[1] = 0x0 // length per service + pos := 2 + + switch uuid { + case shortUUID(gattServiceUUID): + for _, s := range a.localServices { + if s.startHandle >= start && s.endHandle <= end { + if _debug { + println("attOpReadByGroupReq: replying with service", s.startHandle, s.endHandle, s.uuid.String()) + } + + length := 20 + if s.uuid.Is16Bit() { + length = 6 + } + + if response[1] == 0 { + response[1] = byte(length) + } else if response[1] != byte(length) { + // change of UUID size + break + } + + s.Read(response[pos : pos+length]) + pos += length + + if uint16(pos+length) > a.mtu { + break + } + } + } + + switch { + case pos > 2: + if err := a.hci.sendAclPkt(handle, attCID, response[:pos]); err != nil { + return err + } + default: + if err := a.sendError(handle, attOpReadByGroupReq, start, attErrorAttrNotFound); err != nil { + return err + } + } + + return nil + + default: + if _debug { + println("handleReadByGroupReq: unknown uuid", New16BitUUID(uint16(uuid)).String()) + } + if err := a.sendError(handle, attOpReadByGroupReq, start, attErrorAttrNotFound); err != nil { + return err + } + + return nil + } +} + +func (a *att) handleReadByTypeReq(handle, start, end uint16, uuid shortUUID) error { + var response [64]byte + response[0] = attOpReadByTypeResponse + pos := 0 + + switch uuid { + case shortUUID(gattCharacteristicUUID): + pos = 2 + response[1] = 0 + + for _, c := range a.characteristics { + if _debug { + println("handleReadByTypeReq: looking at characteristic", c.startHandle, c.uuid.String()) + } + + if c.startHandle >= start && c.valueHandle <= end { + if _debug { + println("handleReadByTypeReq: replying with characteristic", c.startHandle, c.uuid.String()) + } + + length := 21 + if c.uuid.Is16Bit() { + length = 7 + } + + if response[1] == 0 { + response[1] = byte(length) + } else if response[1] != byte(length) { + // change of UUID size + break + } + + c.Read(response[pos : pos+length]) + pos += length + + if uint16(pos+length) > a.mtu { + break + } + } + } + switch { + case pos > 2: + if err := a.hci.sendAclPkt(handle, attCID, response[:pos]); err != nil { + return err + } + default: + if err := a.sendError(handle, attOpReadByTypeReq, start, attErrorAttrNotFound); err != nil { + return err + } + } + + return nil + + default: + if _debug { + println("handleReadByTypeReq: unknown uuid", New16BitUUID(uint16(uuid)).String()) + } + if err := a.sendError(handle, attOpReadByTypeReq, start, attErrorAttrNotFound); err != nil { + return err + } + + return nil + } +} + +func (a *att) handleFindInfoReq(handle, start, end uint16) error { + var response [64]byte + response[0] = attOpFindInfoResponse + pos := 0 + + pos = 2 + infoType := 0 + response[1] = 0 + + for _, attr := range a.attributes { + if _debug { + println("handleFindInfoReq: looking at attribute") + } + + if attr.handle >= start && attr.handle <= end { + if _debug { + println("handleFindInfoReq: replying with attribute", attr.handle, attr.uuid.String(), attr.typ) + } + + if attr.typ == attributeTypeCharacteristicValue || attr.typ == attributeTypeDescriptor { + infoType = 1 + } else { + infoType = 2 + } + + length := attr.length() + 2 + if response[1] == 0 { + response[1] = byte(infoType) + } else if response[1] != byte(infoType) { + // change of info type + break + } + + attr.Read(response[pos : pos+length]) + pos += length + + if uint16(pos+length) >= a.mtu { + break + } + } + } + switch { + case pos > 2: + if err := a.hci.sendAclPkt(handle, attCID, response[:pos]); err != nil { + return err + } + default: + if err := a.sendError(handle, attOpFindInfoReq, start, attErrorAttrNotFound); err != nil { + return err + } + } + + return nil +} + +func (a *att) handleReadReq(handle, attrHandle uint16) error { + attr := a.findAttribute(attrHandle) + if attr == nil { + if _debug { + println("att.handleReadReq: attribute not found", attrHandle) + } + return a.sendError(handle, attOpReadReq, attrHandle, attErrorAttrNotFound) + } + + var response [64]byte + response[0] = attOpReadResponse + pos := 1 + + switch attr.typ { + case attributeTypeCharacteristicValue: + if _debug { + println("att.handleReadReq: reading characteristic value", attrHandle) + } + + c := a.findCharacteristic(attr.parent) + if c != nil && c.chr != nil { + value, err := c.chr.readValue() + if err != nil { + return a.sendError(handle, attOpReadReq, attrHandle, attErrorReadNotPermitted) + } + + copy(response[pos:], value) + pos += len(value) + + if err := a.hci.sendAclPkt(handle, attCID, response[:pos]); err != nil { + return err + } + } + + case attributeTypeDescriptor: + if _debug { + println("att.handleReadReq: reading descriptor", attrHandle) + } + + c := a.findCharacteristic(attr.parent) + if c != nil && c.chr != nil { + cccd, err := c.chr.readCCCD() + if err != nil { + return a.sendError(handle, attOpReadReq, attrHandle, attErrorReadNotPermitted) + } + + binary.LittleEndian.PutUint16(response[pos:], cccd) + pos += 2 + + if err := a.hci.sendAclPkt(handle, attCID, response[:pos]); err != nil { + return err + } + } + } + + return a.sendError(handle, attOpReadReq, attrHandle, attErrorReadNotPermitted) +} + +func (a *att) handleWriteReq(handle, attrHandle uint16, data []byte) error { + attr := a.findAttribute(attrHandle) + if attr == nil { + if _debug { + println("att.handleWriteReq: attribute not found", attrHandle) + } + return a.sendError(handle, attOpWriteReq, attrHandle, attErrorAttrNotFound) + } + + switch attr.typ { + case attributeTypeCharacteristicValue: + if _debug { + println("att.handleWriteReq: writing characteristic value", attrHandle, hex.EncodeToString(data)) + } + + c := a.findCharacteristic(attr.parent) + if c != nil && c.chr != nil { + if _, err := c.chr.Write(data); err != nil { + return a.sendError(handle, attOpWriteReq, attrHandle, attErrorWriteNotPermitted) + } + + if err := a.hci.sendAclPkt(handle, attCID, []byte{attOpWriteResponse}); err != nil { + return err + } + + return nil + } + + case attributeTypeDescriptor: + if _debug { + println("att.handleWriteReq: writing descriptor", attrHandle, hex.EncodeToString(data)) + } + + c := a.findCharacteristic(attr.parent) + if c != nil && c.chr != nil { + if err := c.chr.writeCCCD(binary.LittleEndian.Uint16(data)); err != nil { + return a.sendError(handle, attOpWriteReq, attrHandle, attErrorWriteNotPermitted) + } + + if err := a.hci.sendAclPkt(handle, attCID, []byte{attOpWriteResponse}); err != nil { + return err + } + + return nil + + } + } + + return a.sendError(handle, attOpWriteReq, attrHandle, attErrorWriteNotPermitted) +} + func (a *att) clearResponse() { a.responded = false a.errored = false @@ -581,3 +1045,71 @@ func (a *att) poll() error { return nil } + +func (a *att) addConnection(handle uint16) { + a.connections = append(a.connections, handle) +} + +func (a *att) removeConnection(handle uint16) { + for i := range a.connections { + if a.connections[i] == handle { + a.connections = append(a.connections[:i], a.connections[i+1:]...) + return + } + } +} + +func (a *att) addLocalAttribute(typ attributeType, parent uint16, uuid UUID, permissions CharacteristicPermissions, value []byte) uint16 { + handle := a.lastHandle + a.attributes = append(a.attributes, + rawAttribute{ + typ: typ, + parent: parent, + handle: handle, + uuid: uuid, + permissions: permissions, + value: append([]byte{}, value...), + }) + a.lastHandle++ + + return handle +} + +func (a *att) addLocalService(start, end uint16, uuid UUID) { + a.localServices = append(a.localServices, rawService{ + startHandle: start, + endHandle: end, + uuid: uuid, + }) +} + +func (a *att) addLocalCharacteristic(startHandle uint16, properties CharacteristicPermissions, valueHandle uint16, uuid UUID, chr *Characteristic) { + a.characteristics = append(a.characteristics, + rawCharacteristic{ + startHandle: startHandle, + properties: uint8(properties), + valueHandle: valueHandle, + uuid: uuid, + chr: chr, + }) +} + +func (a *att) findAttribute(hdl uint16) *rawAttribute { + for i := range a.attributes { + if a.attributes[i].handle == hdl { + return &a.attributes[i] + } + } + + return nil +} + +func (a *att) findCharacteristic(hdl uint16) *rawCharacteristic { + for i := range a.characteristics { + if a.characteristics[i].startHandle == hdl { + return &a.characteristics[i] + } + } + + return nil +} diff --git a/gap_ninafw.go b/gap_ninafw.go index 5cf78402..512f5f6f 100644 --- a/gap_ninafw.go +++ b/gap_ninafw.go @@ -3,7 +3,9 @@ package bluetooth import ( + "encoding/binary" "errors" + "slices" "time" ) @@ -261,3 +263,137 @@ func (d Device) addNotificationRegistration(handle uint16, callback func([]byte) func (d Device) startNotifications() { d.adapter.startNotifications() } + +var defaultAdvertisement Advertisement + +// Advertisement encapsulates a single advertisement instance. +type Advertisement struct { + adapter *Adapter +} + +// DefaultAdvertisement returns the default advertisement instance but does not +// configure it. +func (a *Adapter) DefaultAdvertisement() *Advertisement { + if defaultAdvertisement.adapter == nil { + defaultAdvertisement.adapter = a + + a.AddService( + &Service{ + UUID: ServiceUUIDGenericAccess, + Characteristics: []CharacteristicConfig{ + { + UUID: CharacteristicUUIDDeviceName, + Flags: CharacteristicReadPermission, + }, + { + UUID: CharacteristicUUIDAppearance, + Flags: CharacteristicReadPermission, + }, + }, + }) + a.AddService( + &Service{ + UUID: ServiceUUIDGenericAttribute, + Characteristics: []CharacteristicConfig{ + { + UUID: CharacteristicUUIDServiceChanged, + Flags: CharacteristicIndicatePermission, + }, + }, + }) + } + + return &defaultAdvertisement +} + +// Configure this advertisement. +func (a *Advertisement) Configure(options AdvertisementOptions) error { + // uint8_t type = (_connectable) ? 0x00 : (_localName ? 0x02 : 0x03); + typ := uint8(0x00) + + if err := a.adapter.hci.leSetAdvertisingParameters(uint16(options.Interval), uint16(options.Interval), + typ, 0x00, 0x00, [6]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0x07, 0); err != nil { + return err + } + + var advertisingData [31]byte + advertisingDataLen := uint8(0) + + advertisingData[0] = 0x02 + advertisingData[1] = 0x01 + advertisingData[2] = 0x06 + advertisingDataLen += 3 + + // TODO: handle multiple service UUIDs + if len(options.ServiceUUIDs) > 0 { + uuid := options.ServiceUUIDs[0] + var sz uint8 + + switch { + case uuid.Is16Bit(): + sz = 2 + binary.LittleEndian.PutUint16(advertisingData[5:], uuid.Get16Bit()) + case uuid.Is32Bit(): + sz = 6 + data := uuid.Bytes() + slices.Reverse(data[:]) + copy(advertisingData[5:], data[:]) + } + + advertisingData[3] = sz + 1 + advertisingData[4] = sz + advertisingDataLen += sz + 2 + } + + // TODO: handle manufacturer data + + if err := a.adapter.hci.leSetAdvertisingData(advertisingData[:advertisingDataLen]); err != nil { + return err + } + + var scanResponseData [31]byte + scanResponseDataLen := uint8(0) + + switch { + case len(options.LocalName) > 29: + scanResponseData[1] = 0x08 + scanResponseData[0] = 1 + 29 + copy(scanResponseData[2:], options.LocalName[:29]) + scanResponseDataLen = 31 + case len(options.LocalName) > 0: + scanResponseData[1] = 0x09 + scanResponseData[0] = uint8(1 + len(options.LocalName)) + copy(scanResponseData[2:], options.LocalName) + scanResponseDataLen = uint8(2 + len(options.LocalName)) + } + + return a.adapter.hci.leSetScanResponseData(scanResponseData[:scanResponseDataLen]) +} + +// Start advertisement. May only be called after it has been configured. +func (a *Advertisement) Start() error { + if err := a.adapter.hci.leSetAdvertiseEnable(true); err != nil { + return err + } + + // go routine to poll for HCI events while advertising + go func() { + for { + if err := a.adapter.att.poll(); err != nil { + // TODO: handle error + if _debug { + println("error polling while advertising:", err.Error()) + } + } + + time.Sleep(10 * time.Millisecond) + } + }() + + return nil +} + +// Stop advertisement. May only be called after it has been started. +func (a *Advertisement) Stop() error { + return a.adapter.hci.leSetAdvertiseEnable(false) +} diff --git a/gatts.go b/gatts.go index f092f514..f2559ed3 100644 --- a/gatts.go +++ b/gatts.go @@ -61,3 +61,8 @@ func (p CharacteristicPermissions) WriteWithoutResponse() bool { func (p CharacteristicPermissions) Notify() bool { return p&CharacteristicNotifyPermission != 0 } + +// Indicate returns whether indications are permitted. +func (p CharacteristicPermissions) Indicate() bool { + return p&CharacteristicIndicatePermission != 0 +} diff --git a/gatts_ninafw.go b/gatts_ninafw.go index 904146b1..88c32b53 100644 --- a/gatts_ninafw.go +++ b/gatts_ninafw.go @@ -3,4 +3,106 @@ package bluetooth type Characteristic struct { + adapter *Adapter + handle uint16 + permissions CharacteristicPermissions + value []byte + cccd uint16 +} + +// AddService creates a new service with the characteristics listed in the +// Service struct. +func (a *Adapter) AddService(service *Service) error { + 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() + cuuid := append([]byte{}, data[:]...) + + // add characteristic declaration + charHandle := a.att.addLocalAttribute(attributeTypeCharacteristic, serviceHandle, shortUUID(gattCharacteristicUUID).UUID(), CharacteristicReadPermission, cuuid[:]) + + // add characteristic value + vf := CharacteristicPermissions(0) + if service.Characteristics[i].Flags.Read() { + vf |= CharacteristicReadPermission + } + if service.Characteristics[i].Flags.Write() { + vf |= CharacteristicWritePermission + } + valueHandle = a.att.addLocalAttribute(attributeTypeCharacteristicValue, charHandle, service.Characteristics[i].UUID, vf, service.Characteristics[i].Value) + endHandle = valueHandle + + // add characteristic descriptor + if service.Characteristics[i].Flags.Notify() || + service.Characteristics[i].Flags.Indicate() { + endHandle = a.att.addLocalAttribute(attributeTypeDescriptor, charHandle, shortUUID(gattClientCharacteristicConfigUUID).UUID(), CharacteristicReadPermission|CharacteristicWritePermission, []byte{0, 0}) + } + + if service.Characteristics[i].Handle != nil { + service.Characteristics[i].Handle.adapter = a + service.Characteristics[i].Handle.handle = valueHandle + service.Characteristics[i].Handle.permissions = service.Characteristics[i].Flags + service.Characteristics[i].Handle.value = service.Characteristics[i].Value + } + + if _debug { + println("added characteristic", charHandle, valueHandle, service.Characteristics[i].UUID.String()) + } + + a.att.addLocalCharacteristic(charHandle, service.Characteristics[i].Flags, valueHandle, service.Characteristics[i].UUID, service.Characteristics[i].Handle) + } + + if _debug { + println("added service", serviceHandle, endHandle, service.UUID.String()) + } + + a.att.addLocalService(serviceHandle, endHandle, service.UUID) + + return nil +} + +// Write replaces the characteristic value with a new value. +func (c *Characteristic) Write(p []byte) (n int, err error) { + if !c.permissions.Notify() { + return 0, errNoNotify + } + + c.value = append([]byte{}, p...) + + if c.cccd&0x01 != 0 { + // send notification + c.adapter.att.sendNotification(c.handle, c.value) + } + + return len(c.value), nil +} + +func (c *Characteristic) readCCCD() (uint16, error) { + if !c.permissions.Notify() { + return 0, errNoNotify + } + + return c.cccd, nil +} + +func (c *Characteristic) writeCCCD(val uint16) error { + if !c.permissions.Notify() { + return errNoNotify + } + + c.cccd = val + + return nil +} + +func (c *Characteristic) readValue() ([]byte, error) { + if !c.permissions.Read() { + return nil, errNoRead + } + + return c.value, nil } diff --git a/hci_ninafw.go b/hci_ninafw.go index 7952e8a2..6a777879 100644 --- a/hci_ninafw.go +++ b/hci_ninafw.go @@ -288,7 +288,41 @@ func (h *hci) leSetAdvertiseEnable(enabled bool) error { data[0] = 1 } - return h.sendCommandWithParams(ogfLECtrl<handle, disconnComplete->reason); - // L2CAPSignaling.removeConnection(disconnComplete->handle, disconnComplete->reason); + + handle := binary.LittleEndian.Uint16(buf[3:]) + h.att.removeConnection(handle) return h.leSetAdvertiseEnable(true) @@ -512,7 +563,9 @@ func (h *hci) handleEventData(buf []byte) error { h.connectData.peerBdaddrType = buf[7] copy(h.connectData.peerBdaddr[0:], buf[8:]) - return nil + h.att.addConnection(h.connectData.handle) + + return h.leSetAdvertiseEnable(false) case leMetaEventAdvertisingReport: h.advData.reported = true @@ -567,7 +620,7 @@ func (h *hci) handleEventData(buf []byte) error { binary.LittleEndian.PutUint16(b[10:], 0x000F) binary.LittleEndian.PutUint16(b[12:], 0x0FFF) - return h.sendCommandWithParams(ogfLECtrl<<10|ocfLEParamRequestReply, b[:]) + return h.sendWithoutResponse(ogfLECtrl<<10|ocfLEParamRequestReply, b[:]) case leMetaEventConnectionUpdateComplete: if _debug { @@ -584,6 +637,11 @@ func (h *hci) handleEventData(buf []byte) error { println("leMetaEventGenerateDHKeyComplete") } + case leMetaEventDataLengthChange: + if _debug { + println("leMetaEventDataLengthChange") + } + default: if _debug { println("unknown metaevent", buf[2], buf[3], buf[4], buf[5]) From 0e01e148b904ff2fe465619707cd7f4849f4412e Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 15 Jan 2024 10:13:58 +0100 Subject: [PATCH 09/10] docs: complete README info about nina-fw support Signed-off-by: deadprogram --- README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8745e671..ba40fde2 100644 --- a/README.md +++ b/README.md @@ -99,10 +99,10 @@ func must(action string, err error) { | Connect to peripheral | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Write peripheral characteristics | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Receive notifications | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Advertisement | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :x: | -| Local services | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :x: | -| Local characteristics | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :x: | -| Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :x: | +| Advertisement | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | +| Local services | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | +| Local characteristics | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | +| Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | ## Linux @@ -268,11 +268,23 @@ Flashing will normally reset the board. Go Bluetooth has bare metal support for boards that include a separate ESP32 Bluetooth Low Energy radio co-processor. The ESP32 must be running the Arduino or Adafruit `nina_fw` firmware. -See https://github.com/arduino/nina-fw for more information. +Several boards created by Adafruit and Arduino already have the `nina-fw` firmware pre-loaded. This means you can use TinyGo and the Go Bluetooth package without any additional steps required. -The only currently supported board is the Arduino Nano RP2040 Connect. +Currently supported boards include: -More info soon... +* [Adafruit Metro M4 AirLift](https://www.adafruit.com/product/4000) +* [Adafruit PyBadge](https://www.adafruit.com/product/4200) with [AirLift WiFi FeatherWing](https://www.adafruit.com/product/4264) +* [Adafruit PyPortal](https://www.adafruit.com/product/4116) +* [Arduino Nano 33 IoT](https://docs.arduino.cc/hardware/nano-33-iot) +* [Arduino Nano RP2040 Connect](https://docs.arduino.cc/hardware/nano-rp2040-connect) + +After you have installed TinyGo and the Go Bluetooth package, you should be able to compile/run code for your device. + +For example, this command can be used to compile and flash an Arduino Nano RP2040 Connect board with the example we provide that turns it into a BLE peripheral to act like a heart rate monitor: + + tinygo flash -target nano-rp2040 ./examples/heartrate + +If you want more information about the `nina-fw` firmware, or want to add support for other ESP32-equipped boards, please see https://github.com/arduino/nina-fw ## API stability From 46c28d8b3a45678916c510c5de6828741e63e318 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 15 Jan 2024 10:15:19 +0100 Subject: [PATCH 10/10] build: add nina-fw smoketest as peripheral Signed-off-by: deadprogram --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 28da8159..268d820c 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,8 @@ smoketest-tinygo: @md5sum test.hex $(TINYGO) build -o test.uf2 -size=short -target=pyportal ./examples/discover @md5sum test.hex + $(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/advertisement + @md5sum test.hex smoketest-linux: # Test on Linux.