package darwin import ( "fmt" "github.com/JuulLabs-OSS/ble" "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 ble.UUID conn *conn } // NewClient ... 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), pc: newProfCache(), cm: cm, id: id, } cln.conn.prph.SetDelegate(cln) return cln, nil } // Addr returns UUID of the remote peripheral. func (cln *Client) Addr() ble.Addr { return cln.conn.RemoteAddr() } // Name returns the name of the remote peripheral. // This can be the advertised name, if exists, or the GAP device name, which takes priority. func (cln *Client) Name() string { return cln.name } // Profile returns the discovered profile. func (cln *Client) Profile() *ble.Profile { return cln.profile } // DiscoverProfile discovers the whole hierarchy of a server. func (cln *Client) DiscoverProfile(force bool) (*ble.Profile, error) { if cln.profile != nil && !force { return cln.profile, nil } ss, err := cln.DiscoverServices(nil) if err != nil { return nil, fmt.Errorf("can't discover services: %s", err) } for _, s := range ss { cs, err := cln.DiscoverCharacteristics(nil, s) if err != nil { return nil, fmt.Errorf("can't discover characteristics: %s", err) } for _, c := range cs { _, err := cln.DiscoverDescriptors(nil, c) if err != nil { return nil, fmt.Errorf("can't discover descriptors: %s", err) } } } cln.profile = &ble.Profile{Services: ss} return cln.profile, nil } // 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) { 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 _, 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} } return svcs, nil } // 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) { 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) { cbsvc, err := cln.pc.findCbSvc(s) if 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 _, 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 } // 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) { cbchr, err := cln.pc.findCbChr(c) if err != nil { return nil, err } ch := cln.conn.evl.dscsDiscovered.Listen() defer cln.conn.evl.dscsDiscovered.Close() cln.conn.prph.DiscoverDescriptors(cbchr) if err != nil { return nil, err } 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) { cbchr, err := cln.pc.findCbChr(c) if err != nil { return nil, 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") } 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 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 { cbchr, err := cln.pc.findCbChr(c) if err != nil { return err } if noRsp { cln.conn.prph.WriteCharacteristic(b, cbchr, false) return nil } 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 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) { cbdsc, err := cln.pc.findCbDsc(d) if 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") } 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 { 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 } 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 { 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 } } // ExchangeMTU set the ATT_MTU to the maximum possible value that can be // supported by both devices [Vol 3, Part G, 4.3.1] func (cln *Client) ExchangeMTU(mtu int) (int, error) { // TODO: find the xpc command to tell OS X the rxMTU we can handle. return cln.conn.TxMTU(), nil } // 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 { cbchr, err := cln.pc.findCbChr(c) if err != nil { 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 { cbchr, err := cln.pc.findCbChr(c) if 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.delSub(c) return nil } // ClearSubscriptions clears all subscriptions to notifications and indications. func (cln *Client) ClearSubscriptions() error { for _, s := range cln.conn.subs { if err := cln.Unsubscribe(s.char, false); err != nil { return err } } return nil } // CancelConnection disconnects the connection. func (cln *Client) CancelConnection() error { cln.cm.CancelConnect(cln.conn.prph) return nil } // Disconnected returns a receiving channel, which is closed when the client disconnects. 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), }) }