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:
parent
b278e2b932
commit
d77521461d
7 changed files with 374 additions and 343 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
318
gap_linux.go
318
gap_linux.go
|
@ -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
|
||||
}
|
||||
|
|
128
gattc_linux.go
128
gattc_linux.go
|
@ -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
|
||||
}
|
||||
|
|
191
gatts_linux.go
191
gatts_linux.go
|
@ -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
2
go.mod
|
@ -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
36
go.sum
|
@ -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=
|
||||
|
|
Loading…
Reference in a new issue