10d1c71078
Signed-off-by: deadprogram <ron@hybridgroup.com>
400 lines
8.6 KiB
Go
400 lines
8.6 KiB
Go
//go:build ninafw
|
|
|
|
package bluetooth
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"slices"
|
|
"time"
|
|
)
|
|
|
|
const defaultMTU = 23
|
|
|
|
var (
|
|
ErrConnect = errors.New("bluetooth: could not connect")
|
|
)
|
|
|
|
// Scan starts a BLE scan.
|
|
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
|
|
if a.scanning {
|
|
return errScanning
|
|
}
|
|
|
|
if err := a.hci.leSetScanEnable(false, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
// passive scanning, every 40ms, for 30ms
|
|
if err := a.hci.leSetScanParameters(0x00, 0x0080, 0x0030, 0x00, 0x00); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.scanning = true
|
|
|
|
// scan with duplicates
|
|
if err := a.hci.leSetScanEnable(true, false); err != nil {
|
|
return err
|
|
}
|
|
|
|
lastUpdate := time.Now().UnixNano()
|
|
|
|
for {
|
|
if err := a.hci.poll(); err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case a.hci.advData.reported:
|
|
adf := AdvertisementFields{}
|
|
if a.hci.advData.eirLength > 31 {
|
|
if _debug {
|
|
println("eirLength too long")
|
|
}
|
|
|
|
a.hci.clearAdvData()
|
|
continue
|
|
}
|
|
|
|
for i := 0; i < int(a.hci.advData.eirLength); {
|
|
l, t := int(a.hci.advData.eirData[i]), a.hci.advData.eirData[i+1]
|
|
if l < 1 {
|
|
break
|
|
}
|
|
|
|
switch t {
|
|
case 0x02, 0x03:
|
|
// 16-bit Service Class UUID
|
|
case 0x06, 0x07:
|
|
// 128-bit Service Class UUID
|
|
case 0x08, 0x09:
|
|
if _debug {
|
|
println("local name", string(a.hci.advData.eirData[i+2:i+1+l]))
|
|
}
|
|
|
|
adf.LocalName = string(a.hci.advData.eirData[i+2 : i+1+l])
|
|
case 0xFF:
|
|
// Manufacturer Specific Data
|
|
}
|
|
|
|
i += l + 1
|
|
}
|
|
|
|
random := a.hci.advData.peerBdaddrType == 0x01
|
|
|
|
callback(a, ScanResult{
|
|
Address: Address{
|
|
MACAddress{
|
|
MAC: makeAddress(a.hci.advData.peerBdaddr),
|
|
isRandom: random,
|
|
},
|
|
},
|
|
RSSI: int16(a.hci.advData.rssi),
|
|
AdvertisementPayload: &advertisementFields{
|
|
AdvertisementFields: adf,
|
|
},
|
|
})
|
|
|
|
a.hci.clearAdvData()
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
default:
|
|
if !a.scanning {
|
|
return nil
|
|
}
|
|
|
|
if _debug && (time.Now().UnixNano()-lastUpdate)/int64(time.Second) > 1 {
|
|
println("still scanning...")
|
|
lastUpdate = time.Now().UnixNano()
|
|
}
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *Adapter) StopScan() error {
|
|
if !a.scanning {
|
|
return errNotScanning
|
|
}
|
|
|
|
if err := a.hci.leSetScanEnable(false, false); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.scanning = false
|
|
|
|
return nil
|
|
}
|
|
|
|
// Address contains a Bluetooth MAC address.
|
|
type Address struct {
|
|
MACAddress
|
|
}
|
|
|
|
// Connect starts a connection attempt to the given peripheral device address.
|
|
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
|
|
if _debug {
|
|
println("Connect")
|
|
}
|
|
|
|
random := uint8(0)
|
|
if address.isRandom {
|
|
random = 1
|
|
}
|
|
if err := a.hci.leCreateConn(0x0060, 0x0030, 0x00,
|
|
random, makeNINAAddress(address.MAC),
|
|
0x00, 0x0006, 0x000c, 0x0000, 0x00c8, 0x0004, 0x0006); err != nil {
|
|
return Device{}, err
|
|
}
|
|
|
|
// are we connected?
|
|
start := time.Now().UnixNano()
|
|
for {
|
|
if err := a.hci.poll(); err != nil {
|
|
return Device{}, err
|
|
}
|
|
|
|
if a.hci.connectData.connected {
|
|
defer a.hci.clearConnectData()
|
|
|
|
random := false
|
|
if address.isRandom {
|
|
random = true
|
|
}
|
|
|
|
d := Device{
|
|
Address: Address{
|
|
MACAddress{
|
|
MAC: makeAddress(a.hci.connectData.peerBdaddr),
|
|
isRandom: random},
|
|
},
|
|
deviceInternal: &deviceInternal{
|
|
adapter: a,
|
|
handle: a.hci.connectData.handle,
|
|
mtu: defaultMTU,
|
|
notificationRegistrations: make([]notificationRegistration, 0),
|
|
},
|
|
}
|
|
a.connectedDevices = append(a.connectedDevices, d)
|
|
|
|
return d, nil
|
|
|
|
} else {
|
|
// check for timeout
|
|
if (time.Now().UnixNano()-start)/int64(time.Second) > 5 {
|
|
break
|
|
}
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// cancel connection attempt that failed
|
|
if err := a.hci.leCancelConn(); err != nil {
|
|
return Device{}, err
|
|
}
|
|
|
|
return Device{}, ErrConnect
|
|
}
|
|
|
|
type notificationRegistration struct {
|
|
handle uint16
|
|
callback func([]byte)
|
|
}
|
|
|
|
// Device is a connection to a remote peripheral.
|
|
type Device struct {
|
|
Address Address
|
|
*deviceInternal
|
|
}
|
|
|
|
type deviceInternal struct {
|
|
adapter *Adapter
|
|
handle uint16
|
|
mtu uint16
|
|
|
|
notificationRegistrations []notificationRegistration
|
|
}
|
|
|
|
// Disconnect from the BLE device.
|
|
func (d Device) Disconnect() error {
|
|
if _debug {
|
|
println("Disconnect")
|
|
}
|
|
if err := d.adapter.hci.disconnect(d.handle); err != nil {
|
|
return err
|
|
}
|
|
|
|
d.adapter.connectedDevices = []Device{}
|
|
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 {
|
|
return &n
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d Device) addNotificationRegistration(handle uint16, callback func([]byte)) {
|
|
d.notificationRegistrations = append(d.notificationRegistrations,
|
|
notificationRegistration{
|
|
handle: handle,
|
|
callback: callback,
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|