| package hci |
| |
| import ( |
| "bytes" |
| "encoding/binary" |
| "errors" |
| "fmt" |
| "time" |
| |
| "github.com/go-ble/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: |
| } |