linux: rewrite everything to use DBus directly

This is a big rewrite to use DBus calls directly instead of going
through go-bluetooth first.

This is a big change, but I believe it is an improvement. While the
go-bluetooth works for many cases, it's a layer in between that I
believe hurts more than it helps. Without it, we can just program
directly against the BlueZ D-Bus API. The end result is about 10% more
code.

With this rewrite, I fixed the following issues:

  * All MapToStruct warnings are gone, like in
    https://github.com/tinygo-org/bluetooth/issues/193.
  * Advertisements can be restarted after they were stopped. Previously
    this resulted in a panic.
  * Looking at the source code of go-bluetooth, it appears that it
    includes devices from a different Bluetooth adapter than the one
    that's currently scanning. This is fixed with the rewrite.
  * Fix a bug in Adapter.AddService where it would only allow adding a
    single service. Multiple services can now be added.
    This was actually the motivating bug that led me down to rewrite the
    whole thing because I couldn't figure out where the bug was in
    go-bluetooth (it's many layers deep).
  * The `WriteEvent` callback in a characteristic now also gets the
    'offset' parameter which wasn't provided by go-bluetooth.

This rewrite also avoids go-bluetooth specific workarounds like
https://github.com/tinygo-org/bluetooth/pull/74 and
https://github.com/tinygo-org/bluetooth/pull/121.

I have tested all examples in the smoketest-linux Makefile target. They
all still work with this rewrite.
This commit is contained in:
Ayke van Laethem 2024-01-02 12:51:05 +01:00 committed by Ron Evans
parent b278e2b932
commit d77521461d
7 changed files with 374 additions and 343 deletions

View file

@ -106,7 +106,7 @@ func must(action string, err error) {
## Linux
Go Bluetooth support for Linux uses [BlueZ](http://www.bluez.org/) via the [D-Bus](https://en.wikipedia.org/wiki/D-Bus) interface thanks to the https://github.com/muka/go-bluetooth package. This should work with most distros that support BlueZ such as Ubuntu, Debian, Fedora, and Arch Linux, among others.
Go Bluetooth support for Linux uses [BlueZ](http://www.bluez.org/) via the [D-Bus](https://en.wikipedia.org/wiki/D-Bus) interface. This should work with most distros that support BlueZ such as Ubuntu, Debian, Fedora, and Arch Linux, among others.
Linux can be used both as a BLE Central or as a BLE Peripheral.

View file

@ -7,15 +7,20 @@ package bluetooth
import (
"errors"
"fmt"
"github.com/muka/go-bluetooth/api"
"github.com/muka/go-bluetooth/bluez/profile/adapter"
"github.com/godbus/dbus/v5"
)
const defaultAdapter = "hci0"
type Adapter struct {
adapter *adapter.Adapter1
id string
cancelChan chan struct{}
scanCancelChan chan struct{}
bus *dbus.Conn
bluez dbus.BusObject // object at /
adapter dbus.BusObject // object at /org/bluez/hciX
address string
defaultAdvertisement *Advertisement
connectHandler func(device Address, connected bool)
@ -26,29 +31,38 @@ 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) {
return
},
}
// Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() (err error) {
if a.id == "" {
a.adapter, err = api.GetDefaultAdapter()
if err != nil {
return
}
a.id, err = a.adapter.GetAdapterID()
bus, err := dbus.SystemBus()
if err != nil {
return err
}
a.bus = bus
a.bluez = a.bus.Object("org.bluez", dbus.ObjectPath("/"))
a.adapter = a.bus.Object("org.bluez", dbus.ObjectPath("/org/bluez/"+a.id))
addr, err := a.adapter.GetProperty("org.bluez.Adapter1.Address")
if err != nil {
if err, ok := err.(dbus.Error); ok && err.Name == "org.freedesktop.DBus.Error.UnknownObject" {
return fmt.Errorf("bluetooth: adapter %s does not exist", a.adapter.Path())
}
return fmt.Errorf("could not activate BlueZ adapter: %w", err)
}
addr.Store(&a.address)
return nil
}
func (a *Adapter) Address() (MACAddress, error) {
if a.adapter == nil {
if a.address == "" {
return MACAddress{}, errors.New("adapter not enabled")
}
mac, err := ParseMAC(a.adapter.Properties.Address)
mac, err := ParseMAC(a.address)
if err != nil {
return MACAddress{}, err
}

View file

@ -3,18 +3,20 @@
package bluetooth
import (
"context"
"errors"
"fmt"
"strings"
"sync/atomic"
"github.com/godbus/dbus/v5"
"github.com/muka/go-bluetooth/api"
"github.com/muka/go-bluetooth/bluez"
"github.com/muka/go-bluetooth/bluez/profile/advertising"
"github.com/muka/go-bluetooth/bluez/profile/device"
"github.com/godbus/dbus/v5/prop"
)
var errAdvertisementNotStarted = errors.New("bluetooth: stop advertisement that was not started")
var errAdvertisementAlreadyStarted = errors.New("bluetooth: start advertisement that was already started")
// Unique ID per advertisement (to generate a unique object path).
var advertisementID uint64
// Address contains a Bluetooth MAC address.
type Address struct {
@ -23,10 +25,9 @@ type Address struct {
// Advertisement encapsulates a single advertisement instance.
type Advertisement struct {
adapter *Adapter
advertisement *api.Advertisement
properties *advertising.LEAdvertisement1Properties
cancel func()
adapter *Adapter
properties *prop.Properties
path dbus.ObjectPath
}
// DefaultAdvertisement returns the default advertisement instance but does not
@ -44,42 +45,70 @@ func (a *Adapter) DefaultAdvertisement() *Advertisement {
//
// On Linux with BlueZ, it is not possible to set the advertisement interval.
func (a *Advertisement) Configure(options AdvertisementOptions) error {
if a.advertisement != nil {
if a.properties != nil {
panic("todo: configure advertisement a second time")
}
a.properties = &advertising.LEAdvertisement1Properties{
Type: advertising.AdvertisementTypeBroadcast,
Timeout: 1<<16 - 1,
LocalName: options.LocalName,
ManufacturerData: options.ManufacturerData,
}
var serviceUUIDs []string
for _, uuid := range options.ServiceUUIDs {
a.properties.ServiceUUIDs = append(a.properties.ServiceUUIDs, uuid.String())
serviceUUIDs = append(serviceUUIDs, uuid.String())
}
// Build an org.bluez.LEAdvertisement1 object, to be exported over DBus.
id := atomic.AddUint64(&advertisementID, 1)
a.path = dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/advertisement%d", id))
propsSpec := map[string]map[string]*prop.Prop{
"org.bluez.LEAdvertisement1": {
"Type": {Value: "broadcast"},
"ServiceUUIDs": {Value: serviceUUIDs},
"ManufacturerData": {Value: options.ManufacturerData},
"LocalName": {Value: options.LocalName},
// The documentation states:
// > Timeout of the advertisement in seconds. This defines the
// > lifetime of the advertisement.
// however, the value 0 also works, and presumably means "no
// timeout".
"Timeout": {Value: uint16(0)},
// TODO: MinInterval and MaxInterval (experimental as of BlueZ 5.71)
},
}
props, err := prop.Export(a.adapter.bus, a.path, propsSpec)
if err != nil {
return err
}
a.properties = props
return nil
}
// Start advertisement. May only be called after it has been configured.
func (a *Advertisement) Start() error {
if a.advertisement != nil {
panic("todo: start advertisement a second time")
}
cancel, err := api.ExposeAdvertisement(a.adapter.id, a.properties, uint32(a.properties.Timeout))
// Register our advertisement object to start advertising.
err := a.adapter.adapter.Call("org.bluez.LEAdvertisingManager1.RegisterAdvertisement", 0, a.path, map[string]interface{}{}).Err
if err != nil {
return err
if err, ok := err.(dbus.Error); ok && err.Name == "org.bluez.Error.AlreadyExists" {
return errAdvertisementAlreadyStarted
}
return fmt.Errorf("bluetooth: could not start advertisement: %w", err)
}
// Make us discoverable.
err = a.adapter.adapter.SetProperty("org.bluez.Adapter1.Discoverable", dbus.MakeVariant(true))
if err != nil {
return fmt.Errorf("bluetooth: could not start advertisement: %w", err)
}
a.cancel = cancel
return nil
}
// Stop advertisement. May only be called after it has been started.
func (a *Advertisement) Stop() error {
if a.cancel == nil {
return errAdvertisementNotStarted
err := a.adapter.adapter.Call("org.bluez.LEAdvertisingManager1.UnregisterAdvertisement", 0, a.path).Err
if err != nil {
if err, ok := err.(dbus.Error); ok && err.Name == "org.bluez.Error.DoesNotExist" {
return errAdvertisementNotStarted
}
return fmt.Errorf("bluetooth: could not stop advertisement: %w", err)
}
a.cancel()
return nil
}
@ -92,7 +121,7 @@ func (a *Advertisement) Stop() error {
// possible some events are missed and perhaps even possible that some events
// are duplicated.
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
if a.cancelChan != nil {
if a.scanCancelChan != nil {
return errScanning
}
@ -100,58 +129,61 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
// Detecting whether the scan is stopped can be done by doing a non-blocking
// read from it. If it succeeds, the scan is stopped.
cancelChan := make(chan struct{})
a.cancelChan = cancelChan
a.scanCancelChan = cancelChan
// This appears to be necessary to receive any BLE discovery results at all.
defer a.adapter.SetDiscoveryFilter(nil)
err := a.adapter.SetDiscoveryFilter(map[string]interface{}{
defer a.adapter.Call("org.bluez.Adapter1.SetDiscoveryFilter", 0)
err := a.adapter.Call("org.bluez.Adapter1.SetDiscoveryFilter", 0, map[string]interface{}{
"Transport": "le",
})
if err != nil {
return err
}
bus, err := dbus.SystemBus()
}).Err
if err != nil {
return err
}
signal := make(chan *dbus.Signal)
bus.Signal(signal)
defer bus.RemoveSignal(signal)
a.bus.Signal(signal)
defer a.bus.RemoveSignal(signal)
propertiesChangedMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties")}
bus.AddMatchSignal(propertiesChangedMatchOptions...)
defer bus.RemoveMatchSignal(propertiesChangedMatchOptions...)
a.bus.AddMatchSignal(propertiesChangedMatchOptions...)
defer a.bus.RemoveMatchSignal(propertiesChangedMatchOptions...)
newObjectMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.ObjectManager")}
bus.AddMatchSignal(newObjectMatchOptions...)
defer bus.RemoveMatchSignal(newObjectMatchOptions...)
a.bus.AddMatchSignal(newObjectMatchOptions...)
defer a.bus.RemoveMatchSignal(newObjectMatchOptions...)
// Go through all connected devices and present the connected devices as
// scan results. Also save the properties so that the full list of
// properties is known on a PropertiesChanged signal. We can't present the
// list of cached devices as scan results as devices may be cached for a
// long time, long after they have moved out of range.
deviceList, err := a.adapter.GetDevices()
var deviceList map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err = a.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&deviceList)
if err != nil {
return err
}
devices := make(map[dbus.ObjectPath]*device.Device1Properties)
for _, dev := range deviceList {
if dev.Properties.Connected {
callback(a, makeScanResult(dev.Properties))
devices := make(map[dbus.ObjectPath]map[string]dbus.Variant)
for path, v := range deviceList {
device, ok := v["org.bluez.Device1"]
if !ok {
continue // not a device
}
if !strings.HasPrefix(string(path), string(a.adapter.Path())) {
continue // not part of our adapter
}
if device["Connected"].Value().(bool) {
callback(a, makeScanResult(device))
select {
case <-cancelChan:
return nil
default:
}
}
devices[dev.Path()] = dev.Properties
devices[path] = device
}
// Instruct BlueZ to start discovering.
err = a.adapter.StartDiscovery()
err = a.adapter.Call("org.bluez.Adapter1.StartDiscovery", 0).Err
if err != nil {
return err
}
@ -163,8 +195,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
// StopScan is called).
select {
case <-cancelChan:
a.adapter.StopDiscovery()
return nil
return a.adapter.Call("org.bluez.Adapter1.StopDiscovery", 0).Err
default:
}
@ -180,35 +211,24 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
if !ok {
continue
}
var props *device.Device1Properties
props, _ = props.FromDBusMap(rawprops)
devices[objectPath] = props
callback(a, makeScanResult(props))
devices[objectPath] = rawprops
callback(a, makeScanResult(rawprops))
case "org.freedesktop.DBus.Properties.PropertiesChanged":
interfaceName := sig.Body[0].(string)
if interfaceName != "org.bluez.Device1" {
continue
}
changes := sig.Body[1].(map[string]dbus.Variant)
props := devices[sig.Path]
for field, val := range changes {
switch field {
case "RSSI":
props.RSSI = val.Value().(int16)
case "Name":
props.Name = val.Value().(string)
case "UUIDs":
props.UUIDs = val.Value().([]string)
case "ManufacturerData":
// work around for https://github.com/muka/go-bluetooth/issues/163
mData := make(map[uint16]interface{})
for k, v := range val.Value().(map[uint16]dbus.Variant) {
mData[k] = v.Value().(interface{})
}
props.ManufacturerData = mData
}
device, ok := devices[sig.Path]
if !ok {
// This shouldn't happen, but protect against it just in
// case.
continue
}
callback(a, makeScanResult(props))
for k, v := range changes {
device[k] = v
}
callback(a, makeScanResult(device))
}
case <-cancelChan:
continue
@ -222,49 +242,49 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
// callback to stop the current scan. If no scan is in progress, an error will
// be returned.
func (a *Adapter) StopScan() error {
if a.cancelChan == nil {
if a.scanCancelChan == nil {
return errNotScanning
}
close(a.cancelChan)
a.cancelChan = nil
close(a.scanCancelChan)
a.scanCancelChan = nil
return nil
}
// makeScanResult creates a ScanResult from a Device1 object.
func makeScanResult(props *device.Device1Properties) ScanResult {
// makeScanResult creates a ScanResult from a raw DBus device.
func makeScanResult(props map[string]dbus.Variant) ScanResult {
// Assume the Address property is well-formed.
addr, _ := ParseMAC(props.Address)
addr, _ := ParseMAC(props["Address"].Value().(string))
// Create a list of UUIDs.
var serviceUUIDs []UUID
for _, uuid := range props.UUIDs {
for _, uuid := range props["UUIDs"].Value().([]string) {
// Assume the UUID is well-formed.
parsedUUID, _ := ParseUUID(uuid)
serviceUUIDs = append(serviceUUIDs, parsedUUID)
}
a := Address{MACAddress{MAC: addr}}
a.SetRandom(props.AddressType == "random")
a.SetRandom(props["AddressType"].Value().(string) == "random")
mData := make(map[uint16][]byte)
for k, v := range props.ManufacturerData {
// can be either variant or just byte value
switch val := v.(type) {
case dbus.Variant:
mData[k] = val.Value().([]byte)
case []byte:
mData[k] = val
manufacturerData := make(map[uint16][]byte)
if mdata, ok := props["ManufacturerData"].Value().(map[uint16]dbus.Variant); ok {
for k, v := range mdata {
manufacturerData[k] = v.Value().([]byte)
}
}
// Get optional properties.
localName, _ := props["Name"].Value().(string)
rssi, _ := props["RSSI"].Value().(int16)
return ScanResult{
RSSI: props.RSSI,
RSSI: rssi,
Address: a,
AdvertisementPayload: &advertisementFields{
AdvertisementFields{
LocalName: props.Name,
LocalName: localName,
ServiceUUIDs: serviceUUIDs,
ManufacturerData: mData,
ManufacturerData: manufacturerData,
},
},
}
@ -272,12 +292,9 @@ func makeScanResult(props *device.Device1Properties) ScanResult {
// Device is a connection to a remote peripheral.
type Device struct {
device *device.Device1 // bluez device interface
ctx context.Context // context for our event watcher, canceled on disconnect event
cancel context.CancelFunc // cancel function to halt our event watcher context
propchanged chan *bluez.PropertyChanged // channel that device property changes will show up on
adapter *Adapter // the adapter that was used to form this device connection
address Address // the 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.
@ -285,27 +302,57 @@ type Device struct {
// On Linux and Windows, the IsRandom part of the address is ignored.
func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, error) {
devicePath := dbus.ObjectPath(string(a.adapter.Path()) + "/dev_" + strings.Replace(address.MAC.String(), ":", "_", -1))
dev, err := device.NewDevice1(devicePath)
device := &Device{
device: a.bus.Object("org.bluez", devicePath),
adapter: a,
address: address,
}
// Already start watching for property changes. We do this before reading
// the Connected property below to avoid a race condition: if the device
// were connected between the two calls the signal wouldn't be picked up.
signal := make(chan *dbus.Signal)
a.bus.Signal(signal)
defer a.bus.RemoveSignal(signal)
propertiesChangedMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties")}
a.bus.AddMatchSignal(propertiesChangedMatchOptions...)
defer a.bus.RemoveMatchSignal(propertiesChangedMatchOptions...)
// Read whether this device is already connected.
connected, err := device.device.GetProperty("org.bluez.Device1.Connected")
if err != nil {
return nil, err
}
device := &Device{
device: dev,
adapter: a,
address: address,
}
device.ctx, device.cancel = context.WithCancel(context.Background())
device.watchForConnect() // Set this up before we trigger a connection so we can capture the connect event
if !dev.Properties.Connected {
// Not yet connected, so do it now.
// The properties have just been read so this is fresh data.
err := dev.Connect()
// Connect to the device, if not already connected.
if !connected.Value().(bool) {
// Start connecting (async).
err := device.device.Call("org.bluez.Device1.Connect", 0).Err
if err != nil {
device.cancel() // cancel our watcher routine
return nil, err
return nil, fmt.Errorf("bluetooth: failed to connect: %w", err)
}
// Wait until the device has connected.
connectChan := make(chan struct{})
go func() {
for sig := range signal {
switch sig.Name {
case "org.freedesktop.DBus.Properties.PropertiesChanged":
interfaceName := sig.Body[0].(string)
if interfaceName != "org.bluez.Device1" {
continue
}
if sig.Path != device.device.Path() {
continue
}
changes := sig.Body[1].(map[string]dbus.Variant)
if connected, ok := changes["Connected"].Value().(bool); ok && connected {
close(connectChan)
}
}
}
}()
<-connectChan
}
return device, nil
@ -316,48 +363,5 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er
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.Disconnect()
}
// watchForConnect watches for a signal from the bluez device interface that indicates a Connection/Disconnection.
//
// We can add extra signals to watch for here,
// see https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt, for a full list
func (d *Device) watchForConnect() error {
var err error
d.propchanged, err = d.device.WatchProperties()
if err != nil {
return err
}
go func() {
for {
select {
case changed := <-d.propchanged:
// we will receive a nil if bluez.UnwatchProperties(a, ch) is called, if so we can stop watching
if changed == nil {
d.cancel()
return
}
switch changed.Name {
case "Connected":
// Send off a notification indicating we have connected or disconnected
d.adapter.connectHandler(d.address, d.device.Properties.Connected)
if !d.device.Properties.Connected {
d.cancel()
return
}
}
continue
case <-d.ctx.Done():
return
}
}
}()
return nil
return d.device.Call("org.bluez.Device1.Disconnect", 0).Err
}

View file

@ -9,8 +9,6 @@ import (
"time"
"github.com/godbus/dbus/v5"
"github.com/muka/go-bluetooth/bluez"
"github.com/muka/go-bluetooth/bluez/profile/gatt"
)
var (
@ -24,8 +22,8 @@ type uuidWrapper = UUID
// DeviceService is a BLE service on a connected peripheral device.
type DeviceService struct {
uuidWrapper
service *gatt.GattService1
adapter *Adapter
servicePath string
}
// UUID returns the UUID for this DeviceService.
@ -47,14 +45,16 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
start := time.Now()
for {
resolved, err := d.device.GetServicesResolved()
resolved, err := d.device.GetProperty("org.bluez.Device1.ServicesResolved")
if err != nil {
return nil, err
}
if resolved {
if resolved.Value().(bool) {
break
}
// This is a terrible hack, but I couldn't find another way.
// TODO: actually there is, by waiting for a property change event of
// ServicesResolved.
time.Sleep(10 * time.Millisecond)
if time.Since(start) > 10*time.Second {
return nil, errors.New("timeout on DiscoverServices")
@ -62,16 +62,13 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
}
services := []DeviceService{}
uuidServices := make(map[string]string)
uuidServices := make(map[UUID]struct{})
servicesFound := 0
// Iterate through all objects managed by BlueZ, hoping to find the services
// we're looking for.
om, err := bluez.GetObjectManager()
if err != nil {
return nil, err
}
list, err := om.GetManagedObjects()
var list map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err := d.adapter.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&list)
if err != nil {
return nil, err
}
@ -84,19 +81,17 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
if !strings.HasPrefix(objectPath, string(d.device.Path())+"/service") {
continue
}
suffix := objectPath[len(d.device.Path()+"/"):]
if len(strings.Split(suffix, "/")) != 1 {
properties, ok := list[dbus.ObjectPath(objectPath)]["org.bluez.GattService1"]
if !ok {
continue
}
service, err := gatt.NewGattService1(dbus.ObjectPath(objectPath))
if err != nil {
return nil, err
}
serviceUUID, _ := ParseUUID(properties["UUID"].Value().(string))
if len(uuids) > 0 {
found := false
for _, uuid := range uuids {
if service.Properties.UUID == uuid.String() {
if uuid == serviceUUID {
// One of the services we're looking for.
found = true
break
@ -107,20 +102,21 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
}
}
if _, ok := uuidServices[service.Properties.UUID]; ok {
if _, ok := uuidServices[serviceUUID]; ok {
// There is more than one service with the same UUID?
// Don't overwrite it, to keep the servicesFound count correct.
continue
}
uuid, _ := ParseUUID(service.Properties.UUID)
ds := DeviceService{uuidWrapper: uuid,
service: service,
ds := DeviceService{
uuidWrapper: serviceUUID,
adapter: d.adapter,
servicePath: objectPath,
}
services = append(services, ds)
servicesFound++
uuidServices[service.Properties.UUID] = service.Properties.UUID
uuidServices[serviceUUID] = struct{}{}
}
if servicesFound < len(uuids) {
@ -134,9 +130,10 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
// device.
type DeviceCharacteristic struct {
uuidWrapper
characteristic *gatt.GattCharacteristic1
property chan *bluez.PropertyChanged // channel where notifications are reported
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
}
// UUID returns the UUID for this DeviceCharacteristic.
@ -163,11 +160,8 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
// Iterate through all objects managed by BlueZ, hoping to find the
// characteristic we're looking for.
om, err := bluez.GetObjectManager()
if err != nil {
return nil, err
}
list, err := om.GetManagedObjects()
var list map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err := s.adapter.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&list)
if err != nil {
return nil, err
}
@ -177,21 +171,18 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
}
sort.Strings(objects)
for _, objectPath := range objects {
if !strings.HasPrefix(objectPath, string(s.service.Path())+"/char") {
if !strings.HasPrefix(objectPath, s.servicePath+"/char") {
continue
}
suffix := objectPath[len(s.service.Path()+"/"):]
if len(strings.Split(suffix, "/")) != 1 {
properties, ok := list[dbus.ObjectPath(objectPath)]["org.bluez.GattCharacteristic1"]
if !ok {
continue
}
characteristic, err := gatt.NewGattCharacteristic1(dbus.ObjectPath(objectPath))
if err != nil {
return nil, err
}
cuuid, _ := ParseUUID(characteristic.Properties.UUID)
cuuid, _ := ParseUUID(properties["UUID"].Value().(string))
char := DeviceCharacteristic{
uuidWrapper: cuuid,
characteristic: characteristic,
adapter: s.adapter,
characteristic: s.adapter.bus.Object("org.bluez", dbus.ObjectPath(objectPath)),
}
if len(uuids) > 0 {
@ -231,7 +222,7 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
// writes can be in flight at any given time. This call is also known as a
// "write command" (as opposed to a write request).
func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) {
err = c.characteristic.WriteValue(p, nil)
err = c.characteristic.Call("org.bluez.GattCharacteristic1.WriteValue", 0, p, map[string]dbus.Variant(nil)).Err
if err != nil {
return 0, err
}
@ -251,25 +242,31 @@ func (c *DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) er
return errDupNotif
}
ch, err := c.characteristic.WatchProperties()
if err != nil {
return err
}
// 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)
err = c.characteristic.StartNotify()
err := c.characteristic.Call("org.bluez.GattCharacteristic1.StartNotify", 0).Err
if err != nil {
_ = c.characteristic.UnwatchProperties(ch)
return err
}
c.property = ch
go func() {
for update := range ch {
if update == nil {
continue
}
if update.Interface == "org.bluez.GattCharacteristic1" && update.Name == "Value" {
callback(update.Value.([]byte))
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)
}
}
}
}()
@ -281,26 +278,16 @@ func (c *DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) er
return nil
}
e1 := c.characteristic.StopNotify()
e2 := c.characteristic.UnwatchProperties(c.property)
err := c.adapter.bus.RemoveMatchSignal(c.propertiesChangedMatchOption)
c.adapter.bus.RemoveSignal(c.property)
c.property = nil
// FIXME(sbinet): use errors.Join(e1, e2)
if e1 != nil {
return e1
}
if e2 != nil {
return e2
}
return nil
return err
}
}
// GetMTU returns the MTU for the characteristic.
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
mtu, err := c.characteristic.GetProperty("MTU")
mtu, err := c.characteristic.GetProperty("org.bluez.GattCharacteristic1.MTU")
if err != nil {
return uint16(0), err
}
@ -310,7 +297,8 @@ func (c DeviceCharacteristic) GetMTU() (uint16, error) {
// Read reads the current characteristic value.
func (c *DeviceCharacteristic) Read(data []byte) (int, error) {
options := make(map[string]interface{})
result, err := c.characteristic.ReadValue(options)
var result []byte
err := c.characteristic.Call("org.bluez.GattCharacteristic1.ReadValue", 0, options).Store(&result)
if err != nil {
return 0, err
}

View file

@ -3,94 +3,154 @@
package bluetooth
import (
"github.com/muka/go-bluetooth/api/service"
"github.com/muka/go-bluetooth/bluez/profile/gatt"
"fmt"
"strconv"
"sync/atomic"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/prop"
)
// Unique ID per service (to generate a unique object path).
var serviceID uint64
// Characteristic is a single characteristic in a service. It has an UUID and a
// value.
type Characteristic struct {
handle *service.Char
char *bluezChar
permissions CharacteristicPermissions
}
// A small ObjectManager for a single service.
type objectManager struct {
objects map[dbus.ObjectPath]map[string]map[string]*prop.Prop
}
// This method implements org.freedesktop.DBus.ObjectManager.
func (om *objectManager) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, *dbus.Error) {
// Convert from a map with *prop.Prop keys, to a map with dbus.Variant keys.
objects := map[dbus.ObjectPath]map[string]map[string]dbus.Variant{}
for path, object := range om.objects {
obj := make(map[string]map[string]dbus.Variant)
objects[path] = obj
for iface, props := range object {
ifaceObj := make(map[string]dbus.Variant)
obj[iface] = ifaceObj
for k, v := range props {
ifaceObj[k] = dbus.MakeVariant(v.Value)
}
}
}
return objects, nil
}
// Object that implements org.bluez.GattCharacteristic1 to be exported over
// DBus. Here is the documentation:
// https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/org.bluez.GattCharacteristic.rst
type bluezChar struct {
props *prop.Properties
writeEvent func(client Connection, offset int, value []byte)
}
func (c *bluezChar) ReadValue(options map[string]dbus.Variant) ([]byte, *dbus.Error) {
// TODO: should we use the offset value? The BlueZ documentation doesn't
// clearly specify this. The go-bluetooth library doesn't, but I believe it
// should be respected.
value := c.props.GetMust("org.bluez.GattCharacteristic1", "Value").([]byte)
return value, nil
}
func (c *bluezChar) WriteValue(value []byte, options map[string]dbus.Variant) *dbus.Error {
if c.writeEvent != nil {
// BlueZ doesn't seem to tell who did the write, so pass 0 always as the
// connection ID.
client := Connection(0)
offset, _ := options["offset"].Value().(uint16)
c.writeEvent(client, int(offset), value)
}
return nil
}
// AddService creates a new service with the characteristics listed in the
// Service struct.
func (a *Adapter) AddService(s *Service) error {
app, err := service.NewApp(service.AppOptions{
AdapterID: a.id,
})
if err != nil {
return err
// Create a unique DBus path for this service.
id := atomic.AddUint64(&serviceID, 1)
path := dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/service%d", id))
// All objects that will be part of the ObjectManager.
objects := map[dbus.ObjectPath]map[string]map[string]*prop.Prop{}
// Define the service to be exported over DBus.
serviceSpec := map[string]map[string]*prop.Prop{
"org.bluez.GattService1": {
"UUID": {Value: s.UUID.String()},
"Primary": {Value: true},
},
}
objects[path] = serviceSpec
// disable magic uuid generation because we send through a fully formed UUID.
// muka/go-bluetooth does some magic so you can use short UUIDs and it'll auto
// expand them to the full 128 bit uuid.
// setting these flags disables that behavior.
app.Options.UUIDSuffix = ""
app.Options.UUID = ""
bluezService, err := app.NewService(s.UUID.String())
if err != nil {
return err
}
err = app.AddService(bluezService)
if err != nil {
return err
}
for _, char := range s.Characteristics {
// Create characteristic handle.
bluezChar, err := bluezService.NewChar(char.UUID.String())
if err != nil {
return err
}
// Set properties.
for i, char := range s.Characteristics {
// Calculate Flags field.
bluezCharFlags := []string{
gatt.FlagCharacteristicBroadcast, // bit 0
gatt.FlagCharacteristicRead, // bit 1
gatt.FlagCharacteristicWriteWithoutResponse, // bit 2
gatt.FlagCharacteristicWrite, // bit 3
gatt.FlagCharacteristicNotify, // bit 4
gatt.FlagCharacteristicIndicate, // bit 5
"broadcast", // bit 0
"read", // bit 1
"write-without-response", // bit 2
"write", // bit 3
"notify", // bit 4
"indicate", // bit 5
}
for i := uint(0); i < 5; i++ {
var flags []string
for i := 0; i < len(bluezCharFlags); i++ {
if (char.Flags>>i)&1 != 0 {
bluezChar.Properties.Flags = append(bluezChar.Properties.Flags, bluezCharFlags[i])
flags = append(flags, bluezCharFlags[i])
}
}
bluezChar.Properties.Value = char.Value
if char.Handle != nil {
char.Handle.handle = bluezChar
char.Handle.permissions = char.Flags
// Export the properties of this characteristic.
charPath := path + dbus.ObjectPath("/char"+strconv.Itoa(i))
propsSpec := map[string]map[string]*prop.Prop{
"org.bluez.GattCharacteristic1": {
"UUID": {Value: char.UUID.String()},
"Service": {Value: path},
"Flags": {Value: flags},
"Value": {Value: []byte("foobar"), Writable: true, Emit: prop.EmitTrue},
},
}
// Do a callback when the value changes.
if char.WriteEvent != nil {
callback := char.WriteEvent
bluezChar.OnWrite(func(c *service.Char, value []byte) ([]byte, error) {
// BlueZ doesn't seem to tell who did the write, so pass 0
// always.
// It also doesn't provide which part of the value was written,
// so pretend the entire characteristic was updated (which might
// not be the case).
callback(0, 0, value)
return nil, nil
})
}
// Add characteristic to the service, to activate it.
err = bluezService.AddChar(bluezChar)
objects[charPath] = propsSpec
props, err := prop.Export(a.bus, charPath, propsSpec)
if err != nil {
return err
}
// Export the methods of this characteristic.
obj := &bluezChar{
props: props,
writeEvent: char.WriteEvent,
}
err = a.bus.Export(obj, charPath, "org.bluez.GattCharacteristic1")
if err != nil {
return err
}
// Keep the object around for Characteristic.Write.
if char.Handle != nil {
char.Handle.permissions = char.Flags
char.Handle.char = obj
}
}
return app.Run()
// Export all objects that are part of our service.
om := &objectManager{
objects: objects,
}
err := a.bus.Export(om, path, "org.freedesktop.DBus.ObjectManager")
if err != nil {
return err
}
// Register our service.
return a.adapter.Call("org.bluez.GattManager1.RegisterApplication", 0, path, map[string]dbus.Variant(nil)).Err
}
// Write replaces the characteristic value with a new value.
@ -99,7 +159,10 @@ func (c *Characteristic) Write(p []byte) (n int, err error) {
return 0, nil // nothing to do
}
gattError := c.handle.WriteValue(p, nil)
if c.char.writeEvent != nil {
c.char.writeEvent(0, 0, p)
}
gattError := c.char.props.Set("org.bluez.GattCharacteristic1", "Value", dbus.MakeVariant(p))
if gattError != nil {
return 0, gattError
}

2
go.mod
View file

@ -5,7 +5,6 @@ go 1.18
require (
github.com/go-ole/go-ole v1.2.6
github.com/godbus/dbus/v5 v5.1.0
github.com/muka/go-bluetooth v0.0.0-20221213043340-85dc80edc4e1
github.com/saltosystems/winrt-go v0.0.0-20230921082907-2ab5b7d431e1
github.com/tinygo-org/cbgo v0.0.4
golang.org/x/crypto v0.12.0
@ -15,7 +14,6 @@ require (
)
require (
github.com/fatih/structs v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/sys v0.11.0 // indirect

36
go.sum
View file

@ -1,72 +1,36 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/muka/go-bluetooth v0.0.0-20221213043340-85dc80edc4e1 h1:BuVRHr4HHJbk1DHyWkArJ7E8J/VA8ncCr/VLnQFazBo=
github.com/muka/go-bluetooth v0.0.0-20221213043340-85dc80edc4e1/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/saltosystems/winrt-go v0.0.0-20230921082907-2ab5b7d431e1 h1:L2YoWezgwpAZ2SEKjXk6yLnwOkM3u7mXq/mKuJeEpFM=
github.com/saltosystems/winrt-go v0.0.0-20230921082907-2ab5b7d431e1/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6 h1:w18u47MirULgAl+bP0piUGu5VUZDs7TvXwHASEVXqHk=