package hci import ( "bytes" "encoding/binary" "errors" "fmt" "time" "github.com/JuulLabs-OSS/ble/linux/hci/cmd" ) // Signal ... type Signal interface { Code() int Marshal() ([]byte, error) Unmarshal([]byte) error } type sigCmd []byte func (s sigCmd) code() int { return int(s[0]) } func (s sigCmd) id() uint8 { return s[1] } func (s sigCmd) len() int { return int(binary.LittleEndian.Uint16(s[2:4])) } func (s sigCmd) data() []byte { return s[4 : 4+s.len()] } // Signal ... func (c *Conn) Signal(req Signal, rsp Signal) error { data, err := req.Marshal() if err != nil { return err } buf := bytes.NewBuffer(make([]byte, 0)) 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 } 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) if _, err := c.writePDU(buf.Bytes()); err != nil { return err } var s sigCmd select { case s = <-c.sigSent: case <-time.After(time.Second): // TODO: Find the proper timed out defined in spec, if any. return errors.New("signaling request timed out") } if s.code() != req.Code() { return errors.New("mismatched signaling response") } if s.id() != c.sigID { return errors.New("mismatched signaling id") } c.sigID++ if rsp == nil { return nil } return rsp.Unmarshal(s.data()) } func (c *Conn) sendResponse(code uint8, id uint8, r Signal) (int, error) { data, err := r.Marshal() if err != nil { return 0, err } buf := bytes.NewBuffer(make([]byte, 0)) 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 } logger.Debug("sig", "send", fmt.Sprintf("[%X]", buf.Bytes())) return c.writePDU(buf.Bytes()) } func (c *Conn) handleSignal(p pdu) error { logger.Debug("sig", "recv", fmt.Sprintf("[%X]", p)) // When multiple commands are included in an L2CAP packet and the packet // exceeds the signaling MTU (MTUsig) of the receiver, a single Command Reject // packet shall be sent in response. The identifier shall match the first Request // 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 { _, 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 } s := sigCmd(p.payload()) for len(s) > 0 { // Check if it's a supported request. switch s.code() { case SignalDisconnectRequest: c.handleDisconnectRequest(s) case SignalConnectionParameterUpdateRequest: c.handleConnectionParameterUpdateRequest(s) case SignalLECreditBasedConnectionRequest: c.LECreditBasedConnectionRequest(s) case SignalLEFlowControlCredit: c.LEFlowControlCredit(s) default: // Check if it's a response to a sent command. select { case c.sigSent <- s: continue default: } c.sendResponse( SignalCommandReject, s.id(), &CommandReject{ Reason: 0x0000, // Command not understood. }) } s = s[4+s.len():] // advance to next the packet. } return nil } // DisconnectRequest implements Disconnect Request (0x06) [Vol 3, Part A, 4.6]. func (c *Conn) handleDisconnectRequest(s sigCmd) { var req DisconnectRequest if err := req.Unmarshal(s.data()); err != nil { return } // Send Command Reject when the DCID is unrecognized. if req.DestinationCID != cidLEAtt { endpoints := make([]byte, 4) binary.LittleEndian.PutUint16(endpoints, req.SourceCID) binary.LittleEndian.PutUint16(endpoints, req.DestinationCID) c.sendResponse( SignalCommandReject, s.id(), &CommandReject{ Reason: 0x0002, // Invalid CID in request Data: endpoints, }) return } // Silently discard the request if SCID failed to find the same match. if req.SourceCID != cidLEAtt { return } c.sendResponse( SignalDisconnectResponse, s.id(), &DisconnectResponse{ DestinationCID: req.DestinationCID, SourceCID: req.SourceCID, }) } // ConnectionParameterUpdateRequest implements Connection Parameter Update Request (0x12) [Vol 3, Part A, 4.20]. func (c *Conn) handleConnectionParameterUpdateRequest(s sigCmd) { // This command shall only be sent from the LE slave device to the LE master // device and only if one or more of the LE slave Controller, the LE master // Controller, the LE slave Host and the LE master Host do not support the // Connection Parameters Request Link Layer Control Procedure ([Vol. 6] Part B, // Section 5.1.7). If an LE slave Host receives a Connection Parameter Update // Request packet it shall respond with a Command Reject packet with reason // 0x0000 (Command not understood). if c.param.Role() != roleMaster { c.sendResponse( SignalCommandReject, s.id(), &CommandReject{ Reason: 0x0000, // Command not understood. }) return } var req ConnectionParameterUpdateRequest if err := req.Unmarshal(s.data()); err != nil { return } // LE Connection Update (0x08|0x0013) [Vol 2, Part E, 7.8.18] c.hci.Send(&cmd.LEConnectionUpdate{ ConnectionHandle: c.param.ConnectionHandle(), ConnIntervalMin: req.IntervalMin, ConnIntervalMax: req.IntervalMax, ConnLatency: req.SlaveLatency, SupervisionTimeout: req.TimeoutMultiplier, MinimumCELength: 0, // Informational, and spec doesn't specify the use. MaximumCELength: 0, // Informational, and spec doesn't specify the use. }, nil) // Currently, we (as a slave host) accept all the parameters and forward // it to the controller. The controller might update all, partial or even // none (ignore) of the parameters. The slave(remote) host will be indicated // by its controller if the update actually happens. // TODO: allow users to implement what parameters to accept. c.sendResponse( SignalConnectionParameterUpdateResponse, s.id(), &ConnectionParameterUpdateResponse{ Result: 0, // Accept. }) } // LECreditBasedConnectionRequest ... func (c *Conn) LECreditBasedConnectionRequest(s sigCmd) { // TODO: } // LEFlowControlCredit ... func (c *Conn) LEFlowControlCredit(s sigCmd) { // TODO: }