blob: 175ee448e50bbed6fff5a2f43bb954097efc4357 [file] [log] [blame]
// +build !windows
/**
* 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 bll
import (
"fmt"
"sync"
log "github.com/Sirupsen/logrus"
"github.com/go-ble/ble"
"github.com/runtimeco/go-coap"
"golang.org/x/net/context"
"mynewt.apache.org/newt/util"
"mynewt.apache.org/newtmgr/newtmgr/nmutil"
"mynewt.apache.org/newtmgr/nmxact/bledefs"
"mynewt.apache.org/newtmgr/nmxact/mgmt"
"mynewt.apache.org/newtmgr/nmxact/nmble"
"mynewt.apache.org/newtmgr/nmxact/nmcoap"
"mynewt.apache.org/newtmgr/nmxact/nmp"
"mynewt.apache.org/newtmgr/nmxact/nmxutil"
"mynewt.apache.org/newtmgr/nmxact/sesn"
)
// A session that uses the host machine's native BLE support.
type BllSesn struct {
cfg BllSesnCfg
// The native BLE client. All accesses must be protected by the mutex.
cln ble.Client
txvr *mgmt.Transceiver
mtx sync.Mutex
attMtu uint16
nmpReqChr *ble.Characteristic
nmpRspChr *ble.Characteristic
publicReqChr *ble.Characteristic
publicRspChr *ble.Characteristic
unauthReqChr *ble.Characteristic
unauthRspChr *ble.Characteristic
secureReqChr *ble.Characteristic
secureRspChr *ble.Characteristic
txFilterCb nmcoap.MsgFilter
rxFilterCb nmcoap.MsgFilter
}
func NewBllSesn(cfg BllSesnCfg) *BllSesn {
return &BllSesn{
cfg: cfg,
txFilterCb: cfg.TxFilterCb,
rxFilterCb: cfg.RxFilterCb,
}
}
func (s *BllSesn) getCln() (ble.Client, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.cln == nil {
return nil, fmt.Errorf("disconnected")
}
return s.cln, nil
}
func (s *BllSesn) setCln(c ble.Client) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.cln = c
}
func (s *BllSesn) listenDisconnect() {
go func() {
cln, err := s.getCln()
if err != nil {
return
}
<-cln.Disconnected()
s.txvr.ErrorAll(fmt.Errorf("disconnected"))
s.txvr.Stop()
s.setCln(nil)
}()
}
func (s *BllSesn) txConnect(f ble.AdvFilter) (ble.Client, error) {
ctx := ble.WithSigHandler(context.WithTimeout(context.Background(),
s.cfg.ConnTimeout))
client, err := ble.Connect(ctx, s.cfg.AdvFilter)
if err != nil {
if nmutil.ErrorCausedBy(err, context.DeadlineExceeded) {
return nil, fmt.Errorf("Failed to connect to peer after %s",
s.cfg.ConnTimeout.String())
} else {
return nil, err
}
}
return client, nil
}
func (s *BllSesn) txDiscoverProfile() (*ble.Profile, error) {
cln, err := s.getCln()
if err != nil {
return nil, err
}
return cln.DiscoverProfile(true)
}
func (s *BllSesn) txSubscribe(
c *ble.Characteristic,
ind bool,
fn ble.NotificationHandler) error {
cln, err := s.getCln()
if err != nil {
return err
}
return cln.Subscribe(c, ind, fn)
}
func (s *BllSesn) txExchangeMtu(mtu uint16) (uint16, error) {
cln, err := s.getCln()
if err != nil {
return 0, err
}
return exchangeMtu(cln, uint16(mtu))
}
func (s *BllSesn) txCancelConnection() error {
cln, err := s.getCln()
if err != nil {
return err
}
return cln.CancelConnection()
}
func (s *BllSesn) txWriteCharacteristic(
c *ble.Characteristic,
b []byte,
noRsp bool) error {
cln, err := s.getCln()
if err != nil {
return err
}
return cln.WriteCharacteristic(c, b, noRsp)
}
func (s *BllSesn) connect() error {
log.Debugf("Connecting to peer")
cln, err := s.txConnect(s.cfg.AdvFilter)
if err != nil {
if nmutil.ErrorCausedBy(err, context.DeadlineExceeded) {
return fmt.Errorf("Failed to connect to peer after %s",
s.cfg.ConnTimeout.String())
} else {
return err
}
}
s.setCln(cln)
s.listenDisconnect()
return nil
}
func findChr(profile *ble.Profile, chrId *bledefs.BleChrId) (
*ble.Characteristic, error) {
if chrId == nil {
return nil, fmt.Errorf("BLE session not configured with required " +
"characteristic")
}
for _, s := range profile.Services {
uuid, err := UuidFromBllUuid(s.UUID)
if err != nil {
return nil, err
}
if bledefs.CompareUuids(uuid, chrId.SvcUuid) == 0 {
for _, c := range s.Characteristics {
uuid, err := UuidFromBllUuid(c.UUID)
if err != nil {
return nil, err
}
if bledefs.CompareUuids(uuid, chrId.ChrUuid) == 0 {
return c, nil
}
}
}
}
return nil, nil
}
func (s *BllSesn) discoverAll() error {
log.Debugf("Discovering profile")
p, err := s.txDiscoverProfile()
if err != nil {
return err
}
mgmtChrs, err := nmble.BuildMgmtChrs(s.cfg.MgmtProto)
if err != nil {
return err
}
s.nmpReqChr, _ = findChr(p, mgmtChrs.NmpReqChr)
s.nmpRspChr, _ = findChr(p, mgmtChrs.NmpRspChr)
s.publicReqChr, _ = findChr(p, mgmtChrs.ResPublicReqChr)
s.publicRspChr, _ = findChr(p, mgmtChrs.ResPublicRspChr)
s.unauthReqChr, _ = findChr(p, mgmtChrs.ResUnauthReqChr)
s.unauthRspChr, _ = findChr(p, mgmtChrs.ResUnauthRspChr)
s.secureReqChr, _ = findChr(p, mgmtChrs.ResSecureReqChr)
s.secureRspChr, _ = findChr(p, mgmtChrs.ResSecureRspChr)
return nil
}
// Subscribes to the peer's characteristic implementing NMP.
func (s *BllSesn) subscribe() error {
log.Debugf("Subscribing to NMP response characteristic")
onNotify := func(data []byte) {
s.txvr.DispatchNmpRsp(data)
}
if s.nmpRspChr != nil {
if err := s.txSubscribe(s.nmpRspChr, false, onNotify); err != nil {
return err
}
}
if s.publicRspChr != nil {
if err := s.txSubscribe(s.publicRspChr, false, onNotify); err != nil {
return err
}
}
if s.unauthRspChr != nil {
if err := s.txSubscribe(s.unauthRspChr, false, onNotify); err != nil {
return err
}
}
if s.secureRspChr != nil {
if err := s.txSubscribe(s.secureRspChr, false, onNotify); err != nil {
return err
}
}
return nil
}
func (s *BllSesn) exchangeMtu() error {
mtu, err := s.txExchangeMtu(s.cfg.PreferredMtu)
if err != nil {
return err
}
s.attMtu = mtu
return nil
}
// @return bool Whether to retry the open attempt; false
// on success.
// error The cause of a failed open; nil on success.
func (s *BllSesn) openOnce() (bool, error) {
if s.IsOpen() {
return false, nmxutil.NewSesnAlreadyOpenError(
"Attempt to open an already-open bll session")
}
txvr, err := mgmt.NewTransceiver(s.txFilterCb, s.rxFilterCb, true, s.cfg.MgmtProto, 3)
if err != nil {
return false, err
}
s.txvr = txvr
if err := s.connect(); err != nil {
return false, err
}
if err := s.exchangeMtu(); err != nil {
return true, err
}
if err := s.discoverAll(); err != nil {
return false, err
}
if err := s.subscribe(); err != nil {
return false, err
}
return false, nil
}
func (s *BllSesn) Open() error {
var err error
for i := 0; i < s.cfg.ConnTries; i++ {
var retry bool
retry, err = s.openOnce()
if err != nil {
// Ensure the session is closed.
s.Close()
}
if !retry {
break
}
}
return err
}
func (s *BllSesn) Close() error {
if !s.IsOpen() {
return nmxutil.NewSesnClosedError(
"Attempt to close an unopened bll session")
}
if err := s.txCancelConnection(); err != nil {
return err
}
s.setCln(nil)
return nil
}
// Indicates whether the session is currently open.
func (s *BllSesn) IsOpen() bool {
cln, _ := s.getCln()
return cln != nil
}
// Retrieves the maximum data payload for incoming NMP responses.
func (s *BllSesn) MtuIn() int {
return int(s.attMtu) - nmble.NOTIFY_CMD_BASE_SZ
}
// Retrieves the maximum data payload for outgoing NMP requests.
func (s *BllSesn) MtuOut() int {
return util.IntMin(s.MtuIn(), bledefs.BLE_ATT_ATTR_MAX_LEN)
}
// Stops a receive operation in progress. This must be called from a
// separate thread, as sesn receive operations are blocking.
func (s *BllSesn) AbortRx(nmpSeq uint8) error {
s.txvr.ErrorOne(nmpSeq, fmt.Errorf("Rx aborted"))
return nil
}
func (s *BllSesn) RxAccept() (sesn.Sesn, *sesn.SesnCfg, error) {
return nil, nil, fmt.Errorf("Op not implemented yet")
}
func (s *BllSesn) RxCoap(opt sesn.TxOptions) (coap.Message, error) {
return nil, fmt.Errorf("Op not implemented yet")
}
// Performs a blocking transmit a single NMP message and listens for the
// response.
// * nil: success.
// * nmxutil.SesnClosedError: session not open.
// * other error
func (s *BllSesn) TxNmpOnce(msg *nmp.NmpMsg, opt sesn.TxOptions) (
nmp.NmpRsp, error) {
if !s.IsOpen() {
return nil, nmxutil.NewSesnClosedError(
"Attempt to transmit over closed BLE session")
}
if s.nmpReqChr == nil || s.nmpRspChr == nil {
return nil, fmt.Errorf("Cannot send NMP request; peer doesn't " +
"support request or response characteristic")
}
txRaw := func(b []byte) error {
return s.txWriteCharacteristic(s.nmpReqChr, b, true)
}
return s.txvr.TxNmp(txRaw, msg, s.MtuOut(), opt.Timeout)
}
func (s *BllSesn) resReqChr(resType sesn.ResourceType) (
*ble.Characteristic, error) {
m := map[sesn.ResourceType]*ble.Characteristic{
sesn.RES_TYPE_PUBLIC: s.publicReqChr,
sesn.RES_TYPE_UNAUTH: s.unauthReqChr,
sesn.RES_TYPE_SECURE: s.secureReqChr,
}
chr := m[resType]
if chr == nil {
return nil, fmt.Errorf("BLE session not configured with "+
"characteristic for %s resources", resType)
}
return chr, nil
}
func (s *BllSesn) TxCoapOnce(m coap.Message, resType sesn.ResourceType,
opt sesn.TxOptions) (coap.COAPCode, []byte, error) {
chr, err := s.resReqChr(resType)
if err != nil {
return 0, nil, err
}
txRaw := func(b []byte) error {
return s.txWriteCharacteristic(chr, b, !s.cfg.WriteRsp)
}
rsp, err := s.txvr.TxOic(txRaw, m, s.MtuOut(), opt.Timeout)
if err != nil {
return 0, nil, err
} else if rsp == nil {
return 0, nil, nil
} else {
return rsp.Code(), rsp.Payload(), nil
}
}
func (s *BllSesn) TxCoapObserve(m coap.Message, resType sesn.ResourceType,
opt sesn.TxOptions, NotifCb sesn.GetNotifyCb, stopsignal chan int) (coap.COAPCode, []byte, []byte, error) {
chr, err := s.resReqChr(resType)
if err != nil {
return 0, nil, nil, err
}
txRaw := func(b []byte) error {
return s.txWriteCharacteristic(chr, b, !s.cfg.WriteRsp)
}
rsp, err := s.txvr.TxOicObserve(txRaw, m, s.MtuOut(), opt.Timeout, NotifCb, stopsignal)
if err != nil {
return 0, nil, nil, err
} else if rsp == nil {
return 0, nil, nil, nil
} else {
return rsp.Code(), rsp.Payload(), rsp.Token(), nil
}
}
func (s *BllSesn) MgmtProto() sesn.MgmtProto {
return s.cfg.MgmtProto
}
func (s *BllSesn) CoapIsTcp() bool {
return true
}
func (s *BllSesn) Filters() (nmcoap.MsgFilter, nmcoap.MsgFilter) {
return s.txFilterCb, s.rxFilterCb
}