461 lines
11 KiB
Go
461 lines
11 KiB
Go
|
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),
|
||
|
})
|
||
|
}
|