//go:build ninafw package bluetooth import "errors" var ( errNotYetImplemented = errors.New("bluetooth: not yet implemented") errNoWrite = errors.New("bluetooth: write not permitted") errNoWriteWithoutResponse = errors.New("bluetooth: write without response not permitted") errWriteFailed = errors.New("bluetooth: write failed") errNoRead = errors.New("bluetooth: read not permitted") errReadFailed = errors.New("bluetooth: read failed") errNoNotify = errors.New("bluetooth: notify/indicate not permitted") errEnableNotificationsFailed = errors.New("bluetooth: enable notifications failed") errServiceNotFound = errors.New("bluetooth: service not found") errCharacteristicNotFound = errors.New("bluetooth: characteristic not found") ) const ( maxDefaultServicesToDiscover = 6 maxDefaultCharacteristicsToDiscover = 8 ) const ( charPropertyBroadcast = 0x01 charPropertyRead = 0x02 charPropertyWriteWithoutResponse = 0x04 charPropertyWrite = 0x08 charPropertyNotify = 0x10 charPropertyIndicate = 0x20 ) // DeviceService is a BLE service on a connected peripheral device. type DeviceService struct { uuid UUID device *Device startHandle, endHandle uint16 } // UUID returns the UUID for this DeviceService. func (s *DeviceService) UUID() UUID { return s.uuid } // DiscoverServices starts a service discovery procedure. Pass a list of service // UUIDs you are interested in to this function. Either a slice of all services // is returned (of the same length as the requested UUIDs and in the same // order), or if some services could not be discovered an error is returned. // // Passing a nil slice of UUIDs will return a complete list of // services. func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { if _debug { println("DiscoverServices") } services := make([]DeviceService, 0, maxDefaultServicesToDiscover) foundServices := make(map[UUID]DeviceService) startHandle := uint16(0x0001) endHandle := uint16(0xffff) for endHandle == uint16(0xffff) { err := d.adapter.att.readByGroupReq(d.handle, startHandle, endHandle, gattServiceUUID) if err != nil { return nil, err } if _debug { println("found d.adapter.att.services", len(d.adapter.att.services)) } if len(d.adapter.att.services) == 0 { break } for _, rawService := range d.adapter.att.services { if len(uuids) == 0 || rawService.uuid.isIn(uuids) { foundServices[rawService.uuid] = DeviceService{ device: d, uuid: rawService.uuid, startHandle: rawService.startHandle, endHandle: rawService.endHandle, } } startHandle = rawService.endHandle + 1 if startHandle == 0x0000 { endHandle = 0x0000 } } // reset raw services d.adapter.att.services = []rawService{} } switch { case len(uuids) > 0: // put into correct order for _, uuid := range uuids { s, ok := foundServices[uuid] if !ok { return nil, errServiceNotFound } services = append(services, s) } default: for _, s := range foundServices { services = append(services, s) } } return services, nil } // DeviceCharacteristic is a BLE characteristic on a connected peripheral // device. type DeviceCharacteristic struct { uuid UUID service *DeviceService permissions CharacteristicPermissions handle uint16 properties uint8 callback func(buf []byte) } // UUID returns the UUID for this DeviceCharacteristic. func (c *DeviceCharacteristic) UUID() UUID { return c.uuid } // DiscoverCharacteristics discovers characteristics in this service. Pass a // list of characteristic UUIDs you are interested in to this function. Either a // list of all requested services is returned, or if some services could not be // discovered an error is returned. If there is no error, the characteristics // slice has the same length as the UUID slice with characteristics in the same // order in the slice as in the requested UUID list. // // Passing a nil slice of UUIDs will return a complete // list of characteristics. func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) { if _debug { println("DiscoverCharacteristics") } characteristics := make([]DeviceCharacteristic, 0, maxDefaultCharacteristicsToDiscover) foundCharacteristics := make(map[UUID]DeviceCharacteristic) startHandle := s.startHandle endHandle := s.endHandle for startHandle < endHandle { err := s.device.adapter.att.readByTypeReq(s.device.handle, startHandle, endHandle, gattCharacteristicUUID) switch { case err == ErrATTOp && s.device.adapter.att.lastErrorOpcode == attOpReadByTypeReq && s.device.adapter.att.lastErrorCode == attErrorAttrNotFound: // no characteristics found break case err != nil: return nil, err } if _debug { println("found s.device.adapter.att.characteristics", len(s.device.adapter.att.characteristics)) } if len(s.device.adapter.att.characteristics) == 0 { break } for _, rawCharacteristic := range s.device.adapter.att.characteristics { if len(uuids) == 0 || rawCharacteristic.uuid.isIn(uuids) { dc := DeviceCharacteristic{ service: s, uuid: rawCharacteristic.uuid, handle: rawCharacteristic.valueHandle, properties: rawCharacteristic.properties, permissions: CharacteristicPermissions(rawCharacteristic.properties), } foundCharacteristics[rawCharacteristic.uuid] = dc } startHandle = rawCharacteristic.valueHandle + 1 } // reset raw characteristics s.device.adapter.att.characteristics = []rawCharacteristic{} } switch { case len(uuids) > 0: // put into correct order for _, uuid := range uuids { c, ok := foundCharacteristics[uuid] if !ok { return nil, errCharacteristicNotFound } characteristics = append(characteristics, c) } default: for _, c := range foundCharacteristics { characteristics = append(characteristics, c) } } return characteristics, nil } // WriteWithoutResponse replaces the characteristic value with a new value. The // call will return before all data has been written. A limited number of such // writes can be in flight at any given time. This call is also known as a // "write command" (as opposed to a write request). func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) { if !c.permissions.WriteWithoutResponse() { return 0, errNoWriteWithoutResponse } err = c.service.device.adapter.att.writeCmd(c.service.device.handle, c.handle, p) if err != nil { return 0, err } return len(p), nil } // EnableNotifications enables notifications in the Client Characteristic // Configuration Descriptor (CCCD). This means that most peripherals will send a // notification with a new value every time the value of the characteristic // changes. // // Users may call EnableNotifications with a nil callback to disable notifications. func (c *DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error { if !c.permissions.Notify() { return errNoNotify } switch { case callback == nil: // disable notifications if _debug { println("disabling notifications") } err := c.service.device.adapter.att.writeReq(c.service.device.handle, c.handle+1, []byte{0x00, 0x00}) if err != nil { return err } default: // enable notifications if _debug { println("enabling notifications") } err := c.service.device.adapter.att.writeReq(c.service.device.handle, c.handle+1, []byte{0x01, 0x00}) if err != nil { return err } } c.callback = callback c.service.device.startNotifications() c.service.device.addNotificationRegistration(c.handle, c.callback) return nil } // GetMTU returns the MTU for the characteristic. func (c DeviceCharacteristic) GetMTU() (uint16, error) { return 0, errNotYetImplemented } // Read reads the current characteristic value. func (c *DeviceCharacteristic) Read(data []byte) (int, error) { if !c.permissions.Read() { return 0, errNoRead } err := c.service.device.adapter.att.readReq(c.service.device.handle, c.handle) if err != nil { return 0, err } if len(c.service.device.adapter.att.value) == 0 { return 0, errReadFailed } copy(data, c.service.device.adapter.att.value) return len(c.service.device.adapter.att.value), nil }