diff --git a/.gitignore b/.gitignore index f8a9d563..410e6226 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ .tags1 /examples/bin/* +vendor +.idea \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 37dfd94c..d9b98257 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ os: - linux go: - - 1.7 - 1.8 - 1.9 - tip diff --git a/README.md b/README.md index d6d8792f..29a39d84 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ [![codebeat badge](https://codebeat.co/badges/ba9fae6e-77d2-4173-8587-36ac8756676b)](https://codebeat.co/projects/github-com-go-ble-ble-master) [![Build Status](https://travis-ci.org/go-ble/ble.svg?branch=master)](https://travis-ci.org/go-ble/ble) +**ble** is a Golang [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) package for Linux and Mac OS. - -**ble** is a [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) package for Linux and macOS. - +**Note:** The Mac OS portion is not being actively maintained. diff --git a/client.go b/client.go index b8e4f316..8b2214c8 100644 --- a/client.go +++ b/client.go @@ -66,4 +66,7 @@ type Client interface { // Disconnected returns a receiving channel, which is closed when the client disconnects. Disconnected() <-chan struct{} + + // Conn returns the client's current connection. + Conn() Conn } diff --git a/conn.go b/conn.go index 750b04a4..d260dca6 100644 --- a/conn.go +++ b/conn.go @@ -33,6 +33,9 @@ type Conn interface { // SetTxMTU sets the ATT_MTU which the remote device is capable of accepting. SetTxMTU(mtu int) + // ReadRSSI retrieves the current RSSI value of remote peripheral. [Vol 2, Part E, 7.5.4] + ReadRSSI() int + // Disconnected returns a receiving channel, which is closed when the connection disconnects. Disconnected() <-chan struct{} } diff --git a/darwin/adv.go b/darwin/adv.go index 146ecf27..a9ebd46c 100644 --- a/darwin/adv.go +++ b/darwin/adv.go @@ -2,50 +2,33 @@ package darwin import ( "github.com/go-ble/ble" - "github.com/raff/goble/xpc" ) type adv struct { - args xpc.Dict - ad xpc.Dict + localName string + rssi int + mfgData []byte + powerLevel int + connectable bool + svcUUIDs []ble.UUID + svcData []ble.ServiceData + peerUUID ble.Addr } func (a *adv) LocalName() string { - return a.ad.GetString("kCBAdvDataLocalName", a.args.GetString("kCBMsgArgName", "")) + return a.localName } func (a *adv) ManufacturerData() []byte { - return a.ad.GetBytes("kCBAdvDataManufacturerData", nil) + return a.mfgData } func (a *adv) ServiceData() []ble.ServiceData { - xSDs, ok := a.ad["kCBAdvDataServiceData"] - if !ok { - return nil - } - - xSD := xSDs.(xpc.Array) - var sd []ble.ServiceData - for i := 0; i < len(xSD); i += 2 { - sd = append( - sd, ble.ServiceData{ - UUID: ble.UUID(xSD[i].([]byte)), - Data: xSD[i+1].([]byte), - }) - } - return sd + return a.svcData } func (a *adv) Services() []ble.UUID { - xUUIDs, ok := a.ad["kCBAdvDataServiceUUIDs"] - if !ok { - return nil - } - var uuids []ble.UUID - for _, xUUID := range xUUIDs.(xpc.Array) { - uuids = append(uuids, ble.UUID(ble.Reverse(xUUID.([]byte)))) - } - return uuids + return a.svcUUIDs } func (a *adv) OverflowService() []ble.UUID { @@ -53,7 +36,7 @@ func (a *adv) OverflowService() []ble.UUID { } func (a *adv) TxPowerLevel() int { - return a.ad.GetInt("kCBAdvDataTxPowerLevel", 0) + return a.powerLevel } func (a *adv) SolicitedService() []ble.UUID { @@ -61,13 +44,13 @@ func (a *adv) SolicitedService() []ble.UUID { } func (a *adv) Connectable() bool { - return a.ad.GetInt("kCBAdvDataIsConnectable", 0) > 0 + return a.connectable } func (a *adv) RSSI() int { - return a.args.GetInt("kCBMsgArgRssi", 0) + return a.rssi } func (a *adv) Addr() ble.Addr { - return a.args.MustGetUUID("kCBMsgArgDeviceUUID") + return a.peerUUID } diff --git a/darwin/client.go b/darwin/client.go index 4934a600..ef2f979b 100644 --- a/darwin/client.go +++ b/darwin/client.go @@ -4,24 +4,40 @@ import ( "fmt" "github.com/go-ble/ble" - "github.com/raff/goble/xpc" + "github.com/JuulLabs-OSS/cbgo" ) // A Client is a GATT client. type Client struct { + cbgo.PeripheralDelegateBase + profile *ble.Profile + pc profCache name string + cm cbgo.CentralManager - id xpc.UUID + id ble.UUID conn *conn } // NewClient ... -func NewClient(c ble.Conn) (*Client, error) { - return &Client{ +func NewClient(cm cbgo.CentralManager, c ble.Conn) (*Client, error) { + as := c.RemoteAddr().String() + id, err := ble.Parse(as) + if err != nil { + return nil, fmt.Errorf("connection has invalid address: addr=%s", as) + } + + cln := &Client{ conn: c.(*conn), - id: xpc.MakeUUID(c.RemoteAddr().String()), - }, nil + pc: newProfCache(), + cm: cm, + id: id, + } + + cln.conn.prph.SetDelegate(cln) + + return cln, nil } // Addr returns UUID of the remote peripheral. @@ -68,24 +84,29 @@ func (cln *Client) DiscoverProfile(force bool) (*ble.Profile, error) { // DiscoverServices finds all the primary services on a server. [Vol 3, Part G, 4.4.1] // If filter is specified, only filtered services are returned. func (cln *Client) DiscoverServices(ss []ble.UUID) ([]*ble.Service, error) { - rsp, err := cln.conn.sendReq(cmdDiscoverServices, xpc.Dict{ - "kCBMsgArgDeviceUUID": cln.id, - "kCBMsgArgUUIDs": uuidSlice(ss), - }) - if err != nil { - return nil, err - } - if err := rsp.err(); err != nil { - return nil, err + ch := cln.conn.evl.svcsDiscovered.Listen() + defer cln.conn.evl.svcsDiscovered.Close() + + cbuuids := uuidsToCbgoUUIDs(ss) + cln.conn.prph.DiscoverServices(cbuuids) + + select { + case itf := <-ch: + if itf != nil { + return nil, itf.(error) + } + + case <-cln.Disconnected(): + return nil, fmt.Errorf("disconnected") } + svcs := []*ble.Service{} - for _, xss := range rsp.services() { - xs := msg(xss.(xpc.Dict)) - svcs = append(svcs, &ble.Service{ - UUID: ble.MustParse(xs.uuid()), - Handle: uint16(xs.serviceStartHandle()), - EndHandle: uint16(xs.serviceEndHandle()), - }) + for _, dsvc := range cln.conn.prph.Services() { + svc := &ble.Service{ + UUID: ble.UUID(dsvc.UUID()), + } + cln.pc.addSvc(svc, dsvc) + svcs = append(svcs, svc) } if cln.profile == nil { cln.profile = &ble.Profile{Services: svcs} @@ -96,44 +117,40 @@ func (cln *Client) DiscoverServices(ss []ble.UUID) ([]*ble.Service, error) { // DiscoverIncludedServices finds the included services of a service. [Vol 3, Part G, 4.5.1] // If filter is specified, only filtered services are returned. func (cln *Client) DiscoverIncludedServices(ss []ble.UUID, s *ble.Service) ([]*ble.Service, error) { - rsp, err := cln.conn.sendReq(cmdDiscoverIncludedServices, xpc.Dict{ - "kCBMsgArgDeviceUUID": cln.id, - "kCBMsgArgServiceStartHandle": s.Handle, - "kCBMsgArgServiceEndHandle": s.EndHandle, - "kCBMsgArgUUIDs": uuidSlice(ss), - }) - if err != nil { - return nil, err - } - if err := rsp.err(); err != nil { - return nil, err - } return nil, ble.ErrNotImplemented } // DiscoverCharacteristics finds all the characteristics within a service. [Vol 3, Part G, 4.6.1] // If filter is specified, only filtered characteristics are returned. func (cln *Client) DiscoverCharacteristics(cs []ble.UUID, s *ble.Service) ([]*ble.Characteristic, error) { - rsp, err := cln.conn.sendReq(cmdDiscoverCharacteristics, xpc.Dict{ - "kCBMsgArgDeviceUUID": cln.id, - "kCBMsgArgServiceStartHandle": s.Handle, - "kCBMsgArgServiceEndHandle": s.EndHandle, - "kCBMsgArgUUIDs": uuidSlice(cs), - }) + cbsvc, err := cln.pc.findCbSvc(s) if err != nil { return nil, err } - if err := rsp.err(); err != nil { - return nil, err + + ch := cln.conn.evl.chrsDiscovered.Listen() + defer cln.conn.evl.chrsDiscovered.Close() + + cbuuids := uuidsToCbgoUUIDs(cs) + cln.conn.prph.DiscoverCharacteristics(cbuuids, cbsvc) + + select { + case itf := <-ch: + if itf != nil { + return nil, itf.(error) + } + + case <-cln.Disconnected(): + return nil, fmt.Errorf("disconnected") } - for _, xcs := range rsp.characteristics() { - xc := msg(xcs.(xpc.Dict)) - s.Characteristics = append(s.Characteristics, &ble.Characteristic{ - UUID: ble.MustParse(xc.uuid()), - Property: ble.Property(xc.characteristicProperties()), - Handle: uint16(xc.characteristicHandle()), - ValueHandle: uint16(xc.characteristicValueHandle()), - }) + + for _, dchr := range cbsvc.Characteristics() { + chr := &ble.Characteristic{ + UUID: ble.UUID(dchr.UUID()), + Property: ble.Property(dchr.Properties()), + } + cln.pc.addChr(chr, dchr) + s.Characteristics = append(s.Characteristics, chr) } return s.Characteristics, nil } @@ -141,106 +158,177 @@ func (cln *Client) DiscoverCharacteristics(cs []ble.UUID, s *ble.Service) ([]*bl // DiscoverDescriptors finds all the descriptors within a characteristic. [Vol 3, Part G, 4.7.1] // If filter is specified, only filtered descriptors are returned. func (cln *Client) DiscoverDescriptors(ds []ble.UUID, c *ble.Characteristic) ([]*ble.Descriptor, error) { - rsp, err := cln.conn.sendReq(cmdDiscoverDescriptors, xpc.Dict{ - "kCBMsgArgDeviceUUID": cln.id, - "kCBMsgArgCharacteristicHandle": c.Handle, - "kCBMsgArgCharacteristicValueHandle": c.ValueHandle, - "kCBMsgArgUUIDs": uuidSlice(ds), - }) + cbchr, err := cln.pc.findCbChr(c) if err != nil { return nil, err } - if err := rsp.err(); err != nil { + + ch := cln.conn.evl.dscsDiscovered.Listen() + defer cln.conn.evl.dscsDiscovered.Close() + + cln.conn.prph.DiscoverDescriptors(cbchr) + if err != nil { return nil, err } - for _, xds := range rsp.descriptors() { - xd := msg(xds.(xpc.Dict)) - c.Descriptors = append(c.Descriptors, &ble.Descriptor{ - UUID: ble.MustParse(xd.uuid()), - Handle: uint16(xd.descriptorHandle()), - }) + + select { + case itf := <-ch: + if itf != nil { + return nil, itf.(error) + } + + case <-cln.Disconnected(): + return nil, fmt.Errorf("disconnected") + } + + for _, ddsc := range cbchr.Descriptors() { + dsc := &ble.Descriptor{ + UUID: ble.UUID(ddsc.UUID()), + } + c.Descriptors = append(c.Descriptors, dsc) + cln.pc.addDsc(dsc, ddsc) } return c.Descriptors, nil } // ReadCharacteristic reads a characteristic value from a server. [Vol 3, Part G, 4.8.1] func (cln *Client) ReadCharacteristic(c *ble.Characteristic) ([]byte, error) { - rsp, err := cln.conn.sendReq(cmdReadCharacteristic, xpc.Dict{ - "kCBMsgArgDeviceUUID": cln.id, - "kCBMsgArgCharacteristicHandle": c.Handle, - "kCBMsgArgCharacteristicValueHandle": c.ValueHandle, - }) + cbchr, err := cln.pc.findCbChr(c) if err != nil { return nil, err } - if rsp.err() != nil { - return nil, rsp.err() + + ch, err := cln.conn.addChrReader(c) + if err != nil { + return nil, fmt.Errorf("failed to read characteristic: %v", err) + } + defer cln.conn.delChrReader(c) + + cln.conn.prph.ReadCharacteristic(cbchr) + + select { + case itf := <-ch: + if itf != nil { + return nil, itf.(error) + } + + case <-cln.Disconnected(): + return nil, fmt.Errorf("disconnected") } - return rsp.data(), nil + + c.Value = cbchr.Value() + + return c.Value, nil } // ReadLongCharacteristic reads a characteristic value which is longer than the MTU. [Vol 3, Part G, 4.8.3] func (cln *Client) ReadLongCharacteristic(c *ble.Characteristic) ([]byte, error) { - return nil, ble.ErrNotImplemented + return cln.ReadCharacteristic(c) } // WriteCharacteristic writes a characteristic value to a server. [Vol 3, Part G, 4.9.3] func (cln *Client) WriteCharacteristic(c *ble.Characteristic, b []byte, noRsp bool) error { - args := xpc.Dict{ - "kCBMsgArgDeviceUUID": cln.id, - "kCBMsgArgCharacteristicHandle": c.Handle, - "kCBMsgArgCharacteristicValueHandle": c.ValueHandle, - "kCBMsgArgData": b, - "kCBMsgArgType": map[bool]int{false: 0, true: 1}[noRsp], + cbchr, err := cln.pc.findCbChr(c) + if err != nil { + return err } + if noRsp { - return cln.conn.sendCmd(cmdWriteCharacteristic, args) + cln.conn.prph.WriteCharacteristic(b, cbchr, false) + return nil } - m, err := cln.conn.sendReq(cmdWriteCharacteristic, args) - if err != nil { - return err + + ch := cln.conn.evl.chrWritten.Listen() + defer cln.conn.evl.chrWritten.Close() + + cln.conn.prph.WriteCharacteristic(b, cbchr, true) + + select { + case itf := <-ch: + if itf != nil { + return itf.(error) + } + + case <-cln.Disconnected(): + return fmt.Errorf("disconnected") } - return m.err() + + return nil } // ReadDescriptor reads a characteristic descriptor from a server. [Vol 3, Part G, 4.12.1] func (cln *Client) ReadDescriptor(d *ble.Descriptor) ([]byte, error) { - rsp, err := cln.conn.sendReq(cmdReadDescriptor, xpc.Dict{ - "kCBMsgArgDeviceUUID": cln.id, - "kCBMsgArgDescriptorHandle": d.Handle, - }) + cbdsc, err := cln.pc.findCbDsc(d) if err != nil { return nil, err } - if err := rsp.err(); err != nil { - return nil, err + + ch := cln.conn.evl.dscRead.Listen() + defer cln.conn.evl.dscRead.Close() + + cln.conn.prph.ReadDescriptor(cbdsc) + + select { + case itf := <-ch: + if itf != nil { + return nil, itf.(error) + } + + case <-cln.Disconnected(): + return nil, fmt.Errorf("disconnected") } - return rsp.data(), nil + + d.Value = cbdsc.Value() + + return d.Value, nil } // WriteDescriptor writes a characteristic descriptor to a server. [Vol 3, Part G, 4.12.3] func (cln *Client) WriteDescriptor(d *ble.Descriptor, b []byte) error { - rsp, err := cln.conn.sendReq(cmdWriteDescriptor, xpc.Dict{ - "kCBMsgArgDeviceUUID": cln.id, - "kCBMsgArgDescriptorHandle": d.Handle, - "kCBMsgArgData": b, - }) + cbdsc, err := cln.pc.findCbDsc(d) + if err != nil { + return err + } + + ch := cln.conn.evl.dscWritten.Listen() + defer cln.conn.evl.dscWritten.Close() + + cln.conn.prph.WriteDescriptor(b, cbdsc) if err != nil { return err } - return rsp.err() + + select { + case itf := <-ch: + if itf != nil { + return itf.(error) + } + + case <-cln.Disconnected(): + return fmt.Errorf("disconnected") + } + + return nil } // ReadRSSI retrieves the current RSSI value of remote peripheral. [Vol 2, Part E, 7.5.4] func (cln *Client) ReadRSSI() int { - rsp, err := cln.conn.sendReq(cmdReadRSSI, xpc.Dict{"kCBMsgArgDeviceUUID": cln.id}) - if err != nil { - return 0 - } - if rsp.err() != nil { + ch := cln.conn.evl.rssiRead.Listen() + defer cln.conn.evl.rssiRead.Close() + + cln.conn.prph.ReadRSSI() + + select { + case itf := <-ch: + ev := itf.(*eventRSSIRead) + if ev.err != nil { + return 0 + } + return ev.rssi + + case <-cln.Disconnected(): return 0 } - return rsp.rssi() } // ExchangeMTU set the ATT_MTU to the maximum possible value that can be @@ -253,44 +341,58 @@ func (cln *Client) ExchangeMTU(mtu int) (int, error) { // Subscribe subscribes to indication (if ind is set true), or notification of a // characteristic value. [Vol 3, Part G, 4.10 & 4.11] func (cln *Client) Subscribe(c *ble.Characteristic, ind bool, fn ble.NotificationHandler) error { - cln.conn.Lock() - defer cln.conn.Unlock() - cln.conn.subs[c.Handle] = &sub{fn: fn, char: c} - rsp, err := cln.conn.sendReq(cmdSubscribeCharacteristic, xpc.Dict{ - "kCBMsgArgDeviceUUID": cln.id, - "kCBMsgArgCharacteristicHandle": c.Handle, - "kCBMsgArgCharacteristicValueHandle": c.ValueHandle, - "kCBMsgArgState": 1, - }) + cbchr, err := cln.pc.findCbChr(c) if err != nil { - delete(cln.conn.subs, c.Handle) return err } - if err := rsp.err(); err != nil { - delete(cln.conn.subs, c.Handle) - return err + + cln.conn.addSub(c, fn) + + ch := cln.conn.evl.notifyChanged.Listen() + defer cln.conn.evl.notifyChanged.Close() + + cln.conn.prph.SetNotify(true, cbchr) + + select { + case itf := <-ch: + if itf != nil { + cln.conn.delSub(c) + return itf.(error) + } + + case <-cln.Disconnected(): + cln.conn.delSub(c) + return fmt.Errorf("disconnected") } + return nil } // Unsubscribe unsubscribes to indication (if ind is set true), or notification // of a specified characteristic value. [Vol 3, Part G, 4.10 & 4.11] func (cln *Client) Unsubscribe(c *ble.Characteristic, ind bool) error { - rsp, err := cln.conn.sendReq(cmdSubscribeCharacteristic, xpc.Dict{ - "kCBMsgArgDeviceUUID": cln.id, - "kCBMsgArgCharacteristicHandle": c.Handle, - "kCBMsgArgCharacteristicValueHandle": c.ValueHandle, - "kCBMsgArgState": 0, - }) + cbchr, err := cln.pc.findCbChr(c) if err != nil { return err } - if err := rsp.err(); err != nil { - return err + + ch := cln.conn.evl.notifyChanged.Listen() + defer cln.conn.evl.notifyChanged.Close() + + cln.conn.prph.SetNotify(false, cbchr) + + select { + case itf := <-ch: + if itf != nil { + return itf.(error) + } + + case <-cln.Disconnected(): + return fmt.Errorf("disconnected") } - cln.conn.Lock() - defer cln.conn.Unlock() - delete(cln.conn.subs, c.Handle) + + cln.conn.delSub(c) + return nil } @@ -306,11 +408,8 @@ func (cln *Client) ClearSubscriptions() error { // CancelConnection disconnects the connection. func (cln *Client) CancelConnection() error { - rsp, err := cln.conn.sendReq(cmdDisconnect, xpc.Dict{"kCBMsgArgDeviceUUID": cln.id}) - if err != nil { - return err - } - return rsp.err() + cln.cm.CancelConnect(cln.conn.prph) + return nil } // Disconnected returns a receiving channel, which is closed when the client disconnects. @@ -318,7 +417,44 @@ func (cln *Client) Disconnected() <-chan struct{} { return cln.conn.Disconnected() } +// Conn returns the client's current connection. +func (cln *Client) Conn() ble.Conn { + return cln.conn +} + type sub struct { fn ble.NotificationHandler char *ble.Characteristic } + +func (cln *Client) DidDiscoverServices(prph cbgo.Peripheral, err error) { + cln.conn.evl.svcsDiscovered.RxSignal(err) +} +func (cln *Client) DidDiscoverCharacteristics(prph cbgo.Peripheral, svc cbgo.Service, err error) { + cln.conn.evl.chrsDiscovered.RxSignal(err) +} +func (cln *Client) DidDiscoverDescriptors(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { + cln.conn.evl.dscsDiscovered.RxSignal(err) +} +func (cln *Client) DidUpdateValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { + cln.conn.processChrRead(err, chr) +} + +func (cln *Client) DidUpdateValueForDescriptor(prph cbgo.Peripheral, dsc cbgo.Descriptor, err error) { + cln.conn.evl.dscRead.RxSignal(err) +} +func (cln *Client) DidWriteValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { + cln.conn.evl.chrWritten.RxSignal(err) +} +func (cln *Client) DidWriteValueForDescriptor(prph cbgo.Peripheral, dsc cbgo.Descriptor, err error) { + cln.conn.evl.dscWritten.RxSignal(err) +} +func (cln *Client) DidUpdateNotificationState(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) { + cln.conn.evl.notifyChanged.RxSignal(err) +} +func (cln *Client) DidReadRSSI(prph cbgo.Peripheral, rssi int, err error) { + cln.conn.evl.rssiRead.RxSignal(&eventRSSIRead{ + err: err, + rssi: int(rssi), + }) +} diff --git a/darwin/cmgrdlg.go b/darwin/cmgrdlg.go new file mode 100644 index 00000000..e3454a9d --- /dev/null +++ b/darwin/cmgrdlg.go @@ -0,0 +1,71 @@ +// cmgrdlg.go: Implements the CentralManagerDelegate interface. CoreBluetooth +// communicates events asynchronously via callbacks. This file implements a +// synchronous interface by translating these callbacks into channel +// operations. + +package darwin + +import ( + "github.com/go-ble/ble" + "github.com/JuulLabs-OSS/cbgo" +) + +func (d *Device) CentralManagerDidUpdateState(cmgr cbgo.CentralManager) { + d.evl.stateChanged.RxSignal(struct{}{}) +} + +func (d *Device) DidDiscoverPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, + advFields cbgo.AdvFields, rssi int) { + + if d.advHandler == nil { + return + } + + a := &adv{ + localName: advFields.LocalName, + rssi: int(rssi), + mfgData: advFields.ManufacturerData, + } + if advFields.Connectable != nil { + a.connectable = *advFields.Connectable + } + if advFields.TxPowerLevel != nil { + a.powerLevel = *advFields.TxPowerLevel + } + for _, u := range advFields.ServiceUUIDs { + a.svcUUIDs = append(a.svcUUIDs, ble.UUID(u)) + } + for _, sd := range advFields.ServiceData { + a.svcData = append(a.svcData, ble.ServiceData{ + UUID: ble.UUID(sd.UUID), + Data: sd.Data, + }) + } + a.peerUUID = ble.UUID(prph.Identifier()) + + d.advHandler(a) +} + +func (d *Device) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) { + fail := func(err error) { + d.evl.connected.RxSignal(&eventConnected{ + err: err, + }) + } + + c, err := newCentralConn(d, prph) + if err != nil { + fail(err) + } + + d.evl.connected.RxSignal(&eventConnected{ + conn: c, + }) +} + +func (d *Device) DidDisconnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, err error) { + c := d.findConn(ble.NewAddr(prph.Identifier().String())) + if c != nil { + close(c.done) + } +} diff --git a/darwin/conn.go b/darwin/conn.go index 96079818..a45c7c42 100644 --- a/darwin/conn.go +++ b/darwin/conn.go @@ -2,26 +2,71 @@ package darwin import ( "context" + "fmt" "log" "sync" "github.com/go-ble/ble" - "github.com/raff/goble/xpc" + "github.com/JuulLabs-OSS/cbgo" ) -func newConn(d *Device, a ble.Addr) *conn { - return &conn{ +// newGenConn creates a new generic (role-less) connection. This should not be +// called directly; use newCentralConn or newPeripheralConn instead. +func newGenConn(d *Device, a ble.Addr) (*conn, error) { + c := &conn{ dev: d, rxMTU: 23, txMTU: 23, addr: a, done: make(chan struct{}), - notifiers: make(map[uint16]ble.Notifier), - subs: make(map[uint16]*sub), + notifiers: make(map[cbgo.Characteristic]ble.Notifier), - rspc: make(chan msg), + subs: make(map[string]*sub), + chrReads: make(map[string]chan error), } + + err := d.addConn(c) + if err != nil { + return nil, err + } + + go func() { + <-c.done + d.delConn(c.addr) + }() + + return c, nil +} + +// newCentralConn creates a new connection with us acting as central +// (peer=peripheral). +func newCentralConn(d *Device, prph cbgo.Peripheral) (*conn, error) { + c, err := newGenConn(d, ble.NewAddr(prph.Identifier().String())) + if err != nil { + return nil, err + } + + // -3 to account for WriteCommand base. + c.txMTU = prph.MaximumWriteValueLength(false) - 3 + c.prph = prph + + return c, nil +} + +// newCentralConn creates a new connection with us acting as peripheral +// (peer=central). +func newPeripheralConn(d *Device, cent cbgo.Central) (*conn, error) { + c, err := newGenConn(d, ble.NewAddr(cent.Identifier().String())) + if err != nil { + return nil, err + } + + // -3 to account for ATT_HANDLE_VALUE_NTF base. + c.txMTU = cent.MaximumUpdateValueLength() - 3 + c.cent = cent + + return c, nil } type conn struct { @@ -34,15 +79,15 @@ type conn struct { addr ble.Addr done chan struct{} - rspc chan msg + evl clientEventListener - connInterval int - connLatency int - supervisionTimeout int + prph cbgo.Peripheral + cent cbgo.Central - notifiers map[uint16]ble.Notifier // central connection only + notifiers map[cbgo.Characteristic]ble.Notifier // central connection only - subs map[uint16]*sub + subs map[string]*sub + chrReads map[string](chan error) } func (c *conn) Context() context.Context { @@ -75,18 +120,20 @@ func (c *conn) TxMTU() int { } func (c *conn) SetTxMTU(mtu int) { + c.Lock() c.txMTU = mtu + c.Unlock() } func (c *conn) Read(b []byte) (int, error) { return 0, nil } - func (c *conn) Write(b []byte) (int, error) { return 0, nil } func (c *conn) Close() error { + c.evl.Close() return nil } @@ -95,45 +142,84 @@ func (c *conn) Disconnected() <-chan struct{} { return c.done } -// server (peripheral) -func (c *conn) subscribed(char *ble.Characteristic) { - h := char.Handle - if _, found := c.notifiers[h]; found { - return +// processChrRead handles an incoming read response. CoreBluetooth does not +// distinguish explicit reads from unsolicited notifications. This function +// identifies which type the incoming message is. +func (c *conn) processChrRead(err error, cbchr cbgo.Characteristic) { + c.RLock() + defer c.RUnlock() + + uuidStr := uuidStrWithDashes(cbchr.UUID().String()) + found := false + + ch := c.chrReads[uuidStr] + if ch != nil { + ch <- err + found = true } - send := func(b []byte) (int, error) { - err := c.dev.sendCmd(c.dev.pm, cmdSubscribed, xpc.Dict{ - "kCBMsgArgUUIDs": [][]byte{}, - "kCBMsgArgAttributeID": h, - "kCBMsgArgData": b, - }) - return len(b), err + + s := c.subs[uuidStr] + if s != nil { + s.fn(cbchr.Value()) + found = true } - n := ble.NewNotifier(send) - c.notifiers[h] = n - req := ble.NewRequest(c, nil, 0) // convey *conn to user handler. - go char.NotifyHandler.ServeNotify(req, n) -} - -// server (peripheral) -func (c *conn) unsubscribed(char *ble.Characteristic) { - if n, found := c.notifiers[char.Handle]; found { - if err := n.Close(); err != nil { - log.Printf("failed to clone notifier: %v", err) - } - delete(c.notifiers, char.Handle) + + if !found { + log.Printf("received characteristic read response without corresponding request: uuid=%s", uuidStr) } } -func (c *conn) sendReq(id int, args xpc.Dict) (msg, error) { - err := c.dev.sendCmd(c.dev.cm, id, args) - if err != nil { - return msg{}, err +// addChrReader starts listening for a solicited read response. +func (c *conn) addChrReader(char *ble.Characteristic) (chan error, error) { + uuidStr := uuidStrWithDashes(char.UUID.String()) + + c.Lock() + defer c.Unlock() + + if c.chrReads[uuidStr] != nil { + return nil, fmt.Errorf("cannot read from the same attribute twice: uuid=%s", uuidStr) } - m := <-c.rspc - return msg(m.args()), nil + + ch := make(chan error) + c.chrReads[uuidStr] = ch + + return ch, nil } -func (c *conn) sendCmd(id int, args xpc.Dict) error { - return c.dev.sendCmd(c.dev.pm, id, args) +// delChrReader stops listening for a solicited read response. +func (c *conn) delChrReader(char *ble.Characteristic) { + c.Lock() + defer c.Unlock() + + uuidStr := uuidStrWithDashes(char.UUID.String()) + delete(c.chrReads, uuidStr) +} + +// addSub starts listening for unsolicited notifications and indications for a +// particular characteristic. +func (c *conn) addSub(char *ble.Characteristic, fn ble.NotificationHandler) { + uuidStr := uuidStrWithDashes(char.UUID.String()) + + c.Lock() + defer c.Unlock() + + // It feels like we should return an error if we are already subscribed to + // this characteristic. Just quietly overwrite the existing handler to + // preserve backwards compatibility. + + c.subs[uuidStr] = &sub{ + fn: fn, + char: char, + } +} + +// delSub stops listening for unsolicited notifications and indications for a +// particular characteristic. +func (c *conn) delSub(char *ble.Characteristic) { + uuidStr := uuidStrWithDashes(char.UUID.String()) + + c.Lock() + defer c.Unlock() + + delete(c.subs, uuidStr) } diff --git a/darwin/device.go b/darwin/device.go index c94abc61..62b4faaf 100644 --- a/darwin/device.go +++ b/darwin/device.go @@ -1,564 +1,350 @@ package darwin import ( - "bytes" "context" "encoding/binary" + "errors" "fmt" - "log" "time" "github.com/go-ble/ble" - "github.com/pkg/errors" - "github.com/raff/goble/xpc" + "github.com/JuulLabs-OSS/cbgo" "sync" ) // Device is either a Peripheral or Central device. type Device struct { - pm xpc.XPC // peripheralManager - cm xpc.XPC // centralManager + // Embed these two bases so we don't have to override all the esoteric + // functions defined by CoreBluetooth delegate interfaces. + cbgo.CentralManagerDelegateBase + cbgo.PeripheralManagerDelegateBase - role int // 1: peripheralManager (server), 0: centralManager (client) - - rspc chan msg + cm cbgo.CentralManager + pm cbgo.PeripheralManager + evl deviceEventListener + pc profCache conns map[string]*conn connLock sync.Mutex - // Only used in client/centralManager implementation advHandler ble.AdvHandler - chConn chan *conn - - // Only used in server/peripheralManager implementation - chars map[int]*ble.Characteristic - base int } // NewDevice returns a BLE device. -func NewDevice(opts ...Option) (*Device, error) { - err := initXpcIDs() - if err != nil { - return nil, err - } - +func NewDevice(opts ...ble.Option) (*Device, error) { d := &Device{ - rspc: make(chan msg), - conns: make(map[string]*conn), - chConn: make(chan *conn), - chars: make(map[int]*ble.Characteristic), - base: 1, - } - if err := d.Option(opts...); err != nil { - return nil, err + cm: cbgo.NewCentralManager(nil), + pm: cbgo.NewPeripheralManager(nil), + pc: newProfCache(), + conns: make(map[string]*conn), } - d.pm = xpc.XpcConnect(serviceID, d) - d.cm = xpc.XpcConnect(serviceID, d) + // Only proceed if Bluetooth is enabled. - return d, errors.Wrap(d.Init(), "can't init") -} + blockUntilStateChange := func(getState func() cbgo.ManagerState) { + if getState() != cbgo.ManagerStateUnknown { + return + } -// Option sets the options specified. -func (d *Device) Option(opts ...Option) error { - var err error - for _, opt := range opts { - err = opt(d) - } - return err -} + // Wait until state changes or until one second passes (whichever + // happens first). + for { + select { + case <-d.evl.stateChanged.Listen(): + if getState() != cbgo.ManagerStateUnknown { + return + } -// Init ... -func (d *Device) Init() error { - rsp, err := d.sendReq(d.cm, cmdInit, xpc.Dict{ - "kCBMsgArgName": fmt.Sprintf("gopher-%v", time.Now().Unix()), - "kCBMsgArgOptions": xpc.Dict{ - "kCBInitOptionShowPowerAlert": 1, - }, - "kCBMsgArgType": 0, - }) - if err != nil { - return err - } - s := State(rsp.state()) - if s != StatePoweredOn { - return fmt.Errorf("state: %s", s) + case <-time.NewTimer(time.Second).C: + return + } + } } - rsp, err = d.sendReq(d.pm, cmdInit, xpc.Dict{ - "kCBMsgArgName": fmt.Sprintf("gopher-%v", time.Now().Unix()), - "kCBMsgArgOptions": xpc.Dict{ - "kCBInitOptionShowPowerAlert": 1, - }, - "kCBMsgArgType": 1, - }) - if err != nil { - return err + // Ensure central manager is ready. + d.cm.SetDelegate(d) + blockUntilStateChange(d.cm.State) + if d.cm.State() != cbgo.ManagerStatePoweredOn { + return nil, fmt.Errorf("central manager has invalid state: have=%d want=%d: is Bluetooth turned on?", + d.cm.State(), cbgo.ManagerStatePoweredOn) } - s = State(rsp.state()) - if s != StatePoweredOn { - return fmt.Errorf("state: %s", s) + + // Ensure peripheral manager is ready. + d.pm.SetDelegate(d) + blockUntilStateChange(d.pm.State) + if d.pm.State() != cbgo.ManagerStatePoweredOn { + return nil, fmt.Errorf("peripheral manager has invalid state: have=%d want=%d: is Bluetooth turned on?", + d.pm.State(), cbgo.ManagerStatePoweredOn) } + + return d, nil +} + +// Option sets the options specified. +func (d *Device) Option(opts ...ble.Option) error { return nil } -// Advertise advertises the given Advertisement -func (d *Device) Advertise(ctx context.Context, adv ble.Advertisement) error { - rsp, err := d.sendReq(d.pm, cmdAdvertiseStart, xpc.Dict{ - "kCBAdvDataLocalName": adv.LocalName(), - "kCBAdvDataServiceUUIDs": adv.Services(), - "kCBAdvDataAppleMfgData": adv.ManufacturerData(), +// Scan ... +func (d *Device) Scan(ctx context.Context, allowDup bool, h ble.AdvHandler) error { + d.advHandler = h + + d.cm.Scan(nil, &cbgo.CentralManagerScanOpts{ + AllowDuplicates: allowDup, }) - if err != nil { - return err - } - if err := rsp.err(); err != nil { - return err - } + <-ctx.Done() - _ = d.stopAdvertising() - return ctx.Err() + d.cm.StopScan() + return ctx.Err() } -// AdvertiseMfgData ... -func (d *Device) AdvertiseMfgData(ctx context.Context, id uint16, md []byte) error { - l := len(md) - b := []byte{byte(l + 3), 0xFF, uint8(id), uint8(id >> 8)} - rsp, err := d.sendReq(d.pm, cmdAdvertiseStart, xpc.Dict{ - "kCBAdvDataAppleMfgData": append(b, md...), - }) +// Dial ... +func (d *Device) Dial(ctx context.Context, a ble.Addr) (ble.Client, error) { + uuid, err := cbgo.ParseUUID(uuidStrWithDashes(a.String())) if err != nil { - return err - } - if err := rsp.err(); err != nil { - return errors.Wrap(err, "can't advertise") + return nil, fmt.Errorf("dial failed: invalid peer address: %s", a) } - <-ctx.Done() - return ctx.Err() -} -// AdvertiseServiceData16 advertises data associated with a 16bit service uuid -func (d *Device) AdvertiseServiceData16(ctx context.Context, id uint16, b []byte) error { - l := len(b) - prefix := []byte{ - 0x03, 0x03, uint8(id), uint8(id >> 8), - byte(l + 3), 0x16, uint8(id), uint8(id >> 8), + prphs := d.cm.RetrievePeripheralsWithIdentifiers([]cbgo.UUID{uuid}) + if len(prphs) == 0 { + return nil, fmt.Errorf("dial failed: no peer with address: %s", a) } - rsp, err := d.sendReq(d.pm, cmdAdvertiseStart, xpc.Dict{ - "kCBAdvDataAppleMfgData": append(prefix, b...), - }) - if err != nil { - return err - } - if err := rsp.err(); err != nil { - return errors.Wrap(err, "can't advertise") + + ch := d.evl.connected.Listen() + defer d.evl.connected.Close() + + d.cm.Connect(prphs[0], nil) + select { + case <-ctx.Done(): + return nil, ctx.Err() + case itf := <-ch: + if itf == nil { + return nil, fmt.Errorf("connect failed: aborted") + } + + ev := itf.(*eventConnected) + if ev.err != nil { + return nil, ev.err + } else { + ev.conn.SetContext(ctx) + return NewClient(d.cm, ev.conn) + } } - <-ctx.Done() - return ctx.Err() } -// AdvertiseNameAndServices advertises name and specifid service UUIDs. -func (d *Device) AdvertiseNameAndServices(ctx context.Context, name string, ss ...ble.UUID) error { - rsp, err := d.sendReq(d.pm, cmdAdvertiseStart, xpc.Dict{ - "kCBAdvDataLocalName": name, - "kCBAdvDataServiceUUIDs": uuidSlice(ss)}, - ) - if err != nil { - return err - } - if err := rsp.err(); err != nil { - return err - } - <-ctx.Done() - _ = d.stopAdvertising() - return ctx.Err() +// Stop ... +func (d *Device) Stop() error { + return nil } -// AdvertiseIBeaconData advertises iBeacon packet with specified manufacturer data. -func (d *Device) AdvertiseIBeaconData(ctx context.Context, md []byte) error { - var utsname xpc.Utsname - err := xpc.Uname(&utsname) - if err != nil { - return err - } +func (d *Device) closeConns() { + d.connLock.Lock() + defer d.connLock.Unlock() - if utsname.Release >= "14." { - ibeaconCode := []byte{0x02, 0x15} - return d.AdvertiseMfgData(ctx, 0x004C, append(ibeaconCode, md...)) - } - rsp, err := d.sendReq(d.pm, cmdAdvertiseStart, xpc.Dict{"kCBAdvDataAppleBeaconKey": md}) - if err != nil { - return err + for _, c := range d.conns { + c.Close() } - if err := rsp.err(); err != nil { - return err - } - <-ctx.Done() - return d.stopAdvertising() } -// AdvertiseIBeacon advertises iBeacon packet. -func (d *Device) AdvertiseIBeacon(ctx context.Context, u ble.UUID, major, minor uint16, pwr int8) error { - b := make([]byte, 21) - copy(b, ble.Reverse(u)) // Big endian - binary.BigEndian.PutUint16(b[16:], major) // Big endian - binary.BigEndian.PutUint16(b[18:], minor) // Big endian - b[20] = uint8(pwr) // Measured Tx Power - return d.AdvertiseIBeaconData(ctx, b) +func (d *Device) findConn(a ble.Addr) *conn { + d.connLock.Lock() + defer d.connLock.Unlock() + + return d.conns[a.String()] } -// stopAdvertising stops advertising. -func (d *Device) stopAdvertising() error { - rsp, err := d.sendReq(d.pm, cmdAdvertiseStop, nil) - if err != nil { - return errors.Wrap(err, "can't send stop advertising") - } - if err := rsp.err(); err != nil { - return errors.Wrap(err, "can't stop advertising") +func (d *Device) addConn(c *conn) error { + d.connLock.Lock() + defer d.connLock.Unlock() + + if d.conns[c.addr.String()] != nil { + return fmt.Errorf("failed to add connection: already exists: addr=%v", c.addr) } + + d.conns[c.addr.String()] = c + return nil } -// Scan ... -func (d *Device) Scan(ctx context.Context, allowDup bool, h ble.AdvHandler) error { - d.advHandler = h - if err := d.sendCmd(d.cm, cmdScanningStart, xpc.Dict{ - // "kCBMsgArgUUIDs": uuidSlice(ss), - "kCBMsgArgOptions": xpc.Dict{ - "kCBScanOptionAllowDuplicates": map[bool]int{true: 1, false: 0}[allowDup], - }, - }); err != nil { - return err - } - <-ctx.Done() - if err := d.stopScanning(); err != nil { - return errors.Wrap(ctx.Err(), err.Error()) - } - return ctx.Err() -} +func (d *Device) delConn(a ble.Addr) { + d.connLock.Lock() + defer d.connLock.Unlock() -// stopAdvertising stops advertising. -func (d *Device) stopScanning() error { - return errors.Wrap(d.sendCmd(d.cm, cmdScanningStop, nil), "can't stop scanning") + delete(d.conns, a.String()) } -// RemoveAllServices removes all services of device's -func (d *Device) RemoveAllServices() error { - return d.sendCmd(d.pm, cmdServicesRemove, nil) +func (d *Device) connectFail(err error) { + d.evl.connected.RxSignal(&eventConnected{ + err: err, + }) } -// AddService adds a service to device's database. -// The following services are ignored as they are provided by OS X. -// -// 0x1800 (Generic Access) -// 0x1801 (Generic Attribute) -// 0x1805 (Current Time Service) -// 0x180A (Device Information) -// 0x180F (Battery Service) -// 0x1812 (Human Interface Device) -func (d *Device) AddService(s *ble.Service) error { - if s.UUID.Equal(ble.GAPUUID) || - s.UUID.Equal(ble.GATTUUID) || - s.UUID.Equal(ble.CurrentTimeUUID) || - s.UUID.Equal(ble.DeviceInfoUUID) || - s.UUID.Equal(ble.BatteryUUID) || - s.UUID.Equal(ble.HIDUUID) { - return nil - } - xs := xpc.Dict{ - "kCBMsgArgAttributeID": d.base, - "kCBMsgArgAttributeIDs": []int{}, - "kCBMsgArgCharacteristics": nil, - "kCBMsgArgType": 1, // 1 => primary, 0 => excluded - "kCBMsgArgUUID": ble.Reverse(s.UUID), - } - d.base++ - - xcs := xpc.Array{} - for _, c := range s.Characteristics { - props := 0 - perm := 0 - if c.Property&ble.CharRead != 0 { - props |= 0x02 - if ble.CharRead&c.Secure != 0 { - perm |= 0x04 - } else { - perm |= 0x01 - } +func chrPropPerm(c *ble.Characteristic) (cbgo.CharacteristicProperties, cbgo.AttributePermissions) { + var prop cbgo.CharacteristicProperties + var perm cbgo.AttributePermissions + + if c.Property&ble.CharRead != 0 { + prop |= cbgo.CharacteristicPropertyRead + if ble.CharRead&c.Secure != 0 { + perm |= cbgo.AttributePermissionsReadEncryptionRequired + } else { + perm |= cbgo.AttributePermissionsReadable } - if c.Property&ble.CharWriteNR != 0 { - props |= 0x04 - if c.Secure&ble.CharWriteNR != 0 { - perm |= 0x08 - } else { - perm |= 0x02 - } + } + if c.Property&ble.CharWriteNR != 0 { + prop |= cbgo.CharacteristicPropertyWriteWithoutResponse + if c.Secure&ble.CharWriteNR != 0 { + perm |= cbgo.AttributePermissionsWriteEncryptionRequired + } else { + perm |= cbgo.AttributePermissionsWriteable } - if c.Property&ble.CharWrite != 0 { - props |= 0x08 - if c.Secure&ble.CharWrite != 0 { - perm |= 0x08 - } else { - perm |= 0x02 - } + } + if c.Property&ble.CharWrite != 0 { + prop |= cbgo.CharacteristicPropertyWrite + if c.Secure&ble.CharWrite != 0 { + perm |= cbgo.AttributePermissionsWriteEncryptionRequired + } else { + perm |= cbgo.AttributePermissionsWriteable } - if c.Property&ble.CharNotify != 0 { - if c.Secure&ble.CharNotify != 0 { - props |= 0x100 - } else { - props |= 0x10 - } + } + if c.Property&ble.CharNotify != 0 { + if c.Secure&ble.CharNotify != 0 { + prop |= cbgo.CharacteristicPropertyNotifyEncryptionRequired + } else { + prop |= cbgo.CharacteristicPropertyNotify } - if c.Property&ble.CharIndicate != 0 { - if c.Secure&ble.CharIndicate != 0 { - props |= 0x200 - } else { - props |= 0x20 - } + } + if c.Property&ble.CharIndicate != 0 { + if c.Secure&ble.CharIndicate != 0 { + prop |= cbgo.CharacteristicPropertyIndicateEncryptionRequired + } else { + prop |= cbgo.CharacteristicPropertyIndicate } + } - xc := xpc.Dict{ - "kCBMsgArgAttributeID": d.base, - "kCBMsgArgUUID": ble.Reverse(c.UUID), - "kCBMsgArgAttributePermissions": perm, - "kCBMsgArgCharacteristicProperties": props, - "kCBMsgArgData": c.Value, - } - c.Handle = uint16(d.base) - d.chars[d.base] = c - d.base++ + return prop, perm +} - xds := xpc.Array{} +func (d *Device) AddService(svc *ble.Service) error { + chrMap := make(map[*ble.Characteristic]cbgo.Characteristic) + dscMap := make(map[*ble.Descriptor]cbgo.Descriptor) + + msvc := cbgo.NewMutableService(cbgo.UUID(svc.UUID), true) + + var mchrs []cbgo.MutableCharacteristic + for _, c := range svc.Characteristics { + prop, perm := chrPropPerm(c) + mchr := cbgo.NewMutableCharacteristic(cbgo.UUID(c.UUID), prop, c.Value, perm) + + var mdscs []cbgo.MutableDescriptor for _, d := range c.Descriptors { - if d.UUID.Equal(ble.ClientCharacteristicConfigUUID) { - // skip CCCD - continue - } - xd := xpc.Dict{ - "kCBMsgArgData": d.Value, - "kCBMsgArgUUID": ble.Reverse(d.UUID), - } - xds = append(xds, xd) + mdsc := cbgo.NewMutableDescriptor(cbgo.UUID(d.UUID), d.Value) + mdscs = append(mdscs, mdsc) + dscMap[d] = mdsc.Descriptor() } - xc["kCBMsgArgDescriptors"] = xds - xcs = append(xcs, xc) + mchr.SetDescriptors(mdscs) + + mchrs = append(mchrs, mchr) + chrMap[c] = mchr.Characteristic() } - xs["kCBMsgArgCharacteristics"] = xcs + msvc.SetCharacteristics(mchrs) - rsp, err := d.sendReq(d.pm, cmdServicesAdd, xs) - if err != nil { - return err + ch := d.evl.svcAdded.Listen() + d.pm.AddService(msvc) + + itf := <-ch + if itf != nil { + return itf.(error) } - return rsp.err() -} -// SetServices ... -func (d *Device) SetServices(ss []*ble.Service) error { - if err := d.RemoveAllServices(); err != nil { - return nil + d.pc.addSvc(svc, msvc.Service()) + for chr, cbc := range chrMap { + d.pc.addChr(chr, cbc) } - for _, s := range ss { - if err := d.AddService(s); err != nil { - return err - } + for dsc, cbd := range dscMap { + d.pc.addDsc(dsc, cbd) } + return nil } -// Dial ... -func (d *Device) Dial(ctx context.Context, a ble.Addr) (ble.Client, error) { - err := d.sendCmd(d.cm, cmdConnect, xpc.Dict{ - "kCBMsgArgDeviceUUID": xpc.MakeUUID(a.String()), - "kCBMsgArgOptions": xpc.Dict{ - "kCBConnectOptionNotifyOnDisconnection": 1, - }, - }) - if err != nil { - return nil, err - } - select { - case <-ctx.Done(): - return nil, ctx.Err() - case c := <-d.chConn: - c.SetContext(ctx) - return NewClient(c) +func (d *Device) RemoveAllServices() error { + d.pm.RemoveAllServices() + return nil +} + +func (d *Device) SetServices(svcs []*ble.Service) error { + d.RemoveAllServices() + for _, s := range svcs { + d.AddService(s) } + + return nil } -// Stop ... -func (d *Device) Stop() error { +func (d *Device) stopAdvertising() error { + d.pm.StopAdvertising() return nil } -// HandleXpcEvent process Device events and asynchronous errors. -func (d *Device) HandleXpcEvent(event xpc.Dict, err error) { - if err != nil { - log.Println("error:", err) - return +func (d *Device) advData(ctx context.Context, ad cbgo.AdvData) error { + ch := d.evl.advStarted.Listen() + d.pm.StartAdvertising(ad) + + itf := <-ch + if itf != nil { + return itf.(error) } - m := msg(event) - args := msg(msg(event).args()) - logger.Info("recv", "id", m.id(), "args", fmt.Sprintf("%v", m.args())) - - switch m.id() { - case // Device event - evtStateChanged, - evtAdvertisingStarted, - evtAdvertisingStopped, - evtServiceAdded: - d.rspc <- args - - case evtPeripheralDiscovered: - if d.advHandler == nil { - break - } - a := &adv{args: m.args(), ad: args.advertisementData()} - go d.advHandler(a) - - case evtConfirmation: - // log.Printf("confirmed: %d", args.attributeID()) - - case evtATTMTU: - d.conn(args).SetTxMTU(args.attMTU()) - - case evtSlaveConnectionComplete: - // remote peripheral is connected. - fallthrough - case evtMasterConnectionComplete: - // remote central is connected. - - // Could be LEConnectionComplete or LEConnectionUpdateComplete. - c := d.conn(args) - c.connInterval = args.connectionInterval() - c.connLatency = args.connectionLatency() - c.supervisionTimeout = args.supervisionTimeout() - - case evtReadRequest: - aid := args.attributeID() - char := d.chars[aid] - v := char.Value - if v == nil { - c := d.conn(args) - req := ble.NewRequest(c, nil, args.offset()) - buf := bytes.NewBuffer(make([]byte, 0, c.txMTU-1)) - rsp := ble.NewResponseWriter(buf) - char.ReadHandler.ServeRead(req, rsp) - v = buf.Bytes() - } - err := d.sendCmd(d.pm, cmdSendData, xpc.Dict{ - "kCBMsgArgAttributeID": aid, - "kCBMsgArgData": v, - "kCBMsgArgTransactionID": args.transactionID(), - "kCBMsgArgResult": 0, - }) - if err != nil { - log.Printf("error: %v", err) - return - } - case evtWriteRequest: - for _, xxw := range args.attWrites() { - xw := msg(xxw.(xpc.Dict)) - aid := xw.attributeID() - char := d.chars[aid] - req := ble.NewRequest(d.conn(args), xw.data(), xw.offset()) - char.WriteHandler.ServeWrite(req, nil) - if xw.ignoreResponse() == 1 { - continue - } - err := d.sendCmd(d.pm, cmdSendData, xpc.Dict{ - "kCBMsgArgAttributeID": aid, - "kCBMsgArgData": nil, - "kCBMsgArgTransactionID": args.transactionID(), - "kCBMsgArgResult": 0, - }) - if err != nil { - log.Println("error:", err) - return - } - } + <-ctx.Done() + _ = d.stopAdvertising() + return ctx.Err() +} - case evtSubscribe: - // characteristic is subscribed by remote central. - d.conn(args).subscribed(d.chars[args.attributeID()]) +func (d *Device) Advertise(ctx context.Context, adv ble.Advertisement) error { + ad := cbgo.AdvData{} - case evtUnsubscribe: - // characteristic is unsubscribed by remote central. - d.conn(args).unsubscribed(d.chars[args.attributeID()]) + ad.LocalName = adv.LocalName() + for _, u := range adv.Services() { + ad.ServiceUUIDs = append(ad.ServiceUUIDs, cbgo.UUID(u)) + } - case evtPeripheralConnected: - d.chConn <- d.conn(args) + return d.advData(ctx, ad) +} - case evtPeripheralDisconnected: - c := d.conn(args) - select { - case c.rspc <- m: - // Canceled by local central synchronously - default: - // Canceled by remote peripheral asynchronously. - } - d.connLock.Lock() - delete(d.conns, c.RemoteAddr().String()) - d.connLock.Unlock() - close(c.done) - - case evtCharacteristicRead: - // Notification - c := d.conn(args) - - sub := c.subs[uint16(args.characteristicHandle())] - if sub == nil { - log.Printf("notified by unsubscribed handle") - // FIXME: should terminate the connection? - } else { - sub.fn(args.data()) - } - break - - case // Peripheral events - evtRSSIRead, - evtServiceDiscovered, - evtIncludedServicesDiscovered, - evtCharacteristicsDiscovered, - evtCharacteristicWritten, - evtNotificationValueSet, - evtDescriptorsDiscovered, - evtDescriptorRead, - evtDescriptorWritten: - - d.conn(args).rspc <- m - - default: - log.Printf("Unhandled event: %#v", event) +func (d *Device) AdvertiseNameAndServices(ctx context.Context, name string, uuids ...ble.UUID) error { + a := &adv{ + localName: name, + svcUUIDs: uuids, } + + return d.Advertise(ctx, a) } -func (d *Device) conn(m msg) *conn { - // Convert xpc.UUID to ble.UUID. - a := ble.MustParse(m.deviceUUID().String()) - d.connLock.Lock() - c, ok := d.conns[a.String()] - if !ok { - c = newConn(d, a) - d.conns[a.String()] = c - } - d.connLock.Unlock() - return c +func (d *Device) AdvertiseMfgData(ctx context.Context, id uint16, b []byte) error { + // CoreBluetooth doesn't let you specify manufacturer data :( + return errors.New("Not supported") } -// sendReq sends a message and waits for its reply. -func (d *Device) sendReq(x xpc.XPC, id int, args xpc.Dict) (msg, error) { - err := d.sendCmd(x, id, args) - if err != nil { - return msg{}, err +func (d *Device) AdvertiseServiceData16(ctx context.Context, id uint16, b []byte) error { + // CoreBluetooth doesn't let you specify service data :( + return errors.New("Not supported") +} + +func (d *Device) AdvertiseIBeaconData(ctx context.Context, b []byte) error { + ad := cbgo.AdvData{ + IBeaconData: b, } - return <-d.rspc, nil + return d.advData(ctx, ad) } -func (d *Device) sendCmd(x xpc.XPC, id int, args xpc.Dict) error { - logger.Info("send", "id", id, "args", fmt.Sprintf("%v", args)) - x.Send(xpc.Dict{"kCBMsgId": id, "kCBMsgArgs": args}, false) - return nil +func (d *Device) AdvertiseIBeacon(ctx context.Context, u ble.UUID, major, minor uint16, pwr int8) error { + b := make([]byte, 21) + copy(b, ble.Reverse(u)) // Big endian + binary.BigEndian.PutUint16(b[16:], major) // Big endian + binary.BigEndian.PutUint16(b[18:], minor) // Big endian + b[20] = uint8(pwr) // Measured Tx Power + return d.AdvertiseIBeaconData(ctx, b) } diff --git a/darwin/event.go b/darwin/event.go new file mode 100644 index 00000000..b2cb7f0e --- /dev/null +++ b/darwin/event.go @@ -0,0 +1,113 @@ +package darwin + +import ( + "sync" +) + +// eventSlot is a receiver for asynchronous events from CoreBluetooth. To +// prevent deadlock in the case of spurious events, eventSlot discards incoming +// signals if it is not explicitly listening for them. +type eventSlot struct { + ch chan interface{} + mtx sync.Mutex +} + +func (e *eventSlot) closeNoLock() { + if e.ch == nil { + return + } + + // Drain channel. + for len(e.ch) > 0 { + <-e.ch + } + + close(e.ch) + e.ch = nil +} + +func (e *eventSlot) Close() { + e.mtx.Lock() + defer e.mtx.Unlock() + + e.closeNoLock() +} + +// Listen listens for a single event on this slot. It returns the channel on +// which the event will be received. +func (e *eventSlot) Listen() chan interface{} { + e.mtx.Lock() + defer e.mtx.Unlock() + + if e.ch != nil { + e.closeNoLock() + } + + e.ch = make(chan interface{}) + return e.ch +} + +// RxSignal causes the event slot to process the given signal (i.e., it sends a +// signal to the slot). It blocks until the signal is consumed by a client or +// until the slot is closed. +func (e *eventSlot) RxSignal(sig interface{}) { + e.mtx.Lock() + defer e.mtx.Unlock() + + if e.ch == nil { + // Not listening. Discard signal. + return + } + + e.ch <- sig + + // Stop listening. + e.closeNoLock() +} + +type eventConnected struct { + conn *conn + err error +} + +type eventRSSIRead struct { + rssi int + err error +} + +// Each Client owns one of these (us-as-central). +type clientEventListener struct { + svcsDiscovered eventSlot // error + chrsDiscovered eventSlot // error + dscsDiscovered eventSlot // error + chrWritten eventSlot // error + dscRead eventSlot // error + dscWritten eventSlot // error + notifyChanged eventSlot // error + rssiRead eventSlot // *eventRSSIRead +} + +func (cevl *clientEventListener) Close() { + cevl.svcsDiscovered.Close() + cevl.chrsDiscovered.Close() + cevl.dscsDiscovered.Close() + cevl.chrWritten.Close() + cevl.dscRead.Close() + cevl.dscWritten.Close() + cevl.notifyChanged.Close() + cevl.rssiRead.Close() +} + +// Each Device owns one of these (us-as-peripheral). +type deviceEventListener struct { + stateChanged eventSlot // struct{} + connected eventSlot // *eventConnected + svcAdded eventSlot // error + advStarted eventSlot // error +} + +func (devl *deviceEventListener) Close() { + devl.stateChanged.Close() + devl.svcAdded.Close() + devl.advStarted.Close() +} diff --git a/darwin/msg.go b/darwin/msg.go index 9360b511..87b3c5a1 100644 --- a/darwin/msg.go +++ b/darwin/msg.go @@ -12,7 +12,12 @@ func (m msg) args() xpc.Dict { return xpc.Dict(m).MustGetDict("kCBMsgArgs") } func (m msg) advertisementData() xpc.Dict { return xpc.Dict(m).MustGetDict("kCBMsgArgAdvertisementData") } -func (m msg) attMTU() int { return xpc.Dict(m).MustGetInt("kCBMsgArgATTMTU") } + +const macOSXDefaultMTU = 23 + +// Uses GetInt as oppose to MustGetInt due to OSX not supporting 'kCBMsgArgATTMTU'. +// Issue #29 +func (m msg) attMTU() int { return xpc.Dict(m).GetInt("kCBMsgArgATTMTU", macOSXDefaultMTU) } func (m msg) attWrites() xpc.Array { return xpc.Dict(m).MustGetArray("kCBMsgArgATTWrites") } func (m msg) attributeID() int { return xpc.Dict(m).MustGetInt("kCBMsgArgAttributeID") } func (m msg) characteristicHandle() int { diff --git a/darwin/option.go b/darwin/option.go index 924d1d5e..ea31fc46 100644 --- a/darwin/option.go +++ b/darwin/option.go @@ -1,20 +1,59 @@ package darwin -// An Option is a configuration function, which configures the device. -type Option func(*Device) error - -// OptPeripheralRole configures the device to perform Peripheral tasks. -func OptPeripheralRole() Option { - return func(d *Device) error { - d.role = 1 - return nil - } -} - -// OptCentralRole configures the device to perform Central tasks. -func OptCentralRole() Option { - return func(d *Device) error { - d.role = 0 - return nil - } +import ( + "errors" + "time" + + "github.com/go-ble/ble/linux/hci/cmd" + "github.com/go-ble/ble/linux/hci/evt" +) + +// SetConnectedHandler sets handler to be called when new connection is established. +func (d *Device) SetConnectedHandler(f func(evt.LEConnectionComplete)) error { + return errors.New("Not supported") +} + +// SetDisconnectedHandler sets handler to be called on disconnect. +func (d *Device) SetDisconnectedHandler(f func(evt.DisconnectionComplete)) error { + return errors.New("Not supported") +} + +// SetPeripheralRole configures the device to perform Peripheral tasks. +func (d *Device) SetPeripheralRole() error { + return nil +} + +// SetCentralRole configures the device to perform Central tasks. +func (d *Device) SetCentralRole() error { + return nil +} + +// SetDeviceID sets HCI device ID. +func (d *Device) SetDeviceID(id int) error { + return errors.New("Not supported") +} + +// SetDialerTimeout sets dialing timeout for Dialer. +func (d *Device) SetDialerTimeout(dur time.Duration) error { + return errors.New("Not supported") +} + +// SetListenerTimeout sets dialing timeout for Listener. +func (d *Device) SetListenerTimeout(dur time.Duration) error { + return errors.New("Not supported") +} + +// SetConnParams overrides default connection parameters. +func (d *Device) SetConnParams(param cmd.LECreateConnection) error { + return errors.New("Not supported") +} + +// SetScanParams overrides default scanning parameters. +func (d *Device) SetScanParams(param cmd.LESetScanParameters) error { + return errors.New("Not supported") +} + +// SetAdvParams overrides default advertising parameters. +func (d *Device) SetAdvParams(param cmd.LESetAdvertisingParameters) error { + return errors.New("Not supported") } diff --git a/darwin/pmgprdlg.go b/darwin/pmgprdlg.go new file mode 100644 index 00000000..cd8d1211 --- /dev/null +++ b/darwin/pmgprdlg.go @@ -0,0 +1,136 @@ +// pmgrdlg.go: Implements the PeripheralManagerDelegate interface. +// CoreBluetooth communicates events asynchronously via callbacks. This file +// implements a synchronous interface by translating these callbacks into +// channel operations. + +package darwin + +import ( + "bytes" + "fmt" + "log" + + "github.com/go-ble/ble" + "github.com/JuulLabs-OSS/cbgo" +) + +func (d *Device) PeripheralManagerDidUpdateState(pmgr cbgo.PeripheralManager) { + d.evl.stateChanged.RxSignal(struct{}{}) +} + +func (d *Device) DidAddService(pmgr cbgo.PeripheralManager, svc cbgo.Service, err error) { + d.evl.svcAdded.RxSignal(err) +} + +func (d *Device) DidStartAdvertising(pmgr cbgo.PeripheralManager, err error) { + d.evl.advStarted.RxSignal(err) +} + +func (d *Device) DidReceiveReadRequest(pmgr cbgo.PeripheralManager, cbreq cbgo.ATTRequest) { + chr, _ := d.pc.findChr(cbreq.Characteristic()) + if chr == nil || chr.ReadHandler == nil { + return + } + + c := d.findConn(cbreq.Central().Identifier()) + if c == nil { + var err error + c, err = newPeripheralConn(d, cbreq.Central()) + if err != nil { + log.Printf("failed to process read response: %v", err) + return + } + } + + req := ble.NewRequest(c, nil, cbreq.Offset()) + buf := bytes.NewBuffer(make([]byte, 0, c.txMTU-1)) + rsp := ble.NewResponseWriter(buf) + chr.ReadHandler.ServeRead(req, rsp) + cbreq.SetValue(buf.Bytes()) + + pmgr.RespondToRequest(cbreq, cbgo.ATTError(rsp.Status())) +} + +func (d *Device) DidReceiveWriteRequests(pmgr cbgo.PeripheralManager, cbreqs []cbgo.ATTRequest) { + serveOne := func(cbreq cbgo.ATTRequest) { + chr, _ := d.pc.findChr(cbreq.Characteristic()) + if chr == nil || chr.WriteHandler == nil { + return + } + + c := d.findConn(cbreq.Central().Identifier()) + if c == nil { + var err error + c, err = newPeripheralConn(d, cbreq.Central()) + if err != nil { + log.Printf("failed to process write response: %v", err) + return + } + } + + req := ble.NewRequest(c, cbreq.Value(), cbreq.Offset()) + rsp := ble.NewResponseWriter(nil) + chr.WriteHandler.ServeWrite(req, rsp) + + pmgr.RespondToRequest(cbreq, cbgo.ATTError(rsp.Status())) + } + + for _, cbreq := range cbreqs { + serveOne(cbreq) + } +} + +func (d *Device) CentralDidSubscribe(pmgr cbgo.PeripheralManager, cent cbgo.Central, cbchr cbgo.Characteristic) { + c := d.findConn(cent.Identifier()) + if c == nil { + var err error + c, err = newPeripheralConn(d, cent) + if err != nil { + log.Printf("failed to process subscribe request: %v", err) + return + } + } + + if c.notifiers[cbchr] != nil { + return + } + + chr, _ := d.pc.findChr(cbchr) + if chr == nil { + return + } + + send := func(b []byte) (int, error) { + sent := d.pm.UpdateValue(b, cbchr, nil) + if !sent { + return len(b), fmt.Errorf("failed to send notification: tx queue full") + } + + return len(b), nil + } + n := ble.NewNotifier(send) + c.notifiers[cbchr] = n + req := ble.NewRequest(c, nil, 0) // convey *conn to user handler. + + go chr.NotifyHandler.ServeNotify(req, n) +} + +func (d *Device) CentralDidUnsubscribe(pmgr cbgo.PeripheralManager, cent cbgo.Central, chr cbgo.Characteristic) { + c := d.findConn(cent.Identifier()) + if c == nil { + var err error + c, err = newPeripheralConn(d, cent) + if err != nil { + log.Printf("failed to process unsubscribe request: %v", err) + return + } + } + + n := c.notifiers[chr] + if n != nil { + if err := n.Close(); err != nil { + log.Printf("failed to close notifier: %v", err) + } + delete(c.notifiers, chr) + } +} diff --git a/darwin/profcache.go b/darwin/profcache.go new file mode 100644 index 00000000..191b3320 --- /dev/null +++ b/darwin/profcache.go @@ -0,0 +1,133 @@ +package darwin + +// profcache: Profile Cache. This allows a device to match profile objects +// with their corresponding CoreBluetooth objects (e.g., ble.Servive <-> +// cbgo.Service). + +import ( + "fmt" + "sync" + + "github.com/go-ble/ble" + "github.com/JuulLabs-OSS/cbgo" +) + +type profCache struct { + mtx sync.RWMutex + + svcCbMap map[*ble.Service]cbgo.Service + chrCbMap map[*ble.Characteristic]cbgo.Characteristic + dscCbMap map[*ble.Descriptor]cbgo.Descriptor + + cbSvcMap map[cbgo.Service]*ble.Service + cbChrMap map[cbgo.Characteristic]*ble.Characteristic + cbDscMap map[cbgo.Descriptor]*ble.Descriptor +} + +func newProfCache() profCache { + return profCache{ + svcCbMap: map[*ble.Service]cbgo.Service{}, + chrCbMap: map[*ble.Characteristic]cbgo.Characteristic{}, + dscCbMap: map[*ble.Descriptor]cbgo.Descriptor{}, + + cbSvcMap: map[cbgo.Service]*ble.Service{}, + cbChrMap: map[cbgo.Characteristic]*ble.Characteristic{}, + cbDscMap: map[cbgo.Descriptor]*ble.Descriptor{}, + } +} + +func (pc *profCache) addSvc(s *ble.Service, cbs cbgo.Service) { + pc.mtx.Lock() + defer pc.mtx.Unlock() + + pc.svcCbMap[s] = cbs + pc.cbSvcMap[cbs] = s +} + +func (pc *profCache) addChr(c *ble.Characteristic, cbc cbgo.Characteristic) { + pc.mtx.Lock() + defer pc.mtx.Unlock() + + pc.chrCbMap[c] = cbc + pc.cbChrMap[cbc] = c +} + +func (pc *profCache) addDsc(d *ble.Descriptor, cbd cbgo.Descriptor) { + pc.mtx.Lock() + defer pc.mtx.Unlock() + + pc.dscCbMap[d] = cbd + pc.cbDscMap[cbd] = d +} + +func (pc *profCache) findCbSvc(s *ble.Service) (cbgo.Service, error) { + pc.mtx.RLock() + defer pc.mtx.RUnlock() + + cbs, ok := pc.svcCbMap[s] + if !ok { + return cbs, fmt.Errorf("no CB service with UUID=%v", s.UUID) + } + + return cbs, nil +} + +func (pc *profCache) findSvc(cbs cbgo.Service) (*ble.Service, error) { + pc.mtx.RLock() + defer pc.mtx.RUnlock() + + s, ok := pc.cbSvcMap[cbs] + if !ok { + return nil, fmt.Errorf("no service with UUID=%v", cbs.UUID()) + } + + return s, nil +} + +func (pc *profCache) findCbChr(c *ble.Characteristic) (cbgo.Characteristic, error) { + pc.mtx.RLock() + defer pc.mtx.RUnlock() + + cbc, ok := pc.chrCbMap[c] + if !ok { + return cbc, fmt.Errorf("no CB characteristic with UUID=%v", c.UUID) + } + + return cbc, nil +} + +func (pc *profCache) findChr(cbc cbgo.Characteristic) (*ble.Characteristic, error) { + pc.mtx.RLock() + defer pc.mtx.RUnlock() + + c, ok := pc.cbChrMap[cbc] + if !ok { + return nil, fmt.Errorf("no characteristic with UUID=%v", cbc.UUID()) + } + + return c, nil +} + +func (pc *profCache) findCbDsc(d *ble.Descriptor) (cbgo.Descriptor, error) { + pc.mtx.RLock() + defer pc.mtx.RUnlock() + + cbd, ok := pc.dscCbMap[d] + if !ok { + return cbd, fmt.Errorf("no CB descriptor with UUID=%v", d.UUID) + } + + return cbd, nil +} + +func (pc *profCache) findDsc(cbd cbgo.Descriptor) (*ble.Descriptor, error) { + pc.mtx.RLock() + defer pc.mtx.RUnlock() + + d, ok := pc.cbDscMap[cbd] + if !ok { + return nil, fmt.Errorf("no descriptor with UUID=%v", cbd.UUID()) + } + + return d, nil +} diff --git a/darwin/util.go b/darwin/util.go index 1df220c5..6b81f03a 100644 --- a/darwin/util.go +++ b/darwin/util.go @@ -1,6 +1,9 @@ package darwin -import "github.com/go-ble/ble" +import ( + "github.com/go-ble/ble" + "github.com/JuulLabs-OSS/cbgo" +) func uuidSlice(uu []ble.UUID) [][]byte { us := [][]byte{} @@ -9,3 +12,22 @@ func uuidSlice(uu []ble.UUID) [][]byte { } return us } + +func uuidStrWithDashes(s string) string { + if len(s) != 32 { + return s + } + + // 01234567-89ab-cdef-0123-456789abcdef + return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:] +} + +func uuidsToCbgoUUIDs(uuids []ble.UUID) []cbgo.UUID { + var cbuuids []cbgo.UUID + + for _, u := range uuids { + cbuuids = append(cbuuids, cbgo.UUID(u)) + } + + return cbuuids +} diff --git a/darwin/xpcid.go b/darwin/xpcid.go deleted file mode 100644 index c4d677b4..00000000 --- a/darwin/xpcid.go +++ /dev/null @@ -1,173 +0,0 @@ -package darwin - -import ( - "github.com/raff/goble/xpc" -) - -// xpc command IDs are OS X version specific, so we will use a map -// to be able to handle arbitrary versions -var ( - cmdInit, - cmdAdvertiseStart, - cmdAdvertiseStop, - cmdScanningStart, - cmdScanningStop, - cmdServicesAdd, - cmdServicesRemove, - cmdSendData, - cmdSubscribed, - cmdConnect, - cmdDisconnect, - cmdReadRSSI, - cmdDiscoverServices, - cmdDiscoverIncludedServices, - cmdDiscoverCharacteristics, - cmdReadCharacteristic, - cmdWriteCharacteristic, - cmdSubscribeCharacteristic, - cmdDiscoverDescriptors, - cmdReadDescriptor, - cmdWriteDescriptor, - evtStateChanged, - evtAdvertisingStarted, - evtAdvertisingStopped, - evtServiceAdded, - evtReadRequest, - evtWriteRequest, - evtSubscribe, - evtUnsubscribe, - evtConfirmation, - evtPeripheralDiscovered, - evtPeripheralConnected, - evtPeripheralDisconnected, - evtATTMTU, - evtRSSIRead, - evtServiceDiscovered, - evtIncludedServicesDiscovered, - evtCharacteristicsDiscovered, - evtCharacteristicRead, - evtCharacteristicWritten, - evtNotificationValueSet, - evtDescriptorsDiscovered, - evtDescriptorRead, - evtDescriptorWritten, - evtSlaveConnectionComplete, - evtMasterConnectionComplete int -) - -var serviceID string - -func initXpcIDs() error { - var utsname xpc.Utsname - err := xpc.Uname(&utsname) - if err != nil { - return err - } - - cmdInit = 1 - - if utsname.Release < "17." { - // yosemite - cmdAdvertiseStart = 8 - cmdAdvertiseStop = 9 - cmdServicesAdd = 10 - cmdServicesRemove = 12 - - cmdSendData = 13 - cmdSubscribed = 15 - cmdScanningStart = 29 - cmdScanningStop = 30 - cmdConnect = 31 - cmdDisconnect = 32 - cmdReadRSSI = 44 - cmdDiscoverServices = 45 - cmdDiscoverIncludedServices = 60 - cmdDiscoverCharacteristics = 62 - cmdReadCharacteristic = 65 - cmdWriteCharacteristic = 66 - cmdSubscribeCharacteristic = 68 - cmdDiscoverDescriptors = 70 - cmdReadDescriptor = 77 - cmdWriteDescriptor = 78 - - evtStateChanged = 6 - evtAdvertisingStarted = 16 - evtAdvertisingStopped = 17 - evtServiceAdded = 18 - evtReadRequest = 19 - evtWriteRequest = 20 - evtSubscribe = 21 - evtUnsubscribe = 22 - evtConfirmation = 23 - evtPeripheralDiscovered = 37 - evtPeripheralConnected = 38 - evtPeripheralDisconnected = 40 - evtATTMTU = 53 - evtRSSIRead = 55 - evtServiceDiscovered = 56 - evtIncludedServicesDiscovered = 63 - evtCharacteristicsDiscovered = 64 - evtCharacteristicRead = 71 - evtCharacteristicWritten = 72 - evtNotificationValueSet = 74 - evtDescriptorsDiscovered = 76 - evtDescriptorRead = 79 - evtDescriptorWritten = 80 - evtSlaveConnectionComplete = 81 - evtMasterConnectionComplete = 82 - - serviceID = "com.apple.blued" - } else { - // high sierra - cmdSendData = 21 - cmdSubscribed = 22 - cmdAdvertiseStart = 16 - cmdAdvertiseStop = 17 - cmdServicesAdd = 18 - cmdServicesRemove = 19 - cmdScanningStart = 44 - cmdScanningStop = 45 - cmdConnect = 46 - cmdDisconnect = 47 - cmdReadRSSI = 61 - cmdDiscoverServices = 62 - cmdDiscoverIncludedServices = 74 - cmdDiscoverCharacteristics = 75 - cmdReadCharacteristic = 78 - cmdWriteCharacteristic = 79 - cmdSubscribeCharacteristic = 81 - cmdDiscoverDescriptors = 82 - cmdReadDescriptor = 88 - cmdWriteDescriptor = 89 - - evtStateChanged = 4 - evtPeripheralDiscovered = 48 - evtPeripheralConnected = 49 - evtPeripheralDisconnected = 50 - evtRSSIRead = 71 - evtServiceDiscovered = 72 - evtCharacteristicsDiscovered = 77 - evtCharacteristicRead = 83 - evtCharacteristicWritten = 84 - evtNotificationValueSet = 86 - evtDescriptorsDiscovered = 87 - evtDescriptorRead = 90 - evtDescriptorWritten = 91 - evtAdvertisingStarted = 27 - evtAdvertisingStopped = 28 - evtServiceAdded = 29 - evtReadRequest = 30 - evtWriteRequest = 31 - evtSubscribe = 32 - evtUnsubscribe = 33 - evtConfirmation = 34 - evtATTMTU = 57 - evtSlaveConnectionComplete = 60 // should be called params update - evtMasterConnectionComplete = 59 //not confident - evtIncludedServicesDiscovered = 76 - - serviceID = "com.apple.bluetoothd" - } - - return nil -} diff --git a/examples/basic/rssi/main.go b/examples/basic/rssi/main.go new file mode 100644 index 00000000..3d70293d --- /dev/null +++ b/examples/basic/rssi/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "strings" + "time" + + "github.com/go-ble/ble" + "github.com/go-ble/ble/examples/lib/dev" +) + +func main() { + macAddr := flag.String("addr", "", "peripheral MAC address") + flag.Parse() + hciDevice, err := dev.NewDevice("default") + if err != nil { + panic(err) + } + ble.SetDefaultDevice(hciDevice) + + filter := func(a ble.Advertisement) bool { + return strings.ToUpper(a.Addr().String()) == strings.ToUpper(*macAddr) + } + + // Scan for device + log.Printf("Scanning for %s\n", *macAddr) + ctx := ble.WithSigHandler(context.WithTimeout(context.Background(), time.Second*300)) + client, err := ble.Connect(ctx, filter) + if err != nil { + panic(err) + } + + for { + fmt.Printf("Client side RSSI: %d\n", client.ReadRSSI()) + time.Sleep(time.Second) + } + +} \ No newline at end of file diff --git a/examples/blesh/lnx.go b/examples/blesh/lnx.go index 9752ea12..e1d2cb0f 100644 --- a/examples/blesh/lnx.go +++ b/examples/blesh/lnx.go @@ -1,8 +1,8 @@ package main import ( + "github.com/go-ble/ble" "github.com/go-ble/ble/linux" - "github.com/go-ble/ble/linux/hci" "github.com/go-ble/ble/linux/hci/cmd" "github.com/pkg/errors" ) @@ -31,7 +31,7 @@ func updateLinuxParam(d *linux.Device) error { return errors.Wrap(err, "can't set scan param") } - if err := d.HCI.Option(hci.OptConnParams( + if err := d.HCI.Option(ble.OptConnParams( cmd.LECreateConnection{ LEScanInterval: 0x0004, // 0x0004 - 0x4000; N * 0.625 msec LEScanWindow: 0x0004, // 0x0004 - 0x4000; N * 0.625 msec diff --git a/examples/lib/dev/default_darwin.go b/examples/lib/dev/default_darwin.go index 3ca967f9..dd46f76f 100644 --- a/examples/lib/dev/default_darwin.go +++ b/examples/lib/dev/default_darwin.go @@ -6,6 +6,6 @@ import ( ) // DefaultDevice ... -func DefaultDevice() (d ble.Device, err error) { - return darwin.NewDevice() +func DefaultDevice(opts ...ble.Option) (d ble.Device, err error) { + return darwin.NewDevice(opts...) } diff --git a/examples/lib/dev/default_linux.go b/examples/lib/dev/default_linux.go index c01e9bd9..eca7bf60 100644 --- a/examples/lib/dev/default_linux.go +++ b/examples/lib/dev/default_linux.go @@ -6,6 +6,6 @@ import ( ) // DefaultDevice ... -func DefaultDevice() (d ble.Device, err error) { - return linux.NewDevice() +func DefaultDevice(opts ...ble.Option) (d ble.Device, err error) { + return linux.NewDevice(opts...) } diff --git a/examples/lib/dev/dev.go b/examples/lib/dev/dev.go index e449a6a6..cd197e19 100644 --- a/examples/lib/dev/dev.go +++ b/examples/lib/dev/dev.go @@ -1,8 +1,10 @@ package dev -import "github.com/go-ble/ble" +import ( + "github.com/go-ble/ble" +) // NewDevice ... -func NewDevice(impl string) (d ble.Device, err error) { - return DefaultDevice() +func NewDevice(impl string, opts ...ble.Option) (d ble.Device, err error) { + return DefaultDevice(opts...) } diff --git a/gatt.go b/gatt.go index ae8fcc00..dfb76d12 100644 --- a/gatt.go +++ b/gatt.go @@ -133,7 +133,7 @@ func Connect(ctx context.Context, f AdvFilter) (Client, error) { } }() - ch := make(chan Advertisement) + ch := make(chan Advertisement,1) fn := func(a Advertisement) { cancel() ch <- a diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..6dc36f90 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/go-ble/ble + +go 1.13 + +require ( + github.com/JuulLabs-OSS/cbgo v0.0.1 + github.com/mattn/go-colorable v0.1.6 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab + github.com/pkg/errors v0.8.1 + github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99 + github.com/stretchr/testify v1.4.0 // indirect + github.com/urfave/cli v1.22.2 + golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..67e53277 --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/JuulLabs-OSS/cbgo v0.0.1 h1:A5JdglvFot1J9qYR0POZ4qInttpsVPN9lqatjaPp2ro= +github.com/JuulLabs-OSS/cbgo v0.0.1/go.mod h1:L4YtGP+gnyD84w7+jN66ncspFRfOYB5aj9QSXaFHmBA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab h1:n8cgpHzJ5+EDyDri2s/GC7a9+qK3/YEGnBsd0uS/8PY= +github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab/go.mod h1:y1pL58r5z2VvAjeG1VLGc8zOQgSOzbKN7kMHPvFXJ+8= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.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/raff/goble v0.0.0-20190909174656-72afc67d6a99 h1:JtoVdxWJ3tgyqtnPq3r4hJ9aULcIDDnPXBWxZsdmqWU= +github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99/go.mod h1:CxaUhijgLFX0AROtH5mluSY71VqpjQBw9JXE2UKZmc4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= +github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191126131656-8a8471f7e56d h1:kCXqdOO2GMlu0vCsEMBXwj/b0E9wyFpNPBpuv/go/F8= +golang.org/x/sys v0.0.0-20191126131656-8a8471f7e56d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/linux/adv/packet.go b/linux/adv/packet.go index 6ffc7535..f15e6159 100644 --- a/linux/adv/packet.go +++ b/linux/adv/packet.go @@ -185,6 +185,47 @@ func (p *Packet) Field(typ byte) []byte { return nil } +func (p *Packet) getUUIDsByType(typ byte, u []ble.UUID, w int) []ble.UUID { + pos := 0 + var b []byte + for pos < len(p.b) { + if b, pos = p.fieldPos(typ, pos); b != nil { + u = uuidList(u, b, w) + } + } + return u +} + +func (p *Packet) fieldPos(typ byte, offset int) ([]byte, int) { + if offset >= len(p.b) { + return nil, len(p.b) + } + + b := p.b[offset:] + pos := offset + + if len(b) < 2 { + return nil, pos + len(b) + } + + for len(b) > 0 { + l, t := b[0], b[1] + if int(l) < 1 || len(b) < int(1+l) { + return nil, pos + } + if t == typ { + r := b[2 : 2+l-1] + return r, pos + 1 + int(l) + } + b = b[1+l:] + pos += 1 + int(l) + if len(b) < 2 { + break + } + } + return nil, pos +} + // Flags returns the flags of the packet. func (p *Packet) Flags() (flags byte, present bool) { b := p.Field(flags) @@ -214,24 +255,12 @@ func (p *Packet) TxPower() (power int, present bool) { // UUIDs returns a list of service UUIDs. func (p *Packet) UUIDs() []ble.UUID { var u []ble.UUID - if b := p.Field(someUUID16); b != nil { - u = uuidList(u, b, 2) - } - if b := p.Field(allUUID16); b != nil { - u = uuidList(u, b, 2) - } - if b := p.Field(someUUID32); b != nil { - u = uuidList(u, b, 4) - } - if b := p.Field(allUUID32); b != nil { - u = uuidList(u, b, 4) - } - if b := p.Field(someUUID128); b != nil { - u = uuidList(u, b, 16) - } - if b := p.Field(allUUID128); b != nil { - u = uuidList(u, b, 16) - } + u = p.getUUIDsByType(someUUID16, u, 2) + u = p.getUUIDsByType(allUUID16, u, 2) + u = p.getUUIDsByType(someUUID32, u, 4) + u = p.getUUIDsByType(allUUID32, u, 4) + u = p.getUUIDsByType(someUUID128, u, 16) + u = p.getUUIDsByType(allUUID128, u, 16) return u } diff --git a/linux/att/client.go b/linux/att/client.go index f6fe88ef..ad624020 100644 --- a/linux/att/client.go +++ b/linux/att/client.go @@ -466,7 +466,7 @@ func (c *Client) ExecuteWrite(flags uint8) error { switch { case rsp[0] == ErrorResponseCode && len(rsp) == 5: return ble.ATTError(rsp[4]) - case rsp[0] == ErrorResponseCode && len(rsp) == 5: + case rsp[0] == ErrorResponseCode && len(rsp) != 5: fallthrough case rsp[0] != rsp.AttributeOpcode(): return ErrInvalidResponse @@ -538,6 +538,18 @@ func (c *Client) Loop() { b := make([]byte, n) copy(b, c.rxBuf) + // TODO: better request identification + if b[0] == ExchangeMTURequestCode { + // Schedule this to be taken care of + select { + case ch <- asyncWork{handle: c.handleRequest, data: b}: + default: + // If this really happens, especially on a slow machine, enlarge the channel buffer. + _ = logger.Error("client", "req", "can't enqueue incoming request.") + } + continue + } + if (b[0] != HandleValueNotificationCode) && (b[0] != HandleValueIndicationCode) { c.rspc <- b continue @@ -558,3 +570,62 @@ func (c *Client) Loop() { } } } + +func (c *Client) handleRequest(b []byte) { + switch b[0] { + case ExchangeMTURequestCode: + resp := c.handleExchangeMTURequest(b) + if len(resp) != 0 { + err := c.sendCmd(resp) + if err != nil { + _ = logger.Error("client", "req", fmt.Sprintf("error sending MTU response: %s", err.Error())) + } + } + default: + errRsp := newErrorResponse(b[0], 0x0000, ble.ErrReqNotSupp) + _ = c.sendCmd(errRsp) + _ = logger.Warn("client", "req", fmt.Sprintf("Received unhandled request [0x%X]", b)) + } +} + +// handle MTU Exchange request. [Vol 3, Part F, 3.4.2] +// ExchangeMTU informs the server of the client’s maximum receive MTU size and +// request the server to respond with its maximum receive MTU size. [Vol 3, Part F, 3.4.2.1] +func (c *Client) handleExchangeMTURequest(r ExchangeMTURequest) []byte { + // Acquire and reuse the txBuf, and release it after usage. + // The same txBuf, or a newly allocate one, if the txMTU is changed, + // will be released back to the channel. + + // We do this first to prevent races with ExchangeMTURequest + txBuf := <-c.chTxBuf + + // Validate the request. + switch { + case len(r) != 3: + fallthrough + case r.ClientRxMTU() < 23: + return newErrorResponse(r.AttributeOpcode(), 0x0000, ble.ErrInvalidPDU) + } + + txMTU := int(r.ClientRxMTU()) + // Our rxMTU for the response + rxMTU := c.l2c.RxMTU() + + // Update transmit MTU to max supported by the other side + logger.Debug("client", "req", fmt.Sprintf("server requested an MTU change to TX:%d RX:%d", txMTU, rxMTU)) + c.l2c.SetTxMTU(txMTU) + + defer func() { + // Update the tx buffer if needed + if len(txBuf) != txMTU { + c.chTxBuf <- make([]byte, txMTU, txMTU) + } else { + c.chTxBuf <- txBuf + } + }() + + rsp := ExchangeMTUResponse(txBuf) + rsp.SetAttributeOpcode() + rsp.SetServerRxMTU(uint16(rxMTU)) + return rsp[:3] +} diff --git a/linux/att/db.go b/linux/att/db.go index 93585a2f..bee9750a 100644 --- a/linux/att/db.go +++ b/linux/att/db.go @@ -25,7 +25,7 @@ func (r *DB) idx(h int) int { if h < int(r.base) { return tooSmall } - if int(h) >= int(r.base)+len(r.attrs) { + if h >= int(r.base)+len(r.attrs) { return tooLarge } return h - int(r.base) diff --git a/linux/att/server.go b/linux/att/server.go index 5056ebca..0ff0703d 100644 --- a/linux/att/server.go +++ b/linux/att/server.go @@ -302,7 +302,7 @@ func (s *Server) handleFindByTypeValueRequest(r FindByTypeValueRequest) []byte { for _, a := range s.db.subrange(r.StartingHandle(), r.EndingHandle()) { v, starth, endh := a.v, a.h, a.endh - if !(ble.UUID(a.typ).Equal(ble.UUID16(r.AttributeType()))) { + if !a.typ.Equal(ble.UUID16(r.AttributeType())) { continue } if v == nil { diff --git a/linux/device.go b/linux/device.go index 521eb9f6..1a1ec026 100644 --- a/linux/device.go +++ b/linux/device.go @@ -13,32 +13,35 @@ import ( ) // NewDevice returns the default HCI device. -func NewDevice() (*Device, error) { - return NewDeviceWithName("Gopher") +func NewDevice(opts ...ble.Option) (*Device, error) { + return NewDeviceWithName("Gopher", opts...) } // NewDeviceWithName returns the default HCI device. -func NewDeviceWithName(name string) (*Device, error) { - return NewDeviceWithNameAndHandler(name, nil) +func NewDeviceWithName(name string, opts ...ble.Option) (*Device, error) { + return NewDeviceWithNameAndHandler(name, nil, opts...) } -func NewDeviceWithNameAndHandler(name string, handler ble.NotifyHandler) (*Device, error) { - dev, err := hci.NewHCI() +func NewDeviceWithNameAndHandler(name string, handler ble.NotifyHandler, opts ...ble.Option) (*Device, error) { + dev, err := hci.NewHCI(opts...) if err != nil { return nil, errors.Wrap(err, "can't create hci") } if err = dev.Init(); err != nil { + dev.Close() return nil, errors.Wrap(err, "can't init hci") } srv, err := gatt.NewServerWithNameAndHandler(name, handler) if err != nil { + dev.Close() return nil, errors.Wrap(err, "can't create server") } // mtu := ble.DefaultMTU mtu := ble.MaxMTU // TODO: get this from user using Option. if mtu > ble.MaxMTU { + dev.Close() return nil, errors.Wrapf(err, "maximum ATT_MTU is %d", ble.MaxMTU) } diff --git a/linux/gatt/client.go b/linux/gatt/client.go index 3ad108e1..ef29ff56 100644 --- a/linux/gatt/client.go +++ b/linux/gatt/client.go @@ -208,7 +208,13 @@ func (p *Client) DiscoverDescriptors(filter []ble.UUID, c *ble.Characteristic) ( func (p *Client) ReadCharacteristic(c *ble.Characteristic) ([]byte, error) { p.Lock() defer p.Unlock() - return p.ac.Read(c.ValueHandle) + val, err := p.ac.Read(c.ValueHandle) + if err != nil { + return nil, err + } + + c.Value = val + return val, nil } // ReadLongCharacteristic reads a characteristic value which is longer than the MTU. [Vol 3, Part G, 4.8.3] @@ -231,6 +237,8 @@ func (p *Client) ReadLongCharacteristic(c *ble.Characteristic) ([]byte, error) { } buffer = append(buffer, read...) } + + c.Value = buffer return buffer, nil } @@ -248,7 +256,13 @@ func (p *Client) WriteCharacteristic(c *ble.Characteristic, v []byte, noRsp bool func (p *Client) ReadDescriptor(d *ble.Descriptor) ([]byte, error) { p.Lock() defer p.Unlock() - return p.ac.Read(d.Handle) + val, err := p.ac.Read(d.Handle) + if err != nil { + return nil, err + } + + d.Value = val + return val, nil } // WriteDescriptor writes a characteristic descriptor to a server. [Vol 3, Part G, 4.12.3] @@ -262,8 +276,7 @@ func (p *Client) WriteDescriptor(d *ble.Descriptor, v []byte) error { func (p *Client) ReadRSSI() int { p.Lock() defer p.Unlock() - // TODO: - return 0 + return p.conn.ReadRSSI() } // ExchangeMTU informs the server of the client’s maximum receive MTU size and @@ -357,6 +370,11 @@ func (p *Client) Disconnected() <-chan struct{} { return p.conn.Disconnected() } +// Conn returns the client's current connection. +func (p *Client) Conn() ble.Conn { + return p.conn +} + // HandleNotification ... func (p *Client) HandleNotification(req []byte) { p.Lock() diff --git a/linux/hci/conn.go b/linux/hci/conn.go index 46a46f83..24ff0c5f 100644 --- a/linux/hci/conn.go +++ b/linux/hci/conn.go @@ -33,9 +33,6 @@ type Conn struct { txMTU int rxMPS int - // leFrame is set to be true when the LE Credit based flow control is used. - leFrame bool - // Signaling MTUs are The maximum size of command information that the // L2CAP layer entity is capable of accepting. // A L2CAP implementations supporting LE-U should support at least 23 bytes. @@ -47,23 +44,25 @@ type Conn struct { sigRxMTU int sigTxMTU int - // sigID is used to match responses with signaling requests. - // The requesting device sets this field and the responding device uses the - // same value in its response. Within each signalling channel a different - // Identifier shall be used for each successive command. [Vol 3, Part A, 4] - sigID uint8 - sigSent chan []byte - smpSent chan []byte + // smpSent chan []byte chInPkt chan packet chInPDU chan pdu + chDone chan struct{} // Host to Controller Data Flow Control pkt-based Data flow control for LE-U [Vol 2, Part E, 4.1.1] // chSentBufs tracks the HCI buffer occupied by this connection. txBuffer *Client - chDone chan struct{} + // sigID is used to match responses with signaling requests. + // The requesting device sets this field and the responding device uses the + // same value in its response. Within each signalling channel a different + // Identifier shall be used for each successive command. [Vol 3, Part A, 4] + sigID uint8 + + // leFrame is set to be true when the LE Credit based flow control is used. + leFrame bool } func newConn(h *HCI, param evt.LEConnectionComplete) *Conn { @@ -94,7 +93,7 @@ func newConn(h *HCI, param evt.LEConnectionComplete) *Conn { if err != io.EOF { // TODO: wrap and pass the error up. // err := errors.Wrap(err, "recombine failed") - logger.Error("recombine failed: ", "err", err) + _ = logger.Error("recombine failed: ", "err", err) } close(c.chInPDU) return @@ -140,7 +139,7 @@ func (c *Conn) Read(sdu []byte) (n int, err error) { buf.Write(data) for buf.Len() < slen { p := <-c.chInPDU - buf.Write(pdu(p).payload()) + buf.Write(p.payload()) } return slen, nil } @@ -196,6 +195,15 @@ func (c *Conn) writePDU(pdu []byte) (int, error) { c.txBuffer.LockPool() defer c.txBuffer.UnlockPool() + // Fail immediately if the connection is already closed + // Check this with the pool locked to avoid race conditions + // with handleDisconnectionComplete + select { + case <-c.chDone: + return 0, io.ErrClosedPipe + default: + } + for len(pdu) > 0 { // Get a buffer from our pre-allocated and flow-controlled pool. pkt := c.txBuffer.Get() // ACL pkt @@ -205,10 +213,23 @@ func (c *Conn) writePDU(pdu []byte) (int, error) { } // Prepare the Headers - binary.Write(pkt, binary.LittleEndian, uint8(pktTypeACLData)) // HCI Header: pkt Type - binary.Write(pkt, binary.LittleEndian, uint16(c.param.ConnectionHandle()|(flags<<8))) // ACL Header: handle and flags - binary.Write(pkt, binary.LittleEndian, uint16(flen)) // ACL Header: data len - binary.Write(pkt, binary.LittleEndian, pdu[:flen]) // Append payload + + // HCI Header: pkt Type + if err := binary.Write(pkt, binary.LittleEndian, pktTypeACLData); err != nil { + return 0, err + } + // ACL Header: handle and flags + if err := binary.Write(pkt, binary.LittleEndian, c.param.ConnectionHandle()|(flags<<8)); err != nil { + return 0, err + } + // ACL Header: data len + if err := binary.Write(pkt, binary.LittleEndian, uint16(flen)); err != nil { + return 0, err + } + // Append payload + if err := binary.Write(pkt, binary.LittleEndian, pdu[:flen]); err != nil { + return 0, err + } // Flush the pkt to HCI select { @@ -276,6 +297,15 @@ func (c *Conn) Disconnected() <-chan struct{} { return c.chDone } +// ReadRSSI retrieves the current RSSI value of remote peripheral. [Vol 2, Part E, 7.5.4] +func (c *Conn) ReadRSSI() int { + rp := new(cmd.ReadRSSIRP) + c.hci.Send(&cmd.ReadRSSI{ + Handle: c.param.ConnectionHandle(), + }, rp) + return int(rp.RSSI) +} + // Close disconnects the connection by sending hci disconnect command to the device. func (c *Conn) Close() error { select { diff --git a/linux/hci/gap.go b/linux/hci/gap.go index d114a0a0..6bb1f1ea 100644 --- a/linux/hci/gap.go +++ b/linux/hci/gap.go @@ -209,28 +209,35 @@ func (h *HCI) Dial(ctx context.Context, a ble.Addr) (ble.Client, error) { if h.dialerTmo != time.Duration(0) { tmo = time.After(h.dialerTmo) } + select { case <-ctx.Done(): - return nil, ctx.Err() + return h.cancelDial() + case <-tmo: + return h.cancelDial() case <-h.done: return nil, h.err case c := <-h.chMasterConn: return gatt.NewClient(c) - case <-tmo: - err := h.Send(&h.params.connCancel, nil) - if err == nil { - // The pending connection was canceled successfully. - return nil, fmt.Errorf("connection timed out") - } - // The connection has been established, the cancel command - // failed with ErrDisallowed. - if err == ErrDisallowed { - return gatt.NewClient(<-h.chMasterConn) - } - return nil, errors.Wrap(err, "cancel connection failed") + } } +// cancelDial cancels the Dialing +func (h *HCI) cancelDial() (ble.Client, error) { + err := h.Send(&h.params.connCancel, nil) + if err == nil { + // The pending connection was canceled successfully. + return nil, fmt.Errorf("connection canceled") + } + // The connection has been established, the cancel command + // failed with ErrDisallowed. + if err == ErrDisallowed { + return gatt.NewClient(<-h.chMasterConn) + } + return nil, errors.Wrap(err, "cancel connection failed") +} + // Advertise starts advertising. func (h *HCI) Advertise() error { h.params.advEnable.AdvertisingEnable = 1 diff --git a/linux/hci/hci.go b/linux/hci/hci.go index 30bb4283..f59cb535 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -3,6 +3,7 @@ package hci import ( "fmt" "io" + "log" "net" "strings" "sync" @@ -35,13 +36,14 @@ type pkt struct { } // NewHCI returns a hci device. -func NewHCI(opts ...Option) (*HCI, error) { +func NewHCI(opts ...ble.Option) (*HCI, error) { h := &HCI{ id: -1, chCmdPkt: make(chan *pkt), chCmdBufs: make(chan []byte, 16), sent: make(map[int]*pkt), + muSent: &sync.Mutex{}, evth: map[int]handlerFn{}, subh: map[int]handlerFn{}, @@ -73,6 +75,7 @@ type HCI struct { // Host to Controller command flow control [Vol 2, Part E, 4.4] chCmdPkt chan *pkt chCmdBufs chan []byte + muSent *sync.Mutex sent map[int]*pkt // evtHub @@ -108,6 +111,9 @@ type HCI struct { chMasterConn chan *Conn // Dial returns master connections. chSlaveConn chan *Conn // Peripheral accept slave connections. + connectedHandler func(evt.LEConnectionComplete) + disconnectedHandler func(evt.DisconnectionComplete) + dialerTmo time.Duration listenerTmo time.Duration @@ -142,7 +148,7 @@ func (h *HCI) Init() error { } h.skt = skt - h.chCmdBufs <- make([]byte, 64) + h.setAllowedCommands(1) go h.sktLoop() if err := h.init(); err != nil { @@ -169,7 +175,7 @@ func (h *HCI) Error() error { } // Option sets the options specified. -func (h *HCI) Option(opts ...Option) error { +func (h *HCI) Option(opts ...ble.Option) error { var err error for _, opt := range opts { err = opt(h) @@ -248,19 +254,43 @@ func (h *HCI) send(c Command) ([]byte, error) { h.close(fmt.Errorf("hci: failed to marshal cmd")) } - h.sent[c.OpCode()] = p // TODO: lock + h.muSent.Lock() + h.sent[c.OpCode()] = p + h.muSent.Unlock() if n, err := h.skt.Write(b[:4+c.Len()]); err != nil { h.close(fmt.Errorf("hci: failed to send cmd")) } else if n != 4+c.Len() { h.close(fmt.Errorf("hci: failed to send whole cmd pkt to hci socket")) } + var ret []byte + var err error + + // emergency timeout to prevent calls from locking up if the HCI + // interface doesn't respond. Responsed here should normally be fast + // a timeout indicates a major problem with HCI. + timeout := time.NewTimer(10 * time.Second) select { + case <-timeout.C: + err = fmt.Errorf("hci: no response to command, hci connection failed") + ret = nil case <-h.done: - return nil, h.err + err = h.err + ret = nil case b := <-p.done: - return b, nil + err = nil + ret = b } + timeout.Stop() + + // clear sent table when done, we sometimes get command complete or + // command status messages with no matching send, which can attempt to + // access stale packets in sent and fail or lock up. + h.muSent.Lock() + delete(h.sent, c.OpCode()) + h.muSent.Unlock() + + return ret, err } func (h *HCI) sktLoop() { @@ -269,7 +299,11 @@ func (h *HCI) sktLoop() { for { n, err := h.skt.Read(b) if n == 0 || err != nil { - h.err = fmt.Errorf("skt: %s", err) + if err == io.EOF { + h.err = err //callers depend on detecting io.EOF, don't wrap it. + } else { + h.err = fmt.Errorf("skt: %s", err) + } return } p := make([]byte, n) @@ -278,10 +312,10 @@ func (h *HCI) sktLoop() { // Some bluetooth devices may append vendor specific packets at the last, // in this case, simply ignore them. if strings.HasPrefix(err.Error(), "unsupported vendor packet:") { - logger.Error("skt: %v", err) + _ = logger.Error("skt: %v", err) } else { - h.err = fmt.Errorf("skt: %v", err) - return + log.Printf("skt: %v", err) + continue } } } @@ -289,7 +323,10 @@ func (h *HCI) sktLoop() { func (h *HCI) close(err error) error { h.err = err - return h.skt.Close() + if h.skt != nil { + return h.skt.Close() + } + return err } func (h *HCI) handlePkt(b []byte) error { @@ -317,7 +354,7 @@ func (h *HCI) handleACL(b []byte) error { c, ok := h.conns[handle] h.muConns.Unlock() if !ok { - logger.Warn("invalid connection handle on ACL packet", "handle", handle) + _ = logger.Warn("invalid connection handle on ACL packet", "handle", handle) return nil } c.chInPkt <- b @@ -403,16 +440,16 @@ func (h *HCI) handleLEAdvertisingReport(b []byte) error { func (h *HCI) handleCommandComplete(b []byte) error { e := evt.CommandComplete(b) - for i := 0; i < int(e.NumHCICommandPackets()); i++ { - h.chCmdBufs <- make([]byte, 64) - } + h.setAllowedCommands(int(e.NumHCICommandPackets())) // NOP command, used for flow control purpose [Vol 2, Part E, 4.4] + // no handling other than setAllowedCommands needed if e.CommandOpcode() == 0x0000 { - h.chCmdBufs = make(chan []byte, 16) return nil } + h.muSent.Lock() p, found := h.sent[int(e.CommandOpcode())] + h.muSent.Unlock() if !found { return fmt.Errorf("can't find the cmd for CommandCompleteEP: % X", e) } @@ -422,11 +459,11 @@ func (h *HCI) handleCommandComplete(b []byte) error { func (h *HCI) handleCommandStatus(b []byte) error { e := evt.CommandStatus(b) - for i := 0; i < int(e.NumHCICommandPackets()); i++ { - h.chCmdBufs <- make([]byte, 64) - } + h.setAllowedCommands(int(e.NumHCICommandPackets())) + h.muSent.Lock() p, found := h.sent[int(e.CommandOpcode())] + h.muSent.Unlock() if !found { return fmt.Errorf("can't find the cmd for CommandStatusEP: % X", e) } @@ -436,6 +473,10 @@ func (h *HCI) handleCommandStatus(b []byte) error { func (h *HCI) handleLEConnectionComplete(b []byte) error { e := evt.LEConnectionComplete(b) + if e.Role() == roleMaster && ErrCommand(e.Status()) == ErrConnID { + // The connection was canceled successfully. + return nil + } c := newConn(h, e) h.muConns.Lock() h.conns[e.ConnectionHandle()] = c @@ -449,10 +490,6 @@ func (h *HCI) handleLEConnectionComplete(b []byte) error { } return nil } - if ErrCommand(e.Status()) == ErrConnID { - // The connection was canceled successfully. - return nil - } return nil } if e.Status() == 0x00 { @@ -468,10 +505,13 @@ func (h *HCI) handleLEConnectionComplete(b []byte) error { // So we also re-enable the advertising when a connection disconnected h.params.RLock() if h.params.advEnable.AdvertisingEnable == 1 { - go h.Send(&h.params.advEnable, nil) + go h.Send(&cmd.LESetAdvertiseEnable{AdvertisingEnable: 0}, nil) } h.params.RUnlock() } + if h.connectedHandler != nil { + h.connectedHandler(e) + } return nil } @@ -490,7 +530,7 @@ func (h *HCI) handleDisconnectionComplete(b []byte) error { } close(c.chInPkt) - if c.param.Role() == roleMaster { + if c.param.Role() == roleSlave { // Re-enable advertising, if it was advertising. Refer to the // handleLEConnectionComplete() for details. // This may failed with ErrCommandDisallowed, if the controller @@ -506,7 +546,16 @@ func (h *HCI) handleDisconnectionComplete(b []byte) error { } // When a connection disconnects, all the sent packets and weren't acked yet // will be recycled. [Vol2, Part E 4.1.1] + // + // must be done with the pool locked to avoid race conditions where + // writePDU is in progress and does a Get from the pool after this completes, + // leaking a buffer from the main pool. + c.txBuffer.LockPool() c.txBuffer.PutAll() + c.txBuffer.UnlockPool() + if h.disconnectedHandler != nil { + h.disconnectedHandler(e) + } return nil } @@ -534,3 +583,17 @@ func (h *HCI) handleLELongTermKeyRequest(b []byte) error { ConnectionHandle: e.ConnectionHandle(), }, nil) } + +func (h *HCI) setAllowedCommands(n int) { + + //hard-coded limit to command queue depth + //matches make(chan []byte, 16) in NewHCI + // TODO make this a constant, decide correct size + if n > 16 { + n = 16 + } + + for len(h.chCmdBufs) < n { + h.chCmdBufs <- make([]byte, 64) // TODO make buffer size a constant + } +} diff --git a/linux/hci/option.go b/linux/hci/option.go index b0f4832b..61493dd3 100644 --- a/linux/hci/option.go +++ b/linux/hci/option.go @@ -1,42 +1,67 @@ package hci import ( + "errors" + "github.com/go-ble/ble/linux/hci/evt" "time" "github.com/go-ble/ble/linux/hci/cmd" ) -// An Option is a configuration function, which configures the device. -type Option func(*HCI) error +// SetDeviceID sets HCI device ID. +func (h *HCI) SetDeviceID(id int) error { + h.id = id + return nil +} + +// SetDialerTimeout sets dialing timeout for Dialer. +func (h *HCI) SetDialerTimeout(d time.Duration) error { + h.dialerTmo = d + return nil +} + +// SetListenerTimeout sets dialing timeout for Listener. +func (h *HCI) SetListenerTimeout(d time.Duration) error { + h.listenerTmo = d + return nil +} + +// SetConnParams overrides default connection parameters. +func (h *HCI) SetConnParams(param cmd.LECreateConnection) error { + h.params.connParams = param + return nil +} + +// SetScanParams overrides default scanning parameters. +func (h *HCI) SetScanParams(param cmd.LESetScanParameters) error { + h.params.scanParams = param + return nil +} + +// SetConnectedHandler sets handler to be called when new connection is established. +func (h *HCI) SetConnectedHandler(f func(complete evt.LEConnectionComplete)) error { + h.connectedHandler = f + return nil +} -// OptDeviceID sets HCI device ID. -func OptDeviceID(id int) Option { - return func(h *HCI) error { - h.id = id - return nil - } +// SetDisconnectedHandler sets handler to be called on disconnect. +func (h *HCI) SetDisconnectedHandler(f func(evt.DisconnectionComplete)) error { + h.disconnectedHandler = f + return nil } -// OptDialerTimeout sets dialing timeout for Dialer. -func OptDialerTimeout(d time.Duration) Option { - return func(h *HCI) error { - h.dialerTmo = d - return nil - } +// SetAdvParams overrides default advertising parameters. +func (h *HCI) SetAdvParams(param cmd.LESetAdvertisingParameters) error { + h.params.advParams = param + return nil } -// OptListenerTimeout sets dialing timeout for Listener. -func OptListenerTimeout(d time.Duration) Option { - return func(h *HCI) error { - h.listenerTmo = d - return nil - } +// SetPeripheralRole is not supported +func (h *HCI) SetPeripheralRole() error { + return errors.New("Not supported") } -// OptConnParams overrides default connection parameters. -func OptConnParams(param cmd.LECreateConnection) Option { - return func(h *HCI) error { - h.params.connParams = param - return nil - } +// SetCentralRole is not supported +func (h *HCI) SetCentralRole() error { + return errors.New("Not supported") } diff --git a/linux/hci/signal.go b/linux/hci/signal.go index 0badb585..9a18745c 100644 --- a/linux/hci/signal.go +++ b/linux/hci/signal.go @@ -13,7 +13,7 @@ import ( // Signal ... type Signal interface { Code() int - Marshal() []byte + Marshal() ([]byte, error) Unmarshal([]byte) error } @@ -26,15 +26,30 @@ func (s sigCmd) data() []byte { return s[4 : 4+s.len()] } // Signal ... func (c *Conn) Signal(req Signal, rsp Signal) error { - data := req.Marshal() + data, err := req.Marshal() + if err != nil { + return err + } buf := bytes.NewBuffer(make([]byte, 0)) - binary.Write(buf, binary.LittleEndian, uint16(4+len(data))) - binary.Write(buf, binary.LittleEndian, uint16(cidLESignal)) + if err := binary.Write(buf, binary.LittleEndian, uint16(4+len(data))); err != nil { + return err + } + if err := binary.Write(buf, binary.LittleEndian, cidLESignal); err != nil { + return err + } - binary.Write(buf, binary.LittleEndian, uint8(req.Code())) - binary.Write(buf, binary.LittleEndian, uint8(c.sigID)) - binary.Write(buf, binary.LittleEndian, uint16(len(data))) - binary.Write(buf, binary.LittleEndian, data) + if err := binary.Write(buf, binary.LittleEndian, uint8(req.Code())); err != nil { + return err + } + if err := binary.Write(buf, binary.LittleEndian, uint8(c.sigID)); err != nil { + return err + } + if err := binary.Write(buf, binary.LittleEndian, uint16(len(data))); err != nil { + return err + } + if err := binary.Write(buf, binary.LittleEndian, data); err != nil { + return err + } c.sigSent = make(chan []byte) defer close(c.sigSent) @@ -63,13 +78,26 @@ func (c *Conn) Signal(req Signal, rsp Signal) error { } func (c *Conn) sendResponse(code uint8, id uint8, r Signal) (int, error) { - data := r.Marshal() + data, err := r.Marshal() + if err != nil { + return 0, err + } buf := bytes.NewBuffer(make([]byte, 0)) - binary.Write(buf, binary.LittleEndian, uint16(4+len(data))) - binary.Write(buf, binary.LittleEndian, uint16(cidLESignal)) - binary.Write(buf, binary.LittleEndian, uint8(code)) - binary.Write(buf, binary.LittleEndian, uint8(id)) - binary.Write(buf, binary.LittleEndian, uint16(len(data))) + if err := binary.Write(buf, binary.LittleEndian, uint16(4+len(data))); err != nil { + return 0, err + } + if err := binary.Write(buf, binary.LittleEndian, cidLESignal); err != nil { + return 0, err + } + if err := binary.Write(buf, binary.LittleEndian, code); err != nil { + return 0, err + } + if err := binary.Write(buf, binary.LittleEndian, id); err != nil { + return 0, err + } + if err := binary.Write(buf, binary.LittleEndian, uint16(len(data))); err != nil { + return 0, err + } if err := binary.Write(buf, binary.LittleEndian, data); err != nil { return 0, err } @@ -85,13 +113,16 @@ func (c *Conn) handleSignal(p pdu) error { // command in the L2CAP packet. If only Responses are recognized, the packet // shall be silently discarded. [Vol3, Part A, 4.1] if p.dlen() > c.sigRxMTU { - c.sendResponse( + _, err := c.sendResponse( SignalCommandReject, sigCmd(p.payload()).id(), &CommandReject{ Reason: 0x0001, // Signaling MTU exceeded. Data: []byte{uint8(c.sigRxMTU), uint8(c.sigRxMTU >> 8)}, // Actual MTUsig. }) + if err != nil { + _ = logger.Error("send repsonse", fmt.Sprintf("%v", err)) + } return nil } diff --git a/linux/hci/signal_gen.go b/linux/hci/signal_gen.go index 6f2d988c..ed1d828a 100644 --- a/linux/hci/signal_gen.go +++ b/linux/hci/signal_gen.go @@ -18,10 +18,12 @@ type CommandReject struct { func (s CommandReject) Code() int { return 0x01 } // Marshal serializes the command parameters into binary form. -func (s *CommandReject) Marshal() []byte { +func (s *CommandReject) Marshal() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0)) - binary.Write(buf, binary.LittleEndian, s) - return buf.Bytes() + if err := binary.Write(buf, binary.LittleEndian, s); err != nil { + return nil, err + } + return buf.Bytes(), nil } // Unmarshal de-serializes the binary data and stores the result in the receiver. @@ -42,10 +44,12 @@ type DisconnectRequest struct { func (s DisconnectRequest) Code() int { return 0x06 } // Marshal serializes the command parameters into binary form. -func (s *DisconnectRequest) Marshal() []byte { +func (s *DisconnectRequest) Marshal() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0)) - binary.Write(buf, binary.LittleEndian, s) - return buf.Bytes() + if err := binary.Write(buf, binary.LittleEndian, s); err != nil { + return nil, err + } + return buf.Bytes(), nil } // Unmarshal de-serializes the binary data and stores the result in the receiver. @@ -66,10 +70,12 @@ type DisconnectResponse struct { func (s DisconnectResponse) Code() int { return 0x07 } // Marshal serializes the command parameters into binary form. -func (s *DisconnectResponse) Marshal() []byte { +func (s *DisconnectResponse) Marshal() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0)) - binary.Write(buf, binary.LittleEndian, s) - return buf.Bytes() + if err := binary.Write(buf, binary.LittleEndian, s); err != nil { + return nil, err + } + return buf.Bytes(), nil } // Unmarshal de-serializes the binary data and stores the result in the receiver. @@ -92,10 +98,12 @@ type ConnectionParameterUpdateRequest struct { func (s ConnectionParameterUpdateRequest) Code() int { return 0x12 } // Marshal serializes the command parameters into binary form. -func (s *ConnectionParameterUpdateRequest) Marshal() []byte { +func (s *ConnectionParameterUpdateRequest) Marshal() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0)) - binary.Write(buf, binary.LittleEndian, s) - return buf.Bytes() + if err := binary.Write(buf, binary.LittleEndian, s); err != nil { + return nil, err + } + return buf.Bytes(), nil } // Unmarshal de-serializes the binary data and stores the result in the receiver. @@ -115,10 +123,12 @@ type ConnectionParameterUpdateResponse struct { func (s ConnectionParameterUpdateResponse) Code() int { return 0x13 } // Marshal serializes the command parameters into binary form. -func (s *ConnectionParameterUpdateResponse) Marshal() []byte { +func (s *ConnectionParameterUpdateResponse) Marshal() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0)) - binary.Write(buf, binary.LittleEndian, s) - return buf.Bytes() + if err := binary.Write(buf, binary.LittleEndian, s); err != nil { + return nil, err + } + return buf.Bytes(), nil } // Unmarshal de-serializes the binary data and stores the result in the receiver. @@ -142,10 +152,12 @@ type LECreditBasedConnectionRequest struct { func (s LECreditBasedConnectionRequest) Code() int { return 0x14 } // Marshal serializes the command parameters into binary form. -func (s *LECreditBasedConnectionRequest) Marshal() []byte { +func (s *LECreditBasedConnectionRequest) Marshal() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0)) - binary.Write(buf, binary.LittleEndian, s) - return buf.Bytes() + if err := binary.Write(buf, binary.LittleEndian, s); err != nil { + return nil, err + } + return buf.Bytes(), nil } // Unmarshal de-serializes the binary data and stores the result in the receiver. @@ -169,10 +181,12 @@ type LECreditBasedConnectionResponse struct { func (s LECreditBasedConnectionResponse) Code() int { return 0x15 } // Marshal serializes the command parameters into binary form. -func (s *LECreditBasedConnectionResponse) Marshal() []byte { +func (s *LECreditBasedConnectionResponse) Marshal() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0)) - binary.Write(buf, binary.LittleEndian, s) - return buf.Bytes() + if err := binary.Write(buf, binary.LittleEndian, s); err != nil { + return nil, err + } + return buf.Bytes(), nil } // Unmarshal de-serializes the binary data and stores the result in the receiver. @@ -193,10 +207,12 @@ type LEFlowControlCredit struct { func (s LEFlowControlCredit) Code() int { return 0x16 } // Marshal serializes the command parameters into binary form. -func (s *LEFlowControlCredit) Marshal() []byte { +func (s *LEFlowControlCredit) Marshal() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0)) - binary.Write(buf, binary.LittleEndian, s) - return buf.Bytes() + if err := binary.Write(buf, binary.LittleEndian, s); err != nil { + return nil, err + } + return buf.Bytes(), nil } // Unmarshal de-serializes the binary data and stores the result in the receiver. diff --git a/linux/hci/smp.go b/linux/hci/smp.go index 87a43f8e..8e3950e0 100644 --- a/linux/hci/smp.go +++ b/linux/hci/smp.go @@ -25,9 +25,15 @@ const ( func (c *Conn) sendSMP(p pdu) error { buf := bytes.NewBuffer(make([]byte, 0)) - binary.Write(buf, binary.LittleEndian, uint16(4+len(p))) - binary.Write(buf, binary.LittleEndian, cidSMP) - binary.Write(buf, binary.LittleEndian, p) + if err := binary.Write(buf, binary.LittleEndian, uint16(4+len(p))); err != nil { + return err + } + if err := binary.Write(buf, binary.LittleEndian, cidSMP); err != nil { + return err + } + if err := binary.Write(buf, binary.LittleEndian, p); err != nil { + return err + } _, err := c.writePDU(buf.Bytes()) logger.Debug("smp", "send", fmt.Sprintf("[%X]", buf.Bytes())) return err diff --git a/linux/hci/socket/constants_linux.go b/linux/hci/socket/constants_linux.go new file mode 100644 index 00000000..2ee1e144 --- /dev/null +++ b/linux/hci/socket/constants_linux.go @@ -0,0 +1,12 @@ +// +build !mips + +package socket + +const ( + directionWrite = 1 + directionRead = 2 + + directionShift = 30 + sizeShift = 16 + typeShift = 8 +) diff --git a/linux/hci/socket/constants_linux_mips.go b/linux/hci/socket/constants_linux_mips.go new file mode 100644 index 00000000..c3c6a72d --- /dev/null +++ b/linux/hci/socket/constants_linux_mips.go @@ -0,0 +1,10 @@ +package socket + +const ( + directionWrite = 4 + directionRead = 2 + + directionShift = 29 + sizeShift = 16 + typeShift = 8 +) diff --git a/linux/hci/socket/socket.go b/linux/hci/socket/socket.go index 14048187..b07bbe78 100644 --- a/linux/hci/socket/socket.go +++ b/linux/hci/socket/socket.go @@ -13,11 +13,11 @@ import ( ) func ioR(t, nr, size uintptr) uintptr { - return (2 << 30) | (t << 8) | nr | (size << 16) + return (directionRead << directionShift) | (t << typeShift) | nr | (size << sizeShift) } func ioW(t, nr, size uintptr) uintptr { - return (1 << 30) | (t << 8) | nr | (size << 16) + return (directionWrite << directionShift) | (t << typeShift) | nr | (size << sizeShift) } func ioctl(fd, op, arg uintptr) error { @@ -119,14 +119,21 @@ func open(fd, id int) (*Socket, error) { } func (s *Socket) Read(p []byte) (int, error) { + s.rmu.Lock() + n, err := unix.Read(s.fd, p) + s.rmu.Unlock() + // Close always sends a dummy command to wake up Read + // bad things happen to the HCI state machines if they receive + // a reply from that command, so make sure no data is returned + // on a closed socket. + // + // note that if Write and Close are called concurrently it's + // indeterminate which replies get through. select { case <-s.closed: return 0, io.EOF default: } - s.rmu.Lock() - defer s.rmu.Unlock() - n, err := unix.Read(s.fd, p) return n, errors.Wrap(err, "can't read hci socket") } @@ -139,7 +146,7 @@ func (s *Socket) Write(p []byte) (int, error) { func (s *Socket) Close() error { close(s.closed) - s.Write([]byte{0x01, 0x09, 0x10, 0x00}) + s.Write([]byte{0x01, 0x09, 0x10, 0x00}) // no-op command to wake up the Read call if it's blocked s.rmu.Lock() defer s.rmu.Unlock() return errors.Wrap(unix.Close(s.fd), "can't close hci socket") diff --git a/option.go b/option.go new file mode 100644 index 00000000..2616cb04 --- /dev/null +++ b/option.go @@ -0,0 +1,103 @@ +package ble + +import ( + "github.com/go-ble/ble/linux/hci/evt" + "time" + + "github.com/go-ble/ble/linux/hci/cmd" +) + +// DeviceOption is an interface which the device should implement to allow using configuration options +type DeviceOption interface { + SetDeviceID(int) error + SetDialerTimeout(time.Duration) error + SetListenerTimeout(time.Duration) error + SetConnParams(cmd.LECreateConnection) error + SetScanParams(cmd.LESetScanParameters) error + SetAdvParams(cmd.LESetAdvertisingParameters) error + SetConnectedHandler(f func(evt.LEConnectionComplete)) error + SetDisconnectedHandler(f func(evt.DisconnectionComplete)) error + SetPeripheralRole() error + SetCentralRole() error +} + +// An Option is a configuration function, which configures the device. +type Option func(DeviceOption) error + +// OptDeviceID sets HCI device ID. +func OptDeviceID(id int) Option { + return func(opt DeviceOption) error { + opt.SetDeviceID(id) + return nil + } +} + +// OptDialerTimeout sets dialing timeout for Dialer. +func OptDialerTimeout(d time.Duration) Option { + return func(opt DeviceOption) error { + opt.SetDialerTimeout(d) + return nil + } +} + +// OptListenerTimeout sets dialing timeout for Listener. +func OptListenerTimeout(d time.Duration) Option { + return func(opt DeviceOption) error { + opt.SetListenerTimeout(d) + return nil + } +} + +// OptConnParams overrides default connection parameters. +func OptConnParams(param cmd.LECreateConnection) Option { + return func(opt DeviceOption) error { + opt.SetConnParams(param) + return nil + } +} + +// OptScanParams overrides default scanning parameters. +func OptScanParams(param cmd.LESetScanParameters) Option { + return func(opt DeviceOption) error { + opt.SetScanParams(param) + return nil + } +} + +// OptAdvParams overrides default advertising parameters. +func OptAdvParams(param cmd.LESetAdvertisingParameters) Option { + return func(opt DeviceOption) error { + opt.SetAdvParams(param) + return nil + } +} + +func OptConnectHandler(f func(evt.LEConnectionComplete)) Option { + return func(opt DeviceOption) error { + opt.SetConnectedHandler(f) + return nil + } +} + +func OptDisconnectHandler(f func(evt.DisconnectionComplete)) Option { + return func(opt DeviceOption) error { + opt.SetDisconnectedHandler(f) + return nil + } +} + +// OptPeripheralRole configures the device to perform Peripheral tasks. +func OptPeripheralRole() Option { + return func(opt DeviceOption) error { + opt.SetPeripheralRole() + return nil + } +} + +// OptCentralRole configures the device to perform Central tasks. +func OptCentralRole() Option { + return func(opt DeviceOption) error { + opt.SetCentralRole() + return nil + } +} diff --git a/uuid.go b/uuid.go index 79ebf33b..999c2623 100644 --- a/uuid.go +++ b/uuid.go @@ -84,15 +84,13 @@ func Contains(s []UUID, u UUID) bool { // Reverse returns a reversed copy of u. func Reverse(u []byte) []byte { - // Special-case 16 bit UUIDS for speed. l := len(u) - if l == 2 { - return []byte{u[1], u[0]} - } b := make([]byte, l) - for i := 0; i < l/2+1; i++ { - b[i], b[l-i-1] = u[l-i-1], u[i] + + for i := 0; i < l; i++ { + b[l-i-1] = u[i] } + return b } diff --git a/uuid_test.go b/uuid_test.go new file mode 100644 index 00000000..bb823d62 --- /dev/null +++ b/uuid_test.go @@ -0,0 +1,28 @@ +package ble + +import ( + "bytes" + "testing" +) + +var forward = [][]byte{ + []byte{1, 2, 3, 4, 5, 6}, + []byte{12, 143, 231, 123, 87, 124, 209}, + []byte{3, 43, 223, 12, 54}, +} + +var reverse = [][]byte{ + []byte{6, 5, 4, 3, 2, 1}, + []byte{209, 124, 87, 123, 231, 143, 12}, + []byte{54, 12, 223, 43, 3}, +} + +func TestReverse(t *testing.T) { + + for i := 0; i < len(forward); i++ { + r := Reverse(forward[i]) + if !bytes.Equal(r, reverse[i]) { + t.Errorf("Error: %v in reverse should be %v, but is: %v", forward[i], reverse[i], r) + } + } +}