2022-02-22 13:04:20 +03:00
|
|
|
//go:build !baremetal
|
2020-06-28 01:14:25 +03:00
|
|
|
|
|
|
|
package bluetooth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2023-05-04 22:33:45 +03:00
|
|
|
"sort"
|
2020-06-28 01:14:25 +03:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2023-05-04 22:33:45 +03:00
|
|
|
"github.com/godbus/dbus/v5"
|
2020-06-28 01:14:25 +03:00
|
|
|
)
|
|
|
|
|
2023-05-26 11:24:47 +03:00
|
|
|
var (
|
|
|
|
errDupNotif = errors.New("unclosed notifications")
|
|
|
|
)
|
|
|
|
|
2020-09-13 21:21:38 +03:00
|
|
|
// UUIDWrapper is a type alias for UUID so we ensure no conflicts with
|
|
|
|
// struct method of the same name.
|
|
|
|
type uuidWrapper = UUID
|
|
|
|
|
2020-06-28 01:14:25 +03:00
|
|
|
// DeviceService is a BLE service on a connected peripheral device.
|
|
|
|
type DeviceService struct {
|
2020-09-13 21:21:38 +03:00
|
|
|
uuidWrapper
|
2024-01-02 14:51:05 +03:00
|
|
|
adapter *Adapter
|
|
|
|
servicePath string
|
2020-06-28 01:14:25 +03:00
|
|
|
}
|
|
|
|
|
2020-09-13 21:21:38 +03:00
|
|
|
// UUID returns the UUID for this DeviceService.
|
|
|
|
func (s *DeviceService) UUID() UUID {
|
|
|
|
return s.uuidWrapper
|
|
|
|
}
|
|
|
|
|
2020-06-28 01:14:25 +03:00
|
|
|
// 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.
|
|
|
|
//
|
2020-09-02 21:44:17 +03:00
|
|
|
// Passing a nil slice of UUIDs will return a complete list of
|
2020-06-28 01:14:25 +03:00
|
|
|
// services.
|
|
|
|
//
|
|
|
|
// On Linux with BlueZ, this just waits for the ServicesResolved signal (if
|
|
|
|
// services haven't been resolved yet) and uses this list of cached services.
|
|
|
|
func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
|
2021-11-01 10:41:57 +03:00
|
|
|
start := time.Now()
|
|
|
|
|
2020-06-28 01:14:25 +03:00
|
|
|
for {
|
2024-01-02 14:51:05 +03:00
|
|
|
resolved, err := d.device.GetProperty("org.bluez.Device1.ServicesResolved")
|
2020-06-28 01:14:25 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-01-02 14:51:05 +03:00
|
|
|
if resolved.Value().(bool) {
|
2020-06-28 01:14:25 +03:00
|
|
|
break
|
|
|
|
}
|
|
|
|
// This is a terrible hack, but I couldn't find another way.
|
2024-01-02 14:51:05 +03:00
|
|
|
// TODO: actually there is, by waiting for a property change event of
|
|
|
|
// ServicesResolved.
|
2020-06-28 01:14:25 +03:00
|
|
|
time.Sleep(10 * time.Millisecond)
|
2021-11-01 10:41:57 +03:00
|
|
|
if time.Since(start) > 10*time.Second {
|
|
|
|
return nil, errors.New("timeout on DiscoverServices")
|
|
|
|
}
|
2020-06-28 01:14:25 +03:00
|
|
|
}
|
|
|
|
|
2020-09-02 21:44:17 +03:00
|
|
|
services := []DeviceService{}
|
2024-01-02 14:51:05 +03:00
|
|
|
uuidServices := make(map[UUID]struct{})
|
2020-06-28 01:14:25 +03:00
|
|
|
servicesFound := 0
|
|
|
|
|
|
|
|
// Iterate through all objects managed by BlueZ, hoping to find the services
|
|
|
|
// we're looking for.
|
2024-01-02 14:51:05 +03:00
|
|
|
var list map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
|
|
|
err := d.adapter.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&list)
|
2020-06-28 01:14:25 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-05-04 22:33:45 +03:00
|
|
|
objects := make([]string, 0, len(list))
|
2020-06-28 01:14:25 +03:00
|
|
|
for objectPath := range list {
|
2023-05-04 22:33:45 +03:00
|
|
|
objects = append(objects, string(objectPath))
|
|
|
|
}
|
|
|
|
sort.Strings(objects)
|
|
|
|
for _, objectPath := range objects {
|
|
|
|
if !strings.HasPrefix(objectPath, string(d.device.Path())+"/service") {
|
2020-06-28 01:14:25 +03:00
|
|
|
continue
|
|
|
|
}
|
2024-01-02 14:51:05 +03:00
|
|
|
properties, ok := list[dbus.ObjectPath(objectPath)]["org.bluez.GattService1"]
|
|
|
|
if !ok {
|
2020-06-28 01:14:25 +03:00
|
|
|
continue
|
|
|
|
}
|
2024-01-02 14:51:05 +03:00
|
|
|
|
|
|
|
serviceUUID, _ := ParseUUID(properties["UUID"].Value().(string))
|
2020-09-02 21:44:17 +03:00
|
|
|
|
|
|
|
if len(uuids) > 0 {
|
|
|
|
found := false
|
|
|
|
for _, uuid := range uuids {
|
2024-01-02 14:51:05 +03:00
|
|
|
if uuid == serviceUUID {
|
2020-09-02 21:44:17 +03:00
|
|
|
// One of the services we're looking for.
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
2020-06-28 01:14:25 +03:00
|
|
|
}
|
2020-09-02 21:44:17 +03:00
|
|
|
if !found {
|
2020-06-28 01:14:25 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
2020-09-02 21:44:17 +03:00
|
|
|
|
2024-01-02 14:51:05 +03:00
|
|
|
if _, ok := uuidServices[serviceUUID]; ok {
|
2020-09-02 21:44:17 +03:00
|
|
|
// There is more than one service with the same UUID?
|
|
|
|
// Don't overwrite it, to keep the servicesFound count correct.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-01-02 14:51:05 +03:00
|
|
|
ds := DeviceService{
|
|
|
|
uuidWrapper: serviceUUID,
|
|
|
|
adapter: d.adapter,
|
|
|
|
servicePath: objectPath,
|
2020-09-02 21:44:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
services = append(services, ds)
|
|
|
|
servicesFound++
|
2024-01-02 14:51:05 +03:00
|
|
|
uuidServices[serviceUUID] = struct{}{}
|
2020-06-28 01:14:25 +03:00
|
|
|
}
|
|
|
|
|
2020-09-16 08:19:00 +03:00
|
|
|
if servicesFound < len(uuids) {
|
2020-06-28 01:14:25 +03:00
|
|
|
return nil, errors.New("bluetooth: could not find some services")
|
|
|
|
}
|
|
|
|
|
|
|
|
return services, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeviceCharacteristic is a BLE characteristic on a connected peripheral
|
|
|
|
// device.
|
|
|
|
type DeviceCharacteristic struct {
|
2020-09-13 21:21:38 +03:00
|
|
|
uuidWrapper
|
2024-01-02 14:51:05 +03:00
|
|
|
adapter *Adapter
|
|
|
|
characteristic dbus.BusObject
|
|
|
|
property chan *dbus.Signal // channel where notifications are reported
|
|
|
|
propertiesChangedMatchOption dbus.MatchOption // the same value must be passed to RemoveMatchSignal
|
2020-06-28 01:14:25 +03:00
|
|
|
}
|
|
|
|
|
2020-09-13 21:21:38 +03:00
|
|
|
// UUID returns the UUID for this DeviceCharacteristic.
|
|
|
|
func (c *DeviceCharacteristic) UUID() UUID {
|
|
|
|
return c.uuidWrapper
|
|
|
|
}
|
|
|
|
|
2020-06-28 01:14:25 +03:00
|
|
|
// 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.
|
|
|
|
//
|
2020-09-02 21:44:17 +03:00
|
|
|
// Passing a nil slice of UUIDs will return a complete
|
2020-06-28 01:14:25 +03:00
|
|
|
// list of characteristics.
|
|
|
|
func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
|
2023-05-04 22:15:24 +03:00
|
|
|
var chars []DeviceCharacteristic
|
|
|
|
if len(uuids) > 0 {
|
|
|
|
// The caller wants to get a list of characteristics in a specific
|
|
|
|
// order.
|
|
|
|
chars = make([]DeviceCharacteristic, len(uuids))
|
|
|
|
}
|
2020-06-28 01:14:25 +03:00
|
|
|
|
|
|
|
// Iterate through all objects managed by BlueZ, hoping to find the
|
|
|
|
// characteristic we're looking for.
|
2024-01-02 14:51:05 +03:00
|
|
|
var list map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
|
|
|
err := s.adapter.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&list)
|
2020-06-28 01:14:25 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-05-04 22:33:45 +03:00
|
|
|
objects := make([]string, 0, len(list))
|
2020-06-28 01:14:25 +03:00
|
|
|
for objectPath := range list {
|
2023-05-04 22:33:45 +03:00
|
|
|
objects = append(objects, string(objectPath))
|
|
|
|
}
|
|
|
|
sort.Strings(objects)
|
|
|
|
for _, objectPath := range objects {
|
2024-01-02 14:51:05 +03:00
|
|
|
if !strings.HasPrefix(objectPath, s.servicePath+"/char") {
|
2020-06-28 01:14:25 +03:00
|
|
|
continue
|
|
|
|
}
|
2024-01-02 14:51:05 +03:00
|
|
|
properties, ok := list[dbus.ObjectPath(objectPath)]["org.bluez.GattCharacteristic1"]
|
|
|
|
if !ok {
|
2020-06-28 01:14:25 +03:00
|
|
|
continue
|
|
|
|
}
|
2024-01-02 14:51:05 +03:00
|
|
|
cuuid, _ := ParseUUID(properties["UUID"].Value().(string))
|
2023-05-04 22:15:24 +03:00
|
|
|
char := DeviceCharacteristic{
|
|
|
|
uuidWrapper: cuuid,
|
2024-01-02 14:51:05 +03:00
|
|
|
adapter: s.adapter,
|
|
|
|
characteristic: s.adapter.bus.Object("org.bluez", dbus.ObjectPath(objectPath)),
|
2023-05-04 22:15:24 +03:00
|
|
|
}
|
2020-09-02 21:44:17 +03:00
|
|
|
|
|
|
|
if len(uuids) > 0 {
|
2023-05-04 22:15:24 +03:00
|
|
|
// The caller wants to get a list of characteristics in a specific
|
|
|
|
// order. Check whether this is one of those.
|
|
|
|
for i, uuid := range uuids {
|
|
|
|
if chars[i] != (DeviceCharacteristic{}) {
|
|
|
|
// To support multiple identical characteristics, we need to
|
|
|
|
// ignore the characteristics that are already found. See:
|
|
|
|
// https://github.com/tinygo-org/bluetooth/issues/131
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if cuuid == uuid {
|
|
|
|
// one of the characteristics we're looking for.
|
|
|
|
chars[i] = char
|
2020-09-02 21:44:17 +03:00
|
|
|
break
|
|
|
|
}
|
2020-06-28 01:14:25 +03:00
|
|
|
}
|
2023-05-04 22:15:24 +03:00
|
|
|
} else {
|
|
|
|
// The caller wants to get all characteristics, in any order.
|
|
|
|
chars = append(chars, char)
|
2020-09-02 21:44:17 +03:00
|
|
|
}
|
2020-06-28 01:14:25 +03:00
|
|
|
}
|
|
|
|
|
2023-05-04 22:15:24 +03:00
|
|
|
// Check that we have found all characteristics.
|
|
|
|
for _, char := range chars {
|
|
|
|
if char == (DeviceCharacteristic{}) {
|
|
|
|
return nil, errors.New("bluetooth: could not find some characteristics")
|
|
|
|
}
|
2020-06-28 01:14:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return chars, 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) {
|
2024-01-02 14:51:05 +03:00
|
|
|
err = c.characteristic.Call("org.bluez.GattCharacteristic1.WriteValue", 0, p, map[string]dbus.Variant(nil)).Err
|
2020-06-28 01:14:25 +03:00
|
|
|
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.
|
2023-05-26 11:24:47 +03:00
|
|
|
//
|
|
|
|
// Users may call EnableNotifications with a nil callback to disable notifications.
|
|
|
|
func (c *DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error {
|
|
|
|
switch callback {
|
|
|
|
default:
|
|
|
|
if c.property != nil {
|
|
|
|
return errDupNotif
|
|
|
|
}
|
|
|
|
|
2024-01-02 14:51:05 +03:00
|
|
|
// Start watching for changes in the Value property.
|
|
|
|
c.property = make(chan *dbus.Signal)
|
|
|
|
c.adapter.bus.Signal(c.property)
|
|
|
|
c.propertiesChangedMatchOption = dbus.WithMatchInterface("org.freedesktop.DBus.Properties")
|
|
|
|
c.adapter.bus.AddMatchSignal(c.propertiesChangedMatchOption)
|
2023-05-26 11:24:47 +03:00
|
|
|
|
2024-01-02 14:51:05 +03:00
|
|
|
err := c.characteristic.Call("org.bluez.GattCharacteristic1.StartNotify", 0).Err
|
2023-05-26 11:24:47 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
2024-01-02 14:51:05 +03:00
|
|
|
for sig := range c.property {
|
|
|
|
if sig.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" {
|
|
|
|
interfaceName := sig.Body[0].(string)
|
|
|
|
if interfaceName != "org.bluez.GattCharacteristic1" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if sig.Path != c.characteristic.Path() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
changes := sig.Body[1].(map[string]dbus.Variant)
|
|
|
|
if value, ok := changes["Value"].Value().([]byte); ok {
|
|
|
|
callback(value)
|
|
|
|
}
|
2023-05-26 11:24:47 +03:00
|
|
|
}
|
2020-06-28 01:14:25 +03:00
|
|
|
}
|
2023-05-26 11:24:47 +03:00
|
|
|
}()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
case nil:
|
|
|
|
if c.property == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-01-02 14:51:05 +03:00
|
|
|
err := c.adapter.bus.RemoveMatchSignal(c.propertiesChangedMatchOption)
|
|
|
|
c.adapter.bus.RemoveSignal(c.property)
|
2023-05-26 11:24:47 +03:00
|
|
|
c.property = nil
|
2024-01-02 14:51:05 +03:00
|
|
|
return err
|
2023-05-26 11:24:47 +03:00
|
|
|
}
|
2020-06-28 01:14:25 +03:00
|
|
|
}
|
2020-10-22 20:04:47 +03:00
|
|
|
|
2022-05-12 12:49:03 +03:00
|
|
|
// GetMTU returns the MTU for the characteristic.
|
|
|
|
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
|
2024-01-02 14:51:05 +03:00
|
|
|
mtu, err := c.characteristic.GetProperty("org.bluez.GattCharacteristic1.MTU")
|
2022-05-12 12:49:03 +03:00
|
|
|
if err != nil {
|
|
|
|
return uint16(0), err
|
|
|
|
}
|
|
|
|
return mtu.Value().(uint16), nil
|
|
|
|
}
|
|
|
|
|
2020-10-22 20:04:47 +03:00
|
|
|
// Read reads the current characteristic value.
|
|
|
|
func (c *DeviceCharacteristic) Read(data []byte) (int, error) {
|
|
|
|
options := make(map[string]interface{})
|
2024-01-02 14:51:05 +03:00
|
|
|
var result []byte
|
|
|
|
err := c.characteristic.Call("org.bluez.GattCharacteristic1.ReadValue", 0, options).Store(&result)
|
2020-10-22 20:04:47 +03:00
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
copy(data, result)
|
|
|
|
return len(result), nil
|
|
|
|
}
|