From 1dd6d60acd077bda8c3470bf1d9a83e4830828e8 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 16 Mar 2018 09:27:44 +0100 Subject: [PATCH 01/28] Fix cancel via context and respect context deadline (#14) * Fix cancel via context and respect context deadline * Remove timeout overwrite and remove go1.7 from travis --- .travis.yml | 1 - linux/hci/gap.go | 33 ++++++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) 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/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 From 1e3172fbf4e27753bb0e25bef0941669de241664 Mon Sep 17 00:00:00 2001 From: Eric Stutzenberger Date: Fri, 16 Mar 2018 01:37:26 -0700 Subject: [PATCH 02/28] scan full advertising packet for UUIDs (#16) --- linux/adv/packet.go | 65 ++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 18 deletions(-) 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 } From 582be3b04d4952b17db01b6a4562754697256104 Mon Sep 17 00:00:00 2001 From: Eric Stutzenberger Date: Wed, 4 Apr 2018 02:21:16 -0700 Subject: [PATCH 03/28] fill in char and desc values on successful read (#15) --- darwin/client.go | 2 ++ linux/gatt/client.go | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/darwin/client.go b/darwin/client.go index 4934a600..06f448a2 100644 --- a/darwin/client.go +++ b/darwin/client.go @@ -176,6 +176,7 @@ func (cln *Client) ReadCharacteristic(c *ble.Characteristic) ([]byte, error) { if rsp.err() != nil { return nil, rsp.err() } + c.Value = rsp.data() return rsp.data(), nil } @@ -215,6 +216,7 @@ func (cln *Client) ReadDescriptor(d *ble.Descriptor) ([]byte, error) { if err := rsp.err(); err != nil { return nil, err } + d.Value = rsp.data() return rsp.data(), nil } diff --git a/linux/gatt/client.go b/linux/gatt/client.go index 3ad108e1..d25ebb02 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] From 721d2405efb7924028deef3b0105219209fba482 Mon Sep 17 00:00:00 2001 From: Kevin Seidel Date: Wed, 4 Apr 2018 11:27:36 +0200 Subject: [PATCH 04/28] Rollback 619ee1f77e2de4a17af8786baa9c08b1c77e442e --- linux/hci/hci.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/hci/hci.go b/linux/hci/hci.go index 30bb4283..3d760bd1 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -490,7 +490,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 From 7370f1c5787e526512801b4442b79d9fd2ef0645 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 24 May 2018 13:49:37 +0200 Subject: [PATCH 05/28] Refinement (#19) * Simplify uuid.Reverse method * Add uuid test * Fix linting issues --- linux/att/db.go | 2 +- linux/att/server.go | 2 +- linux/hci/conn.go | 46 ++++++++++++++++++----------- linux/hci/hci.go | 4 +-- linux/hci/signal.go | 61 +++++++++++++++++++++++++++++---------- linux/hci/signal_gen.go | 64 +++++++++++++++++++++++++---------------- linux/hci/smp.go | 12 ++++++-- uuid.go | 10 +++---- uuid_test.go | 28 ++++++++++++++++++ 9 files changed, 160 insertions(+), 69 deletions(-) create mode 100644 uuid_test.go 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/hci/conn.go b/linux/hci/conn.go index 46a46f83..d6c39fc1 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 } @@ -205,10 +204,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 { diff --git a/linux/hci/hci.go b/linux/hci/hci.go index 3d760bd1..6602fc91 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -278,7 +278,7 @@ 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 @@ -317,7 +317,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 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/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) + } + } +} From b4a677b035c5804423c7b1a5df861a6058504454 Mon Sep 17 00:00:00 2001 From: Josh Lubawy Date: Wed, 18 Jul 2018 01:57:58 -0700 Subject: [PATCH 06/28] Add isConnected field to darwin.conn type (#25) This prevents multiple evtPeripheralConnected events from causing a deadlock. --- darwin/conn.go | 2 ++ darwin/device.go | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/darwin/conn.go b/darwin/conn.go index 96079818..b5713b31 100644 --- a/darwin/conn.go +++ b/darwin/conn.go @@ -43,6 +43,8 @@ type conn struct { notifiers map[uint16]ble.Notifier // central connection only subs map[uint16]*sub + + isConnected bool } func (c *conn) Context() context.Context { diff --git a/darwin/device.go b/darwin/device.go index c94abc61..af65d68a 100644 --- a/darwin/device.go +++ b/darwin/device.go @@ -489,10 +489,15 @@ func (d *Device) HandleXpcEvent(event xpc.Dict, err error) { d.conn(args).unsubscribed(d.chars[args.attributeID()]) case evtPeripheralConnected: - d.chConn <- d.conn(args) + c := d.conn(args) + if !c.isConnected { + c.isConnected = true + d.chConn <- c + } case evtPeripheralDisconnected: c := d.conn(args) + c.isConnected = false select { case c.rspc <- m: // Canceled by local central synchronously From 11b1dad1df3d73d0ff11b08fb156c617b7687f29 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 18 Jul 2018 11:04:07 +0200 Subject: [PATCH 07/28] Add lock for sent map (#22) --- linux/hci/hci.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/linux/hci/hci.go b/linux/hci/hci.go index 6602fc91..e29975db 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -42,6 +42,7 @@ func NewHCI(opts ...Option) (*HCI, error) { 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 +74,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 @@ -248,7 +250,9 @@ 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() { @@ -412,7 +416,9 @@ func (h *HCI) handleCommandComplete(b []byte) error { 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) } @@ -426,7 +432,9 @@ func (h *HCI) handleCommandStatus(b []byte) error { h.chCmdBufs <- make([]byte, 64) } + 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) } From 731710e91806e163fe770d93dc318683f6f53c63 Mon Sep 17 00:00:00 2001 From: Josh Lubawy Date: Tue, 14 Aug 2018 05:37:35 -0700 Subject: [PATCH 08/28] Add Conn method to Client interface (#26) * Add ability to get RxMTU from a connection * Fix race detector warning * Fix compiler error --- client.go | 3 +++ darwin/client.go | 5 +++++ darwin/conn.go | 6 ++++-- darwin/device.go | 2 +- linux/gatt/client.go | 5 +++++ 5 files changed, 18 insertions(+), 3 deletions(-) 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/darwin/client.go b/darwin/client.go index 06f448a2..43097894 100644 --- a/darwin/client.go +++ b/darwin/client.go @@ -320,6 +320,11 @@ 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 diff --git a/darwin/conn.go b/darwin/conn.go index b5713b31..597f186e 100644 --- a/darwin/conn.go +++ b/darwin/conn.go @@ -9,10 +9,10 @@ import ( "github.com/raff/goble/xpc" ) -func newConn(d *Device, a ble.Addr) *conn { +func newConn(d *Device, a ble.Addr, rxMTU int) *conn { return &conn{ dev: d, - rxMTU: 23, + rxMTU: rxMTU, txMTU: 23, addr: a, done: make(chan struct{}), @@ -77,7 +77,9 @@ 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) { diff --git a/darwin/device.go b/darwin/device.go index af65d68a..1493e90e 100644 --- a/darwin/device.go +++ b/darwin/device.go @@ -546,7 +546,7 @@ func (d *Device) conn(m msg) *conn { d.connLock.Lock() c, ok := d.conns[a.String()] if !ok { - c = newConn(d, a) + c = newConn(d, a, m.attMTU()) d.conns[a.String()] = c } d.connLock.Unlock() diff --git a/linux/gatt/client.go b/linux/gatt/client.go index d25ebb02..e138d9e0 100644 --- a/linux/gatt/client.go +++ b/linux/gatt/client.go @@ -371,6 +371,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() From c9d4987cacc7e9e4e8e2001da40d691497016ecd Mon Sep 17 00:00:00 2001 From: xibz Date: Fri, 31 Aug 2018 03:36:03 -0700 Subject: [PATCH 09/28] Fixes OSX unsupported key for MTU (#30) --- darwin/msg.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 { From e78417b510a348171f21754ff934fe8408b21b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Narajowski?= Date: Tue, 2 Oct 2018 12:26:05 +0200 Subject: [PATCH 10/28] Add option to pass HCI options when creating a device (#28) This can be useful when we need to use specific HCI device ID. --- darwin/device.go | 4 +- darwin/option.go | 50 +++++++++++++++------- examples/blesh/lnx.go | 4 +- examples/lib/dev/default_darwin.go | 4 +- examples/lib/dev/default_linux.go | 4 +- examples/lib/dev/dev.go | 8 ++-- linux/device.go | 12 +++--- linux/hci/hci.go | 4 +- linux/hci/option.go | 52 +++++++++++------------ option.go | 68 ++++++++++++++++++++++++++++++ 10 files changed, 150 insertions(+), 60 deletions(-) create mode 100644 option.go diff --git a/darwin/device.go b/darwin/device.go index 1493e90e..f6e941a3 100644 --- a/darwin/device.go +++ b/darwin/device.go @@ -37,7 +37,7 @@ type Device struct { } // NewDevice returns a BLE device. -func NewDevice(opts ...Option) (*Device, error) { +func NewDevice(opts ...ble.Option) (*Device, error) { err := initXpcIDs() if err != nil { return nil, err @@ -61,7 +61,7 @@ func NewDevice(opts ...Option) (*Device, error) { } // Option sets the options specified. -func (d *Device) Option(opts ...Option) error { +func (d *Device) Option(opts ...ble.Option) error { var err error for _, opt := range opts { err = opt(d) diff --git a/darwin/option.go b/darwin/option.go index 924d1d5e..cdbdde31 100644 --- a/darwin/option.go +++ b/darwin/option.go @@ -1,20 +1,40 @@ 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 - } +import ( + "errors" + "time" + + "github.com/go-ble/ble/linux/hci/cmd" +) + +// SetPeripheralRole configures the device to perform Peripheral tasks. +func (d *Device) SetPeripheralRole() error { + d.role = 1 + return nil +} + +// SetCentralRole configures the device to perform Central tasks. +func (d *Device) SetCentralRole() error { + d.role = 0 + 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") } -// OptCentralRole configures the device to perform Central tasks. -func OptCentralRole() Option { - return func(d *Device) error { - d.role = 0 - return nil - } +// SetConnParams overrides default connection parameters. +func (d *Device) SetConnParams(param cmd.LECreateConnection) error { + return errors.New("Not supported") } 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/linux/device.go b/linux/device.go index 521eb9f6..86fe565f 100644 --- a/linux/device.go +++ b/linux/device.go @@ -13,17 +13,17 @@ 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") } diff --git a/linux/hci/hci.go b/linux/hci/hci.go index e29975db..4abd8491 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -35,7 +35,7 @@ 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, @@ -171,7 +171,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) diff --git a/linux/hci/option.go b/linux/hci/option.go index b0f4832b..d765d803 100644 --- a/linux/hci/option.go +++ b/linux/hci/option.go @@ -1,42 +1,42 @@ package hci import ( + "errors" "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 +} -// OptDeviceID sets HCI device ID. -func OptDeviceID(id int) Option { - return func(h *HCI) error { - h.id = id - return nil - } +// SetListenerTimeout sets dialing timeout for Listener. +func (h *HCI) SetListenerTimeout(d time.Duration) error { + h.listenerTmo = d + return nil } -// OptDialerTimeout sets dialing timeout for Dialer. -func OptDialerTimeout(d time.Duration) Option { - return func(h *HCI) error { - h.dialerTmo = d - return nil - } +// SetConnParams overrides default connection parameters. +func (h *HCI) SetConnParams(param cmd.LECreateConnection) error { + h.params.connParams = 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/option.go b/option.go new file mode 100644 index 00000000..f301c7f6 --- /dev/null +++ b/option.go @@ -0,0 +1,68 @@ +package ble + +import ( + "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 + 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 + } +} + +// 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 + } +} From cc345f21e0b551df3593e53bd265ccd9055cd8fd Mon Sep 17 00:00:00 2001 From: "Iskander (Alex) Sharipov" Date: Mon, 10 Dec 2018 16:48:50 +0300 Subject: [PATCH 11/28] linux/add: fix duplicated switch case error The `rsp[0] == ErrorResponseCode && len(rsp) == 5` case was duplicated. Surrounding code uses `rsp[0] == ErrorResponseCode && len(rsp) != 5` (note "not equal") for the second case, so probably this is probably what we want there as well. --- linux/att/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/att/client.go b/linux/att/client.go index f6fe88ef..95ddb7b3 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 From e4c77014ff5a22003a43b91ce1c53381ac9de901 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 13 May 2019 08:59:55 +0200 Subject: [PATCH 12/28] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d6d8792f..09d0abb1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ [![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) - +## ⚠️ Archive Notice ⚠️ +I sadly have no time to maintain this repository anymore. If you want a version, which is activly maintained and developed take a look at [https://github.com/rigado/ble](https://github.com/rigado/ble) **ble** is a [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) package for Linux and macOS. From 147700f13610085f7387692cd6663a67c495ab87 Mon Sep 17 00:00:00 2001 From: Eric Stutzenberger Date: Tue, 21 May 2019 10:15:21 -0700 Subject: [PATCH 13/28] Some fixes and QoL improvements (#45) * Made Command Flow Control match the actual behavior specified in the Bluetooth spec (Vol 2, Part E, Section 4.4), old version tended to collect extra allowed commands over time and eventually lock up. * comments on magic numbers * add options for setting scanning and advertising parameters. * Close the socket if device intialization fails. * Fix race condition where new data can be written to a connection while it is being disconnected, and the buffers are leaked from the packet pool forever. * Fix typo in comment. * Prevent HCI send from blocking forever if the device doesn't respond. * clear sent table when a reply is received so unexpected packets don't cause random weirdness. * Avoid receiving invalid command response on device stop. * Update readme (#12) * Update with disclaimer * update README * update README --- .gitignore | 2 ++ README.md | 6 ++-- darwin/option.go | 10 ++++++ linux/device.go | 3 ++ linux/hci/conn.go | 9 +++++ linux/hci/hci.go | 69 +++++++++++++++++++++++++++++++------- linux/hci/option.go | 12 +++++++ linux/hci/socket/socket.go | 15 ++++++--- option.go | 18 ++++++++++ 9 files changed, 124 insertions(+), 20 deletions(-) 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/README.md b/README.md index 09d0abb1..29a39d84 100644 --- a/README.md +++ b/README.md @@ -5,8 +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) -## ⚠️ Archive Notice ⚠️ -I sadly have no time to maintain this repository anymore. If you want a version, which is activly maintained and developed take a look at [https://github.com/rigado/ble](https://github.com/rigado/ble) - -**ble** is a [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) package for Linux and macOS. +**ble** is a Golang [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) package for Linux and Mac OS. +**Note:** The Mac OS portion is not being actively maintained. diff --git a/darwin/option.go b/darwin/option.go index cdbdde31..1d86b7c3 100644 --- a/darwin/option.go +++ b/darwin/option.go @@ -38,3 +38,13 @@ func (d *Device) SetListenerTimeout(dur time.Duration) error { 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/linux/device.go b/linux/device.go index 86fe565f..1a1ec026 100644 --- a/linux/device.go +++ b/linux/device.go @@ -28,17 +28,20 @@ func NewDeviceWithNameAndHandler(name string, handler ble.NotifyHandler, opts .. 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/hci/conn.go b/linux/hci/conn.go index d6c39fc1..247795e4 100644 --- a/linux/hci/conn.go +++ b/linux/hci/conn.go @@ -195,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 diff --git a/linux/hci/hci.go b/linux/hci/hci.go index 4abd8491..75207592 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -144,7 +144,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 { @@ -259,12 +259,34 @@ func (h *HCI) send(c Command) ([]byte, error) { 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() { @@ -273,7 +295,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) @@ -293,7 +319,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 { @@ -407,13 +436,11 @@ 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() @@ -428,9 +455,7 @@ 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())] @@ -514,7 +539,13 @@ 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() return nil } @@ -542,3 +573,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 d765d803..a2ae51dd 100644 --- a/linux/hci/option.go +++ b/linux/hci/option.go @@ -31,6 +31,18 @@ func (h *HCI) SetConnParams(param cmd.LECreateConnection) error { return nil } +// SetScanParams overrides default scanning parameters. +func (h *HCI) SetScanParams(param cmd.LESetScanParameters) error { + h.params.scanParams = param + return nil +} + +// SetAdvParams overrides default advertising parameters. +func (h *HCI) SetAdvParams(param cmd.LESetAdvertisingParameters) error { + h.params.advParams = param + return nil +} + // SetPeripheralRole is not supported func (h *HCI) SetPeripheralRole() error { return errors.New("Not supported") diff --git a/linux/hci/socket/socket.go b/linux/hci/socket/socket.go index 14048187..5b0be4f3 100644 --- a/linux/hci/socket/socket.go +++ b/linux/hci/socket/socket.go @@ -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 index f301c7f6..93aef928 100644 --- a/option.go +++ b/option.go @@ -12,6 +12,8 @@ type DeviceOption interface { SetDialerTimeout(time.Duration) error SetListenerTimeout(time.Duration) error SetConnParams(cmd.LECreateConnection) error + SetScanParams(cmd.LESetScanParameters) error + SetAdvParams(cmd.LESetAdvertisingParameters) error SetPeripheralRole() error SetCentralRole() error } @@ -51,6 +53,22 @@ func OptConnParams(param cmd.LECreateConnection) Option { } } +// 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 + } +} + // OptPeripheralRole configures the device to perform Peripheral tasks. func OptPeripheralRole() Option { return func(opt DeviceOption) error { From 5328f95e216a6f1a0f85974b739b5983a2f7cb24 Mon Sep 17 00:00:00 2001 From: rkj Date: Fri, 2 Aug 2019 14:31:06 +0200 Subject: [PATCH 14/28] linux.hci: fix stop advertise on new connection --- linux/hci/hci.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/hci/hci.go b/linux/hci/hci.go index 75207592..958fe12c 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -501,7 +501,7 @@ 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{0}, nil) } h.params.RUnlock() } From 31b5b19639c69358efa0ad7365a818f3ab0b5434 Mon Sep 17 00:00:00 2001 From: rkj Date: Sun, 28 Jul 2019 22:45:00 +0200 Subject: [PATCH 15/28] hci: add Connect/DisconnectHandler options --- linux/hci/hci.go | 11 +++++++++++ linux/hci/option.go | 13 +++++++++++++ option.go | 17 +++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/linux/hci/hci.go b/linux/hci/hci.go index 75207592..91e4b519 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -3,6 +3,7 @@ package hci import ( "fmt" "io" + "log" "net" "strings" "sync" @@ -110,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 @@ -505,6 +509,10 @@ func (h *HCI) handleLEConnectionComplete(b []byte) error { } h.params.RUnlock() } + log.Println("incoming:", c.RemoteAddr()) + if h.connectedHandler != nil { + h.connectedHandler(e) + } return nil } @@ -546,6 +554,9 @@ func (h *HCI) handleDisconnectionComplete(b []byte) error { c.txBuffer.LockPool() c.txBuffer.PutAll() c.txBuffer.UnlockPool() + if h.disconnectedHandler != nil { + h.disconnectedHandler(e) + } return nil } diff --git a/linux/hci/option.go b/linux/hci/option.go index a2ae51dd..61493dd3 100644 --- a/linux/hci/option.go +++ b/linux/hci/option.go @@ -2,6 +2,7 @@ package hci import ( "errors" + "github.com/go-ble/ble/linux/hci/evt" "time" "github.com/go-ble/ble/linux/hci/cmd" @@ -37,6 +38,18 @@ func (h *HCI) SetScanParams(param cmd.LESetScanParameters) error { 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 +} + +// SetDisconnectedHandler sets handler to be called on disconnect. +func (h *HCI) SetDisconnectedHandler(f func(evt.DisconnectionComplete)) error { + h.disconnectedHandler = f + return nil +} + // SetAdvParams overrides default advertising parameters. func (h *HCI) SetAdvParams(param cmd.LESetAdvertisingParameters) error { h.params.advParams = param diff --git a/option.go b/option.go index 93aef928..2616cb04 100644 --- a/option.go +++ b/option.go @@ -1,6 +1,7 @@ package ble import ( + "github.com/go-ble/ble/linux/hci/evt" "time" "github.com/go-ble/ble/linux/hci/cmd" @@ -14,6 +15,8 @@ type DeviceOption interface { 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 } @@ -69,6 +72,20 @@ func OptAdvParams(param cmd.LESetAdvertisingParameters) Option { } } +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 { From 965572da648852cb5a1b086c3c76f2071a9eaf30 Mon Sep 17 00:00:00 2001 From: Andrew Markee Date: Tue, 3 Sep 2019 08:43:54 -0500 Subject: [PATCH 16/28] Corrected ioctl on mips (#52) --- linux/hci/socket/constants_linux.go | 12 ++++++++++++ linux/hci/socket/constants_linux_mips.go | 10 ++++++++++ linux/hci/socket/socket.go | 4 ++-- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 linux/hci/socket/constants_linux.go create mode 100644 linux/hci/socket/constants_linux_mips.go 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 5b0be4f3..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 { From 27f603274e53fcfc130359bdd42fd778393d8794 Mon Sep 17 00:00:00 2001 From: rk Date: Thu, 9 Jan 2020 21:55:54 +0100 Subject: [PATCH 17/28] linux.hci: do not exit sktLoop on handlePacket error (#51) - acheiving much more stability with this, since errors sometimes happen when network is busy, but are in no way fatal --- linux/hci/hci.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linux/hci/hci.go b/linux/hci/hci.go index 75207592..e50f32c2 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -310,8 +310,8 @@ func (h *HCI) sktLoop() { if strings.HasPrefix(err.Error(), "unsupported vendor packet:") { _ = logger.Error("skt: %v", err) } else { - h.err = fmt.Errorf("skt: %v", err) - return + log.Printf("skt: %v", err) + continue } } } From 6e59c6975489540c6b832fe6eeac9c8865782ff7 Mon Sep 17 00:00:00 2001 From: Cliff Brake Date: Thu, 9 Jan 2020 15:56:43 -0500 Subject: [PATCH 18/28] add go.mod/sum (#54) This makes it easier to build the project with recent versions of Go -- check it out anywhere and build, vs having to put in GOPATH --- go.mod | 15 +++++++++++++++ go.sum | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..009d2727 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/go-ble/ble + +go 1.13 + +require ( + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.10 // 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-20191126131656-8a8471f7e56d +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..f7c19d37 --- /dev/null +++ b/go.sum @@ -0,0 +1,37 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +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/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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-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= +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= From 5ba052eb5acfdcd809dbfd8ca33c2dfe30342dcf Mon Sep 17 00:00:00 2001 From: rkj Date: Fri, 10 Jan 2020 16:20:44 +0100 Subject: [PATCH 19/28] remove log entry --- linux/hci/hci.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/linux/hci/hci.go b/linux/hci/hci.go index 91e4b519..bcaae545 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -3,7 +3,6 @@ package hci import ( "fmt" "io" - "log" "net" "strings" "sync" @@ -509,7 +508,6 @@ func (h *HCI) handleLEConnectionComplete(b []byte) error { } h.params.RUnlock() } - log.Println("incoming:", c.RemoteAddr()) if h.connectedHandler != nil { h.connectedHandler(e) } From 15dbdb1e9d44205e4e4d29e3df3d18fa2d724050 Mon Sep 17 00:00:00 2001 From: Armaan Roshani Date: Thu, 16 Jan 2020 20:34:39 -0800 Subject: [PATCH 20/28] re-adding log log was removed at 5ba052e but used again at 27f6032 --- linux/hci/hci.go | 1 + 1 file changed, 1 insertion(+) diff --git a/linux/hci/hci.go b/linux/hci/hci.go index e3e2c8ca..1c823005 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -3,6 +3,7 @@ package hci import ( "fmt" "io" + "log" "net" "strings" "sync" From 9f14c3a7527e2763d9a4101458226d2b4c0c0dd0 Mon Sep 17 00:00:00 2001 From: Peter Hellberg Date: Sun, 8 Mar 2020 11:48:38 +0100 Subject: [PATCH 21/28] Add SetConnectedHandler and SetDisconnectedHandler to *darwin.Device --- darwin/option.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/darwin/option.go b/darwin/option.go index 1d86b7c3..27d324f7 100644 --- a/darwin/option.go +++ b/darwin/option.go @@ -5,8 +5,19 @@ import ( "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 { d.role = 1 From 2323c1a80ca8f5e0ba3f6c7842d473f1b4e7dbaf Mon Sep 17 00:00:00 2001 From: Mattia Fiumara Date: Sat, 14 Mar 2020 13:01:01 +0100 Subject: [PATCH 22/28] fix go vet warning for unkeyed field in composite literal --- linux/hci/hci.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/hci/hci.go b/linux/hci/hci.go index 1c823005..7efbdcdc 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -505,7 +505,7 @@ 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(&cmd.LESetAdvertiseEnable{0}, nil) + go h.Send(&cmd.LESetAdvertiseEnable{AdvertisingEnable: 0}, nil) } h.params.RUnlock() } From e97c2ff063bc29b147a513b1aa027db78ee115c7 Mon Sep 17 00:00:00 2001 From: Christopher Collins Date: Mon, 27 Apr 2020 10:43:47 -0700 Subject: [PATCH 23/28] Mojave / Catalina support This commit replaces the use of XPC services with the "cbgo" library (CoreBluetooth Go). --- darwin/adv.go | 49 ++-- darwin/client.go | 395 ++++++++++++++++--------- darwin/cmgrdlg.go | 71 +++++ darwin/conn.go | 180 ++++++++---- darwin/device.go | 688 +++++++++++++++----------------------------- darwin/event.go | 113 ++++++++ darwin/option.go | 2 - darwin/pmgprdlg.go | 136 +++++++++ darwin/profcache.go | 133 +++++++++ darwin/util.go | 24 +- darwin/xpcid.go | 173 ----------- go.mod | 6 +- go.sum | 15 + 13 files changed, 1127 insertions(+), 858 deletions(-) create mode 100644 darwin/cmgrdlg.go create mode 100644 darwin/event.go create mode 100644 darwin/pmgprdlg.go create mode 100644 darwin/profcache.go delete mode 100644 darwin/xpcid.go 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 43097894..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,108 +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") } - c.Value = rsp.data() - 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") } - d.Value = rsp.data() - 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 @@ -255,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 } @@ -308,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. @@ -329,3 +426,35 @@ 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 597f186e..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, rxMTU int) *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: rxMTU, + 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,17 +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 - - isConnected bool + subs map[string]*sub + chrReads map[string](chan error) } func (c *conn) Context() context.Context { @@ -85,12 +128,12 @@ func (c *conn) SetTxMTU(mtu int) { 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 } @@ -99,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 +} + +// 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) } -func (c *conn) sendCmd(id int, args xpc.Dict) error { - return c.dev.sendCmd(c.dev.pm, id, args) +// 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 f6e941a3..826efd24 100644 --- a/darwin/device.go +++ b/darwin/device.go @@ -1,569 +1,329 @@ 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 ...ble.Option) (*Device, error) { - err := initXpcIDs() - if err != nil { - return nil, err - } - d := &Device{ - rspc: make(chan msg), - conns: make(map[string]*conn), - chConn: make(chan *conn), - chars: make(map[int]*ble.Characteristic), - base: 1, + cm: cbgo.NewCentralManager(nil), + pm: cbgo.NewPeripheralManager(nil), + pc: newProfCache(), + conns: make(map[string]*conn), } - if err := d.Option(opts...); err != nil { - return nil, err + + // Each manager object will receive a state change event as soon as its + // delegate is set. Only proceed if the event indicates Bluetooth is + // enabled. + + d.cm.SetDelegate(d) + <-d.evl.stateChanged.Listen() + 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) } - d.pm = xpc.XpcConnect(serviceID, d) - d.cm = xpc.XpcConnect(serviceID, d) + d.pm.SetDelegate(d) + <-d.evl.stateChanged.Listen() + if d.cm.State() != cbgo.ManagerStatePoweredOn { + return nil, fmt.Errorf("peripheral manager has invalid state: have=%d want=%d: is Bluetooth turned on?", + d.cm.State(), cbgo.ManagerStatePoweredOn) + } - return d, errors.Wrap(d.Init(), "can't init") + return d, nil } // Option sets the options specified. func (d *Device) Option(opts ...ble.Option) error { - var err error - for _, opt := range opts { - err = opt(d) - } - return err + return nil } -// 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) - } +// Scan ... +func (d *Device) Scan(ctx context.Context, allowDup bool, h ble.AdvHandler) error { + d.advHandler = h - rsp, err = d.sendReq(d.pm, cmdInit, xpc.Dict{ - "kCBMsgArgName": fmt.Sprintf("gopher-%v", time.Now().Unix()), - "kCBMsgArgOptions": xpc.Dict{ - "kCBInitOptionShowPowerAlert": 1, - }, - "kCBMsgArgType": 1, + d.cm.Scan(nil, &cbgo.CentralManagerScanOpts{ + AllowDuplicates: allowDup, }) - if err != nil { - return err - } - s = State(rsp.state()) - if s != StatePoweredOn { - return fmt.Errorf("state: %s", s) - } - 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(), - }) - 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 +} + +func (d *Device) AddService(svc *ble.Service) error { + chrMap := make(map[*ble.Characteristic]cbgo.Characteristic) + dscMap := make(map[*ble.Descriptor]cbgo.Descriptor) - xds := xpc.Array{} + 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: - c := d.conn(args) - if !c.isConnected { - c.isConnected = true - d.chConn <- c - } + return d.advData(ctx, ad) +} - case evtPeripheralDisconnected: - c := d.conn(args) - c.isConnected = false - 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, m.attMTU()) - 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/option.go b/darwin/option.go index 27d324f7..ea31fc46 100644 --- a/darwin/option.go +++ b/darwin/option.go @@ -20,13 +20,11 @@ func (d *Device) SetDisconnectedHandler(f func(evt.DisconnectionComplete)) error // SetPeripheralRole configures the device to perform Peripheral tasks. func (d *Device) SetPeripheralRole() error { - d.role = 1 return nil } // SetCentralRole configures the device to perform Central tasks. func (d *Device) SetCentralRole() error { - d.role = 0 return nil } 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/go.mod b/go.mod index 009d2727..6dc36f90 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,13 @@ module github.com/go-ble/ble go 1.13 require ( - github.com/mattn/go-colorable v0.1.4 // indirect - github.com/mattn/go-isatty v0.0.10 // indirect + 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-20191126131656-8a8471f7e56d + golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae ) diff --git a/go.sum b/go.sum index f7c19d37..67e53277 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,21 @@ 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= @@ -22,15 +30,22 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0 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= From 91f80b511c8be87da605f0874c487fe96e1f3c60 Mon Sep 17 00:00:00 2001 From: Christopher Collins Date: Sat, 16 May 2020 12:56:10 -0700 Subject: [PATCH 24/28] Fix hang when cmgr/pmgr initialize early This fixes what seems to be a Catalina-specific issue. Pre-Catalina: The DidUpdateState delegate callbacks get called *after* the delegate properties gets assigned. Catalina: DidUpdateState gets called immediately upon construction of the manager objects (CBCentralManager and CBPeripheralManager). Prior to this commit, the code assumed it could assign the delegate properties and then listen for the transition to the powered-on state. This works for older versions of macOS, but with Catalina the code blocks forever, waiting for an event that has already occurred. The fix is to use the following procedure: (for each manager object): 1. Assign the delegate. 2. Check the state. If the state is not "unknown" then the procedure is done. 3. Else, wait up to one second for a state change event. --- darwin/device.go | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/darwin/device.go b/darwin/device.go index 826efd24..62b4faaf 100644 --- a/darwin/device.go +++ b/darwin/device.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "errors" "fmt" + "time" "github.com/go-ble/ble" "github.com/JuulLabs-OSS/cbgo" @@ -39,22 +40,42 @@ func NewDevice(opts ...ble.Option) (*Device, error) { conns: make(map[string]*conn), } - // Each manager object will receive a state change event as soon as its - // delegate is set. Only proceed if the event indicates Bluetooth is - // enabled. + // Only proceed if Bluetooth is enabled. + blockUntilStateChange := func(getState func() cbgo.ManagerState) { + if getState() != cbgo.ManagerStateUnknown { + return + } + + // Wait until state changes or until one second passes (whichever + // happens first). + for { + select { + case <-d.evl.stateChanged.Listen(): + if getState() != cbgo.ManagerStateUnknown { + return + } + + case <-time.NewTimer(time.Second).C: + return + } + } + } + + // Ensure central manager is ready. d.cm.SetDelegate(d) - <-d.evl.stateChanged.Listen() + 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) } + // Ensure peripheral manager is ready. d.pm.SetDelegate(d) - <-d.evl.stateChanged.Listen() - if d.cm.State() != cbgo.ManagerStatePoweredOn { + 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.cm.State(), cbgo.ManagerStatePoweredOn) + d.pm.State(), cbgo.ManagerStatePoweredOn) } return d, nil From 9ab27207b7c138b82ce4f2beab82ce2479fb5612 Mon Sep 17 00:00:00 2001 From: Alex Ferm Date: Fri, 31 Jul 2020 16:29:24 -0500 Subject: [PATCH 25/28] Added support for reading RSSI of a connected peripheral --- conn.go | 3 +++ examples/basic/rssi/main.go | 41 +++++++++++++++++++++++++++++++++++++ linux/gatt/client.go | 3 +-- linux/hci/conn.go | 9 ++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 examples/basic/rssi/main.go 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/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/linux/gatt/client.go b/linux/gatt/client.go index e138d9e0..ef29ff56 100644 --- a/linux/gatt/client.go +++ b/linux/gatt/client.go @@ -276,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 diff --git a/linux/hci/conn.go b/linux/hci/conn.go index 247795e4..24ff0c5f 100644 --- a/linux/hci/conn.go +++ b/linux/hci/conn.go @@ -297,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 { From d66aac6bfbb2f0a82249146eee6009ecf43214a4 Mon Sep 17 00:00:00 2001 From: Alex Ferm Date: Wed, 2 Sep 2020 15:34:35 -0500 Subject: [PATCH 26/28] Client should also support incoming MTU exchanges --- linux/att/client.go | 71 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/linux/att/client.go b/linux/att/client.go index f6fe88ef..37c9d010 100644 --- a/linux/att/client.go +++ b/linux/att/client.go @@ -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] +} From c72d72889bd3f0efc7c8da0d34dcbd7c0e680bdc Mon Sep 17 00:00:00 2001 From: Mark Drayton Date: Sun, 27 Dec 2020 15:51:04 +0100 Subject: [PATCH 27/28] Don't try to handle canceled connections Currently the check to see if a connection was canceled happens *after* newConn() is called, leaking a goroutine waiting to receive on chInPkt. Move the check up. --- linux/hci/hci.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/linux/hci/hci.go b/linux/hci/hci.go index 1c823005..489c0830 100644 --- a/linux/hci/hci.go +++ b/linux/hci/hci.go @@ -473,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 @@ -486,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 { From 918dbc1d19f00274c5834cb12cd4d5eca68ca47e Mon Sep 17 00:00:00 2001 From: "jialiang.yuan" <1281598246@qq.com> Date: Tue, 24 Aug 2021 12:01:56 +0800 Subject: [PATCH 28/28] fix bug --- gatt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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