blob: 54c04eecf468bd78815d9ab0451bd460bfbcd265 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package nmble
import (
"fmt"
"sync"
"time"
"github.com/runtimeco/go-coap"
log "github.com/sirupsen/logrus"
"mynewt.apache.org/newt/util"
. "mynewt.apache.org/newtmgr/nmxact/bledefs"
"mynewt.apache.org/newtmgr/nmxact/mgmt"
"mynewt.apache.org/newtmgr/nmxact/nmcoap"
"mynewt.apache.org/newtmgr/nmxact/nmp"
"mynewt.apache.org/newtmgr/nmxact/nmxutil"
"mynewt.apache.org/newtmgr/nmxact/sesn"
"mynewt.apache.org/newtmgr/nmxact/task"
)
type NakedSesnState int
const (
// Session not open and no open in progress.
NS_STATE_CLOSED NakedSesnState = iota
// Open in progress.
NS_STATE_OPENING_ACTIVE
// After a failed open; additional retries remain.
NS_STATE_OPENING_IDLE
// Open complete.
NS_STATE_OPEN
)
// Implements a BLE session that does not acquire the master resource on
// connect. The user of this type must acquire the resource manually.
type NakedSesn struct {
cfg sesn.SesnCfg
bx *BleXport
conn *Conn
mgmtChrs BleMgmtChrs
txvr *mgmt.Transceiver
tq task.TaskQueue
wg sync.WaitGroup
stopChan chan struct{}
// Protects `enabled` and `opening`.
mtx sync.Mutex
state NakedSesnState
shuttingDown bool
smIo SmIo
}
func (s *NakedSesn) init() error {
s.conn = NewConn(s.bx)
s.stopChan = make(chan struct{})
if s.txvr != nil {
s.txvr.Stop()
}
txvr, err := mgmt.NewTransceiver(s.cfg.TxFilterCb, s.cfg.RxFilterCb, true,
s.cfg.MgmtProto, 3)
if err != nil {
return err
}
s.txvr = txvr
s.tq.Stop(fmt.Errorf("Ensuring task is stopped"))
if err := s.tq.Start(10); err != nil {
nmxutil.Assert(false)
return err
}
return nil
}
func NewNakedSesn(bx *BleXport, cfg sesn.SesnCfg) (*NakedSesn, error) {
mgmtChrs, err := BuildMgmtChrs(cfg.MgmtProto)
if err != nil {
return nil, err
}
s := &NakedSesn{
cfg: cfg,
bx: bx,
mgmtChrs: mgmtChrs,
}
s.init()
return s, nil
}
func (s *NakedSesn) runTask(fn func() error) error {
err := s.tq.Run(fn)
if err == task.InactiveError {
return nmxutil.NewXportError("attempt to use closed BLE session")
}
return err
}
func (s *NakedSesn) shutdown(cause error) error {
initiate := func() error {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.shuttingDown || s.state == NS_STATE_CLOSED ||
s.state == NS_STATE_OPENING_IDLE {
return nmxutil.NewSesnClosedError(
"Attempt to close an already-closed session")
}
s.shuttingDown = true
return nil
}
if err := initiate(); err != nil {
return err
}
defer func() {
s.mtx.Lock()
defer s.mtx.Unlock()
s.shuttingDown = false
}()
// Stop the task queue to flush all pending events.
s.tq.StopNoWait(cause)
s.conn.Stop()
if s.IsOpen() {
s.bx.RemoveSesn(s.conn.connHandle)
}
// Signal error to all listeners.
s.txvr.ErrorAll(cause)
s.txvr.Stop()
// Stop Goroutines associated with notification listeners.
close(s.stopChan)
// Block until close completes.
s.wg.Wait()
// Call the on-close callback if the session was fully open.
s.mtx.Lock()
fullyOpen := s.state == NS_STATE_OPEN
if fullyOpen {
s.state = NS_STATE_CLOSED
} else {
s.state = NS_STATE_OPENING_IDLE
}
s.mtx.Unlock()
if fullyOpen && s.cfg.OnCloseCb != nil {
s.cfg.OnCloseCb(s, cause)
}
return nil
}
func (s *NakedSesn) enqueueShutdown(cause error) chan error {
return s.tq.Enqueue(func() error { return s.shutdown(cause) })
}
func (s *NakedSesn) initiateSecurity() error {
if err := s.conn.InitiateSecurity(); err != nil {
if serr := ToSecurityErr(err); serr != nil {
return serr
} else {
return err
}
}
return nil
}
func (s *NakedSesn) Open() error {
initiate := func() error {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.state != NS_STATE_CLOSED {
return nmxutil.NewSesnAlreadyOpenError(
"Attempt to open an already-open BLE session")
}
s.state = NS_STATE_OPENING_ACTIVE
return nil
}
if err := initiate(); err != nil {
return err
}
var err error
for i := 0; i < s.cfg.Ble.Central.ConnTries; i++ {
var retry bool
retry, err = s.openOnce()
if err != nil {
s.shutdown(err)
}
if !retry {
break
}
}
if err != nil {
s.mtx.Lock()
s.state = NS_STATE_CLOSED
s.mtx.Unlock()
return err
}
s.bx.AddSesn(s.conn.connHandle, s)
s.mtx.Lock()
s.state = NS_STATE_OPEN
s.mtx.Unlock()
return nil
}
func (s *NakedSesn) OpenConnected(
connHandle uint16, eventListener *Listener) error {
initiate := func() error {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.state != NS_STATE_CLOSED {
return nmxutil.NewSesnAlreadyOpenError(
"Attempt to open an already-open BLE session")
}
s.state = NS_STATE_OPENING_ACTIVE
return nil
}
if err := initiate(); err != nil {
return err
}
defer func() {
s.mtx.Lock()
defer s.mtx.Unlock()
s.state = NS_STATE_CLOSED
}()
if err := s.init(); err != nil {
return err
}
if err := s.conn.Inherit(connHandle, eventListener); err != nil {
return err
}
// Listen for disconnect in the background.
s.disconnectListen()
// Listen for incoming notifications in the background.
s.notifyListen()
// Listen for authentication IO requests in the background.
s.smIoDemandListen()
if s.cfg.Ble.EncryptWhen == BLE_ENCRYPT_ALWAYS {
if err := s.initiateSecurity(); err != nil {
return err
}
}
// Give a record of this open session to the transport.
s.bx.AddSesn(connHandle, s)
s.mtx.Lock()
s.state = NS_STATE_OPEN
s.mtx.Unlock()
return nil
}
func (s *NakedSesn) failIfNotOpen() error {
if !s.IsOpen() {
return nmxutil.NewSesnClosedError("Attempt to use closed session")
}
return nil
}
func (s *NakedSesn) TxRxMgmt(m *nmp.NmpMsg,
timeout time.Duration) (nmp.NmpRsp, error) {
if err := s.failIfNotOpen(); err != nil {
return nil, err
}
var rsp nmp.NmpRsp
fn := func() error {
chr, err := s.getChr(s.mgmtChrs.NmpReqChr)
if err != nil {
return err
}
txRaw := func(b []byte) error {
if s.cfg.Ble.WriteRsp {
return s.conn.WriteChr(chr, b, "nmp")
} else {
return s.conn.WriteChrNoRsp(chr, b, "nmp")
}
}
rsp, err = s.txvr.TxRxMgmt(txRaw, m, s.MtuOut(), timeout)
return err
}
if err := s.runTask(fn); err != nil {
return nil, err
}
return rsp, nil
}
func (s *NakedSesn) ListenCoap(
mc nmcoap.MsgCriteria) (*nmcoap.Listener, error) {
return s.txvr.ListenCoap(mc)
}
func (s *NakedSesn) StopListenCoap(mc nmcoap.MsgCriteria) {
s.txvr.StopListenCoap(mc)
}
func (s *NakedSesn) TxCoap(m coap.Message) error {
if err := s.failIfNotOpen(); err != nil {
return err
}
fn := func() error {
chr, err := s.getChr(s.mgmtChrs.ResReqChr)
if err != nil {
return err
}
txRaw := func(b []byte) error {
if s.cfg.Ble.WriteRsp {
return s.conn.WriteChr(chr, b, "coap")
} else {
return s.conn.WriteChrNoRsp(chr, b, "coap")
}
}
return s.txvr.TxCoap(txRaw, m, s.MtuOut())
}
return s.runTask(fn)
}
func (s *NakedSesn) AbortRx(seq uint8) error {
if err := s.failIfNotOpen(); err != nil {
return err
}
fn := func() error {
s.txvr.AbortRx(seq)
return nil
}
return s.runTask(fn)
}
func (s *NakedSesn) Close() error {
if err := s.failIfNotOpen(); err != nil {
return err
}
fn := func() error {
return s.shutdown(fmt.Errorf("BLE session manually closed"))
}
return s.runTask(fn)
}
func (s *NakedSesn) IsOpen() bool {
s.mtx.Lock()
defer s.mtx.Unlock()
return s.state == NS_STATE_OPEN
}
func (s *NakedSesn) MtuIn() int {
return int(s.conn.AttMtu()) - NOTIFY_CMD_BASE_SZ
}
func (s *NakedSesn) MtuOut() int {
return util.IntMin(s.MtuIn(), BLE_ATT_ATTR_MAX_LEN)
}
func (s *NakedSesn) CoapIsTcp() bool {
return true
}
func (s *NakedSesn) MgmtProto() sesn.MgmtProto {
return s.cfg.MgmtProto
}
func (s *NakedSesn) ConnInfo() (BleConnDesc, error) {
if err := s.failIfNotOpen(); err != nil {
return BleConnDesc{}, err
}
return s.conn.ConnInfo(), nil
}
func (s *NakedSesn) SetOobKey(key []byte) {
s.smIo.Oob = key
}
func (s *NakedSesn) openOnce() (bool, error) {
s.mtx.Lock()
s.state = NS_STATE_OPENING_ACTIVE
s.mtx.Unlock()
if err := s.init(); err != nil {
return false, err
}
// Listen for disconnect in the background.
s.disconnectListen()
if err := s.conn.Connect(
s.cfg.Ble.OwnAddrType,
s.cfg.PeerSpec.Ble,
s.cfg.Ble.Central.ConnTimeout); err != nil {
// An ENOTCONN error code implies the "conn_find" request failed
// because the connection dropped immediately after being established.
// If this happened, retry the connect procedure.
bhdErr := nmxutil.ToBleHost(err)
retry := bhdErr != nil && bhdErr.Status == ERR_CODE_ENOTCONN
return retry, err
}
if err := s.conn.ExchangeMtu(); err != nil {
// An ENOTCONN error code implies the connection dropped before the
// first ACL data transmission. If this happened, retry the connect
// procedure.
bhdErr := nmxutil.ToBleHost(err)
retry := bhdErr != nil && bhdErr.Status == ERR_CODE_ENOTCONN
return retry, err
}
if err := s.conn.DiscoverSvcs(); err != nil {
return false, err
}
if chr, _ := s.getChr(s.mgmtChrs.NmpRspChr); chr != nil {
if chr.SubscribeType() != 0 {
if err := s.conn.Subscribe(chr); err != nil {
return false, err
}
}
}
// Listen for incoming notifications in the background.
s.notifyListen()
// Listen for authentication IO requests in the background.
s.smIoDemandListen()
if s.cfg.Ble.EncryptWhen == BLE_ENCRYPT_ALWAYS {
if err := s.initiateSecurity(); err != nil {
return false, err
}
}
return false, nil
}
func (s *NakedSesn) smRespondIo(dmnd SmIoDemand) error {
io := SmIo{
Action: dmnd.Action,
}
switch dmnd.Action {
case BLE_SM_ACTION_OOB:
if s.smIo.Oob == nil {
return fmt.Errorf("OOB key requested but none configured; " +
"allowing pairing procedure to time out")
}
io.Oob = s.smIo.Oob
case BLE_SM_ACTION_INPUT, BLE_SM_ACTION_DISP, BLE_SM_ACTION_NUMCMP:
return fmt.Errorf("Unsupported SM IO method requested: %s",
io.Action.String())
default:
return fmt.Errorf("Unknown SM IO method requested: %v", io.Action)
}
return s.conn.SmInjectIo(io)
}
// Listens for disconnect in the background.
func (s *NakedSesn) disconnectListen() {
discChan := s.conn.DisconnectChan()
// Terminates on:
// * Receive from connection disconnect-channel.
s.wg.Add(1)
go func() {
defer s.wg.Done()
// Block until disconnect.
err := <-discChan
s.enqueueShutdown(err)
}()
}
func (s *NakedSesn) smIoDemandListen() {
// Terminates on:
// * Receive from stop channel.
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
select {
case dmnd, ok := <-s.conn.SmIoDemandChan():
if ok {
log.Debugf("Received SM IO demand for %s",
dmnd.Action.String())
s.smRespondIo(dmnd)
}
case <-s.stopChan:
return
}
}
}()
}
func (s *NakedSesn) getChr(chrId *BleChrId) (*Characteristic, error) {
if chrId == nil {
return nil, fmt.Errorf("BLE session not configured with required " +
"characteristic")
}
chr := s.conn.Profile().FindChrByUuid(*chrId)
if chr == nil {
return nil, fmt.Errorf("BLE peer doesn't support required "+
"characteristic: %s", chrId.String())
}
return chr, nil
}
func (s *NakedSesn) createNotifyListener(chrId *BleChrId) (
*NotifyListener, error) {
chr, err := s.getChr(chrId)
if err != nil {
return nil, err
}
return s.conn.ListenForNotifications(chr)
}
func (s *NakedSesn) notifyListenOnce(chrId *BleChrId,
dispatchCb func(b []byte)) {
nl, err := s.createNotifyListener(chrId)
if err != nil {
log.Debugf("error listening for notifications: %s", err.Error())
return
}
stopChan := s.stopChan
// Terminates on:
// * Notify listener error.
// * Receive from stop channel.
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
select {
case <-nl.ErrChan:
return
case n, ok := <-nl.NotifyChan:
if ok {
dispatchCb(n.Data)
}
case <-stopChan:
return
}
}
}()
}
func (s *NakedSesn) notifyListen() {
s.notifyListenOnce(s.mgmtChrs.ResRspChr, s.txvr.DispatchCoap)
s.notifyListenOnce(s.mgmtChrs.NmpRspChr, s.txvr.DispatchNmpRsp)
}
func (s *NakedSesn) RxAccept() (sesn.Sesn, *sesn.SesnCfg, error) {
return nil, nil, fmt.Errorf("Op not implemented yet")
}
func (s *NakedSesn) RxCoap(opt sesn.TxOptions) (coap.Message, error) {
return nil, fmt.Errorf("Op not implemented yet")
}
func (s *NakedSesn) Filters() (nmcoap.MsgFilter, nmcoap.MsgFilter) {
return s.txvr.Filters()
}
func (s *NakedSesn) SetFilters(txFilter nmcoap.MsgFilter,
rxFilter nmcoap.MsgFilter) {
s.txvr.SetFilters(txFilter, rxFilter)
}