You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mynewt.apache.org by cc...@apache.org on 2017/08/04 19:28:12 UTC

[mynewt-newtmgr] 02/06: nmxact - GATT server

This is an automated email from the ASF dual-hosted git repository.

ccollins pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newtmgr.git

commit e119e6be463ee104d25e4f09f1cdb755f1ee73bf
Author: Christopher Collins <cc...@apache.org>
AuthorDate: Fri Aug 4 12:06:31 2017 -0700

    nmxact - GATT server
---
 newtmgr/bll/bll_xport.go                           |   5 +
 .../newtmgr/nmxact/bledefs/bledefs.go              |   7 +-
 nmxact/adv/adv.go                                  |  43 ++++
 nmxact/bledefs/bledefs.go                          | 168 ++++++++++++++-
 nmxact/example/ble_adv/ble_adv                     | Bin 0 -> 14025652 bytes
 .../{ble_loop/ble_loop.go => ble_adv/ble_adv.go}   |  85 +++-----
 nmxact/example/ble_adv/bleholog                    |   0
 nmxact/example/ble_loop/ble_loop.go                |   6 +-
 nmxact/nmble/ble_act.go                            | 165 +++++++++++++--
 nmxact/nmble/ble_advertiser.go                     | 214 +++++++++++++++++++
 nmxact/nmble/ble_coap_sesn.go                      | 176 ++++++++++++++++
 nmxact/nmble/ble_fsm.go                            | 105 ++++++----
 nmxact/nmble/ble_proto.go                          | 226 ++++++++++++++++++---
 nmxact/nmble/ble_util.go                           | 138 +++++++++++++
 nmxact/nmble/ble_xport.go                          |  50 ++++-
 nmxact/nmble/chrmgr.go                             | 122 +++++++++++
 nmxact/nmble/dispatch.go                           |  20 ++
 nmxact/nmserial/serial_xport.go                    |   5 +
 nmxact/nmxutil/nmxutil.go                          |   6 +-
 nmxact/scan/scan.go                                |   1 -
 nmxact/sesn/sesn.go                                |   2 +
 nmxact/sesn/sesn_cfg.go                            |  19 --
 nmxact/udp/udp_xport.go                            |   5 +
 nmxact/xport/xport.go                              |   2 +
 24 files changed, 1403 insertions(+), 167 deletions(-)

diff --git a/newtmgr/bll/bll_xport.go b/newtmgr/bll/bll_xport.go
index aa3519f..bb17e17 100644
--- a/newtmgr/bll/bll_xport.go
+++ b/newtmgr/bll/bll_xport.go
@@ -25,6 +25,7 @@ import (
 	"github.com/currantlabs/ble"
 	"github.com/currantlabs/ble/examples/lib/dev"
 
+	"mynewt.apache.org/newtmgr/nmxact/adv"
 	"mynewt.apache.org/newtmgr/nmxact/scan"
 	"mynewt.apache.org/newtmgr/nmxact/sesn"
 )
@@ -93,3 +94,7 @@ func (bx *BllXport) BuildScanner() (scan.Scanner, error) {
 func (bx *BllXport) Tx(data []byte) error {
 	return fmt.Errorf("BllXport.Tx() not supported")
 }
+
+func (bx *BllXport) BuildAdvertiser() (adv.Advertiser, error) {
+	return nil, fmt.Errorf("BllXport#BuildAdvertiser unsupported")
+}
diff --git a/newtmgr/vendor/mynewt.apache.org/newtmgr/nmxact/bledefs/bledefs.go b/newtmgr/vendor/mynewt.apache.org/newtmgr/nmxact/bledefs/bledefs.go
index eeb310f..3541ac2 100644
--- a/newtmgr/vendor/mynewt.apache.org/newtmgr/nmxact/bledefs/bledefs.go
+++ b/newtmgr/vendor/mynewt.apache.org/newtmgr/nmxact/bledefs/bledefs.go
@@ -526,9 +526,10 @@ const (
 )
 
 var BleAdvFilterPolicyStringMap = map[BleAdvFilterPolicy]string{
-	BLE_ADV_FILTER_POLICY_NON: "non",
-	BLE_ADV_FILTER_POLICY_LTD: "ltd",
-	BLE_ADV_FILTER_POLICY_GEN: "gen",
+	BLE_ADV_FILTER_POLICY_NONE: "none",
+	BLE_ADV_FILTER_POLICY_SCAN: "scan",
+	BLE_ADV_FILTER_POLICY_CONN: "conn",
+	BLE_ADV_FILTER_POLICY_BOTH: "both",
 }
 
 func BleAdvFilterPolicyToString(discMode BleAdvFilterPolicy) string {
diff --git a/nmxact/adv/adv.go b/nmxact/adv/adv.go
new file mode 100644
index 0000000..859d6ea
--- /dev/null
+++ b/nmxact/adv/adv.go
@@ -0,0 +1,43 @@
+package adv
+
+import (
+	"mynewt.apache.org/newtmgr/nmxact/bledefs"
+	"mynewt.apache.org/newtmgr/nmxact/sesn"
+)
+
+type CfgBle struct {
+	// Mandatory
+	OwnAddrType   bledefs.BleAddrType
+	ConnMode      bledefs.BleAdvConnMode
+	DiscMode      bledefs.BleAdvDiscMode
+	ItvlMin       uint16
+	ItvlMax       uint16
+	ChannelMap    uint8
+	FilterPolicy  bledefs.BleAdvFilterPolicy
+	HighDutyCycle bool
+	AdvFields     bledefs.BleAdvFields
+	RspFields     bledefs.BleAdvFields
+	SesnCfg       sesn.SesnCfg
+
+	// Only required for direct advertisements
+	PeerAddr *bledefs.BleAddr
+}
+
+type Cfg struct {
+	Ble CfgBle
+}
+
+type Advertiser interface {
+	Start(cfg Cfg) (sesn.Sesn, error)
+	Stop() error
+}
+
+func NewCfg() Cfg {
+	return Cfg{
+		Ble: CfgBle{
+			OwnAddrType: bledefs.BLE_ADDR_TYPE_RANDOM,
+			ConnMode:    bledefs.BLE_ADV_CONN_MODE_UND,
+			DiscMode:    bledefs.BLE_ADV_DISC_MODE_GEN,
+		},
+	}
+}
diff --git a/nmxact/bledefs/bledefs.go b/nmxact/bledefs/bledefs.go
index eeb310f..0a0ece3 100644
--- a/nmxact/bledefs/bledefs.go
+++ b/nmxact/bledefs/bledefs.go
@@ -520,15 +520,17 @@ func (a *BleAdvDiscMode) UnmarshalJSON(data []byte) error {
 type BleAdvFilterPolicy int
 
 const (
-	BLE_ADV_FILTER_POLICY_NON BleAdvFilterPolicy = iota
-	BLE_ADV_FILTER_POLICY_LTD
-	BLE_ADV_FILTER_POLICY_GEN
+	BLE_ADV_FILTER_POLICY_NONE BleAdvFilterPolicy = iota
+	BLE_ADV_FILTER_POLICY_SCAN
+	BLE_ADV_FILTER_POLICY_CONN
+	BLE_ADV_FILTER_POLICY_BOTH
 )
 
 var BleAdvFilterPolicyStringMap = map[BleAdvFilterPolicy]string{
-	BLE_ADV_FILTER_POLICY_NON: "non",
-	BLE_ADV_FILTER_POLICY_LTD: "ltd",
-	BLE_ADV_FILTER_POLICY_GEN: "gen",
+	BLE_ADV_FILTER_POLICY_NONE: "none",
+	BLE_ADV_FILTER_POLICY_SCAN: "scan",
+	BLE_ADV_FILTER_POLICY_CONN: "conn",
+	BLE_ADV_FILTER_POLICY_BOTH: "both",
 }
 
 func BleAdvFilterPolicyToString(discMode BleAdvFilterPolicy) string {
@@ -606,6 +608,13 @@ type BleAdvReport struct {
 type BleAdvRptFn func(r BleAdvReport)
 type BleAdvPredicate func(adv BleAdvReport) bool
 
+type BleRole int
+
+const (
+	BLE_ROLE_MASTER BleRole = iota
+	BLE_ROLE_SLAVE
+)
+
 type BleConnDesc struct {
 	ConnHandle      uint16
 	OwnIdAddrType   BleAddrType
@@ -616,6 +625,7 @@ type BleConnDesc struct {
 	PeerIdAddr      BleAddr
 	PeerOtaAddrType BleAddrType
 	PeerOtaAddr     BleAddr
+	Role            BleRole
 }
 
 func (d *BleConnDesc) String() string {
@@ -640,3 +650,149 @@ const (
 	BLE_ENCRYPT_PRIV_ONLY
 	BLE_ENCRYPT_ALWAYS
 )
+
+type BleGattOp int
+
+const (
+	BLE_GATT_ACCESS_OP_READ_CHR  BleGattOp = 0
+	BLE_GATT_ACCESS_OP_WRITE_CHR           = 1
+	BLE_GATT_ACCESS_OP_READ_DSC            = 2
+	BLE_GATT_ACCESS_OP_WRITE_DSC           = 3
+)
+
+var BleGattOpStringMap = map[BleGattOp]string{
+	BLE_GATT_ACCESS_OP_READ_CHR:  "read_chr",
+	BLE_GATT_ACCESS_OP_WRITE_CHR: "write_chr",
+	BLE_GATT_ACCESS_OP_READ_DSC:  "read_dsc",
+	BLE_GATT_ACCESS_OP_WRITE_DSC: "write_dsc",
+}
+
+func BleGattOpToString(op BleGattOp) string {
+	s := BleGattOpStringMap[op]
+	if s == "" {
+		return "???"
+	}
+
+	return s
+}
+
+func BleGattOpFromString(s string) (BleGattOp, error) {
+	for op, name := range BleGattOpStringMap {
+		if s == name {
+			return op, nil
+		}
+	}
+
+	return BleGattOp(0),
+		fmt.Errorf("Invalid BleGattOp string: %s", s)
+}
+
+type BleSvcType int
+
+const (
+	BLE_SVC_TYPE_PRIMARY BleSvcType = iota
+	BLE_SVC_TYPE_SECONDARY
+)
+
+var BleSvcTypeStringMap = map[BleSvcType]string{
+	BLE_SVC_TYPE_PRIMARY:   "primary",
+	BLE_SVC_TYPE_SECONDARY: "secondary",
+}
+
+func BleSvcTypeToString(svcType BleSvcType) string {
+	s := BleSvcTypeStringMap[svcType]
+	if s == "" {
+		return "???"
+	}
+
+	return s
+}
+
+func BleSvcTypeFromString(s string) (BleSvcType, error) {
+	for svcType, name := range BleSvcTypeStringMap {
+		if s == name {
+			return svcType, nil
+		}
+	}
+
+	return BleSvcType(0),
+		fmt.Errorf("Invalid BleSvcType string: %s", s)
+}
+
+func (a BleSvcType) MarshalJSON() ([]byte, error) {
+	return json.Marshal(BleSvcTypeToString(a))
+}
+
+func (a *BleSvcType) UnmarshalJSON(data []byte) error {
+	var err error
+
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+
+	*a, err = BleSvcTypeFromString(s)
+	return err
+}
+
+type BleChrFlags int
+
+const (
+	BLE_GATT_F_BROADCAST       BleChrFlags = 0x0001
+	BLE_GATT_F_READ                        = 0x0002
+	BLE_GATT_F_WRITE_NO_RSP                = 0x0004
+	BLE_GATT_F_WRITE                       = 0x0008
+	BLE_GATT_F_NOTIFY                      = 0x0010
+	BLE_GATT_F_INDICATE                    = 0x0020
+	BLE_GATT_F_AUTH_SIGN_WRITE             = 0x0040
+	BLE_GATT_F_RELIABLE_WRITE              = 0x0080
+	BLE_GATT_F_AUX_WRITE                   = 0x0100
+	BLE_GATT_F_READ_ENC                    = 0x0200
+	BLE_GATT_F_READ_AUTHEN                 = 0x0400
+	BLE_GATT_F_READ_AUTHOR                 = 0x0800
+	BLE_GATT_F_WRITE_ENC                   = 0x1000
+	BLE_GATT_F_WRITE_AUTHEN                = 0x2000
+	BLE_GATT_F_WRITE_AUTHOR                = 0x4000
+)
+
+type BleAttFlags int
+
+const (
+	BLE_ATT_F_READ         BleAttFlags = 0x01
+	BLE_ATT_F_WRITE                    = 0x02
+	BLE_ATT_F_READ_ENC                 = 0x04
+	BLE_ATT_F_READ_AUTHEN              = 0x08
+	BLE_ATT_F_READ_AUTHOR              = 0x10
+	BLE_ATT_F_WRITE_ENC                = 0x20
+	BLE_ATT_F_WRITE_AUTHEN             = 0x40
+	BLE_ATT_F_WRITE_AUTHOR             = 0x80
+)
+
+type BleGattAccess struct {
+	Op      BleGattOp
+	SvcUuid BleUuid
+	ChrUuid BleUuid
+	Data    []byte
+}
+
+type BleGattAccessFn func(access BleGattAccess) uint8
+
+type BleDsc struct {
+	Uuid       BleUuid
+	AttFlags   BleAttFlags
+	MinKeySize int
+}
+
+type BleChr struct {
+	Uuid       BleUuid
+	Flags      BleChrFlags
+	MinKeySize int
+	AccessCb   BleGattAccessFn
+	Dscs       []BleDsc
+}
+
+type BleSvc struct {
+	Uuid    BleUuid
+	SvcType BleSvcType
+	Chrs    []BleChr
+}
diff --git a/nmxact/example/ble_adv/ble_adv b/nmxact/example/ble_adv/ble_adv
new file mode 100755
index 0000000..a3f23f3
Binary files /dev/null and b/nmxact/example/ble_adv/ble_adv differ
diff --git a/nmxact/example/ble_loop/ble_loop.go b/nmxact/example/ble_adv/ble_adv.go
similarity index 51%
copy from nmxact/example/ble_loop/ble_loop.go
copy to nmxact/example/ble_adv/ble_adv.go
index 9e79d29..a82a771 100644
--- a/nmxact/example/ble_loop/ble_loop.go
+++ b/nmxact/example/ble_adv/ble_adv.go
@@ -24,21 +24,20 @@ import (
 	"os"
 	"os/signal"
 	"syscall"
-	"time"
 
 	log "github.com/Sirupsen/logrus"
 
-	"mynewt.apache.org/newtmgr/nmxact/bledefs"
+	"mynewt.apache.org/newt/util"
+	"mynewt.apache.org/newtmgr/nmxact/adv"
 	"mynewt.apache.org/newtmgr/nmxact/nmble"
 	"mynewt.apache.org/newtmgr/nmxact/nmxutil"
 	"mynewt.apache.org/newtmgr/nmxact/sesn"
-	"mynewt.apache.org/newtmgr/nmxact/xact"
 	"mynewt.apache.org/newtmgr/nmxact/xport"
 )
 
 func configExitHandler(x xport.Xport, s sesn.Sesn) {
 	onExit := func() {
-		if s.IsOpen() {
+		if s != nil && s.IsOpen() {
 			s.Close()
 		}
 
@@ -55,20 +54,24 @@ func configExitHandler(x xport.Xport, s sesn.Sesn) {
 			case os.Interrupt, syscall.SIGTERM:
 				onExit()
 				os.Exit(0)
+
+			case syscall.SIGQUIT:
+				util.PrintStacks()
 			}
 		}
 	}()
 }
 
 func main() {
-	//nmxutil.SetLogLevel(log.DebugLevel)
-	nmxutil.SetLogLevel(log.InfoLevel)
+	nmxutil.Debug = true
+	nmxutil.SetLogLevel(log.DebugLevel)
+	//nmxutil.SetLogLevel(log.InfoLevel)
 
 	// Initialize the BLE transport.
 	params := nmble.NewXportCfg()
 	params.SockPath = "/tmp/blehostd-uds"
 	params.BlehostdPath = "blehostd"
-	params.DevPath = "/dev/cu.usbmodem142121"
+	params.DevPath = "/dev/cu.usbmodem142141"
 
 	x, err := nmble.NewBleXport(params)
 	if err != nil {
@@ -85,69 +88,29 @@ func main() {
 	}
 	defer x.Stop()
 
-	// Find a device to connect to:
-	//     * Peer has name "nimble-bleprph"
-	//     * We use a random address.
-	dev, err := nmble.DiscoverDeviceWithName(
-		x, bledefs.BLE_ADDR_TYPE_RANDOM, 10*time.Second, "c4")
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "error discovering device: %s\n", err.Error())
-		os.Exit(1)
-	}
-	if dev == nil {
-		fmt.Fprintf(os.Stderr, "couldn't find device")
-		os.Exit(1)
-	}
-
-	// Prepare a BLE session:
-	//     * Plain NMP (not tunnelled over OIC).
-	//     * We use a random address.
-	sc := sesn.NewSesnCfg()
-	sc.MgmtProto = sesn.MGMT_PROTO_NMP
-	sc.Ble.OwnAddrType = bledefs.BLE_ADDR_TYPE_RANDOM
-	sc.PeerSpec.Ble = *dev
+	configExitHandler(x, nil)
 
-	s, err := x.BuildSesn(sc)
+	advertiser, err := x.BuildAdvertiser()
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "error creating BLE session: %s\n", err.Error())
+		fmt.Fprintf(os.Stderr, "error building BLE advertiser: %s\n",
+			err.Error())
 		os.Exit(1)
 	}
 
-	configExitHandler(x, s)
-
-	// Repeatedly:
-	//     * Connect to peer if unconnected.
-	//     * Send an echo command to peer.
-	//
-	// If blehostd crashes or the controller is unplugged, nmxact should
-	// recover on the next connect attempt.
 	for {
-		if !s.IsOpen() {
-			// Connect to the peer (open the session).
-			if err := s.Open(); err != nil {
-				fmt.Fprintf(os.Stderr, "error starting BLE session: %s\n",
-					err.Error())
-				continue
-			}
-		}
-
-		// Send an echo command to the peer.
-		c := xact.NewEchoCmd()
-		c.Payload = "hello"
-
-		res, err := c.Run(s)
+		ac := adv.NewCfg()
+		ac.Ble.AdvFields.Flags = new(uint8)
+		*ac.Ble.AdvFields.Flags = 6
+		ac.Ble.AdvFields.Name = new(string)
+		*ac.Ble.AdvFields.Name = "gwadv"
+		_, err := advertiser.Start(ac)
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "error executing echo command: %s\n",
+			fmt.Fprintf(os.Stderr, "error starting advertise: %s\n",
 				err.Error())
-			continue
-		}
-
-		if res.Status() != 0 {
-			fmt.Printf("Peer responded negatively to echo command; status=%d\n",
-				res.Status())
+			os.Exit(1)
 		}
 
-		eres := res.(*xact.EchoResult)
-		fmt.Printf("Peer echoed back: %s\n", eres.Rsp.Payload)
+		//s.Close()
+		select {}
 	}
 }
diff --git a/nmxact/example/ble_adv/bleholog b/nmxact/example/ble_adv/bleholog
new file mode 100644
index 0000000..e69de29
diff --git a/nmxact/example/ble_loop/ble_loop.go b/nmxact/example/ble_loop/ble_loop.go
index 9e79d29..0e5bd99 100644
--- a/nmxact/example/ble_loop/ble_loop.go
+++ b/nmxact/example/ble_loop/ble_loop.go
@@ -68,7 +68,7 @@ func main() {
 	params := nmble.NewXportCfg()
 	params.SockPath = "/tmp/blehostd-uds"
 	params.BlehostdPath = "blehostd"
-	params.DevPath = "/dev/cu.usbmodem142121"
+	params.DevPath = "/dev/cu.usbmodem142111"
 
 	x, err := nmble.NewBleXport(params)
 	if err != nil {
@@ -131,6 +131,10 @@ func main() {
 			}
 		}
 
+		if err := s.Open(); err != nil {
+			s.Close()
+		}
+
 		// Send an echo command to the peer.
 		c := xact.NewEchoCmd()
 		c.Payload = "hello"
diff --git a/nmxact/nmble/ble_act.go b/nmxact/nmble/ble_act.go
index 62c30ac..cfa4c14 100644
--- a/nmxact/nmble/ble_act.go
+++ b/nmxact/nmble/ble_act.go
@@ -159,7 +159,7 @@ func connCancel(x *BleXport, bl *Listener, r *BleConnCancelReq) error {
 
 // Blocking.
 func discSvcUuid(x *BleXport, bl *Listener, r *BleDiscSvcUuidReq) (
-	*BleSvc, error) {
+	*BleDiscSvc, error) {
 
 	const rspType = MSG_TYPE_DISC_SVC_UUID
 	const evtType = MSG_TYPE_DISC_SVC_EVT
@@ -173,7 +173,7 @@ func discSvcUuid(x *BleXport, bl *Listener, r *BleDiscSvcUuidReq) (
 		return nil, err
 	}
 
-	var svc *BleSvc
+	var svc *BleDiscSvc
 	for {
 		select {
 		case err := <-bl.ErrChan:
@@ -214,7 +214,7 @@ func discSvcUuid(x *BleXport, bl *Listener, r *BleDiscSvcUuidReq) (
 
 // Blocking.
 func discAllChrs(x *BleXport, bl *Listener, r *BleDiscAllChrsReq) (
-	[]*BleChr, error) {
+	[]*BleDiscChr, error) {
 
 	const rspType = MSG_TYPE_DISC_ALL_CHRS
 	const evtType = MSG_TYPE_DISC_CHR_EVT
@@ -228,7 +228,7 @@ func discAllChrs(x *BleXport, bl *Listener, r *BleDiscAllChrsReq) (
 		return nil, err
 	}
 
-	chrs := []*BleChr{}
+	chrs := []*BleDiscChr{}
 	for {
 		select {
 		case err := <-bl.ErrChan:
@@ -533,37 +533,47 @@ func encInitiate(x *BleXport, bl *Listener,
 }
 
 // Blocking
-func advStart(x *BleXport, bl *Listener, r *BleAdvStartReq) error {
+func advStart(x *BleXport, bl *Listener, r *BleAdvStartReq) (uint16, error) {
 	const rspType = MSG_TYPE_ADV_START
 
 	j, err := json.Marshal(r)
 	if err != nil {
-		return err
+		return 0, err
 	}
 
 	if err := x.Tx(j); err != nil {
-		return err
+		return 0, err
 	}
 
 	for {
 		select {
 		case err := <-bl.ErrChan:
-			return err
+			return 0, err
 
 		case bm := <-bl.MsgChan:
 			switch msg := bm.(type) {
 			case *BleAdvStartRsp:
 				bl.Acked = true
 				if msg.Status != 0 {
-					return StatusError(MSG_OP_RSP, rspType, msg.Status)
+					return 0, StatusError(MSG_OP_RSP, rspType, msg.Status)
+				}
+
+			case *BleConnectEvt:
+				if msg.Status == 0 {
+					return msg.ConnHandle, nil
+				} else {
+					str := fmt.Sprintf("BLE peer failed to connect to us; "+
+						"status=%s (%d)",
+						ErrCodeToString(msg.Status), msg.Status)
+					log.Debugf(str)
+					return 0, nmxutil.NewBleHostError(msg.Status, str)
 				}
-				return nil
 
 			default:
 			}
 
 		case <-bl.AfterTimeout(x.RspTimeout()):
-			return BhdTimeoutError(rspType, r.Seq)
+			return 0, BhdTimeoutError(rspType, r.Seq)
 		}
 	}
 }
@@ -704,7 +714,7 @@ func advFields(x *BleXport, bl *Listener, r *BleAdvFieldsReq) (
 					return nil, StatusError(MSG_OP_RSP, rspType, msg.Status)
 				}
 
-				return msg.Data, nil
+				return msg.Data.Bytes, nil
 
 			default:
 			}
@@ -715,6 +725,136 @@ func advFields(x *BleXport, bl *Listener, r *BleAdvFieldsReq) (
 	}
 }
 
+func clearSvcs(x *BleXport, bl *Listener, r *BleClearSvcsReq) error {
+	const rspType = MSG_TYPE_CLEAR_SVCS
+
+	j, err := json.Marshal(r)
+	if err != nil {
+		return err
+	}
+
+	x.txNoSync(j)
+	for {
+		select {
+		case err := <-bl.ErrChan:
+			return err
+
+		case bm := <-bl.MsgChan:
+			switch msg := bm.(type) {
+			case *BleClearSvcsRsp:
+				bl.Acked = true
+				if msg.Status != 0 {
+					return StatusError(MSG_OP_RSP, rspType, msg.Status)
+				}
+				return nil
+
+			default:
+			}
+
+		case <-bl.AfterTimeout(x.RspTimeout()):
+			return BhdTimeoutError(rspType, r.Seq)
+		}
+	}
+}
+
+func addSvcs(x *BleXport, bl *Listener, r *BleAddSvcsReq) error {
+	const rspType = MSG_TYPE_ADD_SVCS
+
+	j, err := json.Marshal(r)
+	if err != nil {
+		return err
+	}
+
+	x.txNoSync(j)
+	for {
+		select {
+		case err := <-bl.ErrChan:
+			return err
+
+		case bm := <-bl.MsgChan:
+			switch msg := bm.(type) {
+			case *BleAddSvcsRsp:
+				bl.Acked = true
+				if msg.Status != 0 {
+					return StatusError(MSG_OP_RSP, rspType, msg.Status)
+				}
+				return nil
+
+			default:
+			}
+
+		case <-bl.AfterTimeout(x.RspTimeout()):
+			return BhdTimeoutError(rspType, r.Seq)
+		}
+	}
+}
+
+func commitSvcs(x *BleXport, bl *Listener, r *BleCommitSvcsReq) (
+	[]BleRegSvc, error) {
+
+	const rspType = MSG_TYPE_COMMIT_SVCS
+
+	j, err := json.Marshal(r)
+	if err != nil {
+		return nil, err
+	}
+
+	x.txNoSync(j)
+	for {
+		select {
+		case err := <-bl.ErrChan:
+			return nil, err
+
+		case bm := <-bl.MsgChan:
+			switch msg := bm.(type) {
+			case *BleCommitSvcsRsp:
+				bl.Acked = true
+				if msg.Status != 0 {
+					return nil, StatusError(MSG_OP_RSP, rspType, msg.Status)
+				}
+				return msg.Svcs, nil
+
+			default:
+			}
+
+		case <-bl.AfterTimeout(x.RspTimeout()):
+			return nil, BhdTimeoutError(rspType, r.Seq)
+		}
+	}
+}
+
+func accessStatus(x *BleXport, bl *Listener, r *BleAccessStatusReq) error {
+	const rspType = MSG_TYPE_ACCESS_STATUS
+
+	j, err := json.Marshal(r)
+	if err != nil {
+		return err
+	}
+
+	x.Tx(j)
+	for {
+		select {
+		case err := <-bl.ErrChan:
+			return err
+
+		case bm := <-bl.MsgChan:
+			switch msg := bm.(type) {
+			case *BleAccessStatusRsp:
+				bl.Acked = true
+				if msg.Status != 0 {
+					return StatusError(MSG_OP_RSP, rspType, msg.Status)
+				}
+				return nil
+
+			default:
+			}
+
+		case <-bl.AfterTimeout(x.RspTimeout()):
+			return BhdTimeoutError(rspType, r.Seq)
+		}
+	}
+}
+
 // Asks the controller to generate a random address.  This is done when the
 // transport is starting up, and therefore does not require the transport to be
 // synced.  Only the transport should call this function.
@@ -821,6 +961,7 @@ func setPreferredMtu(x *BleXport, bl *Listener,
 
 		case <-bl.AfterTimeout(x.RspTimeout()):
 			return BhdTimeoutError(rspType, r.Seq)
+
 		}
 	}
 }
diff --git a/nmxact/nmble/ble_advertiser.go b/nmxact/nmble/ble_advertiser.go
new file mode 100644
index 0000000..6888009
--- /dev/null
+++ b/nmxact/nmble/ble_advertiser.go
@@ -0,0 +1,214 @@
+package nmble
+
+import (
+	"fmt"
+
+	log "github.com/Sirupsen/logrus"
+
+	"mynewt.apache.org/newtmgr/nmxact/adv"
+	. "mynewt.apache.org/newtmgr/nmxact/bledefs"
+	"mynewt.apache.org/newtmgr/nmxact/nmxutil"
+	"mynewt.apache.org/newtmgr/nmxact/sesn"
+)
+
+type Advertiser struct {
+	bx          *BleXport
+	stopChan    chan struct{}
+	stoppedChan chan struct{}
+}
+
+func NewAdvertiser(bx *BleXport) *Advertiser {
+	return &Advertiser{
+		bx: bx,
+	}
+}
+
+func (a *Advertiser) fields(f BleAdvFields) ([]byte, error) {
+	r := BleAdvFieldsToReq(f)
+
+	bl, err := a.bx.AddListener(SeqKey(r.Seq))
+	if err != nil {
+		return nil, err
+	}
+	defer a.bx.RemoveListener(bl)
+
+	return advFields(a.bx, bl, r)
+}
+
+func (a *Advertiser) setAdvData(data []byte) error {
+	r := NewBleAdvSetDataReq()
+	r.Data = BleBytes{data}
+
+	bl, err := a.bx.AddListener(SeqKey(r.Seq))
+	if err != nil {
+		return err
+	}
+	defer a.bx.RemoveListener(bl)
+
+	if err := advSetData(a.bx, bl, r); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (a *Advertiser) setRspData(data []byte) error {
+	r := NewBleAdvRspSetDataReq()
+	r.Data = BleBytes{data}
+
+	bl, err := a.bx.AddListener(SeqKey(r.Seq))
+	if err != nil {
+		return err
+	}
+	defer a.bx.RemoveListener(bl)
+
+	if err := advRspSetData(a.bx, bl, r); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (a *Advertiser) advertise(cfg adv.Cfg) (uint16, *Listener, error) {
+	r := NewBleAdvStartReq()
+
+	r.OwnAddrType = cfg.Ble.OwnAddrType
+	r.DurationMs = 0x7fffffff
+	r.ConnMode = cfg.Ble.ConnMode
+	r.DiscMode = cfg.Ble.DiscMode
+	r.ItvlMin = cfg.Ble.ItvlMin
+	r.ItvlMax = cfg.Ble.ItvlMax
+	r.ChannelMap = cfg.Ble.ChannelMap
+	r.FilterPolicy = cfg.Ble.FilterPolicy
+	r.HighDutyCycle = cfg.Ble.HighDutyCycle
+	r.PeerAddr = cfg.Ble.PeerAddr
+
+	bl, err := a.bx.AddListener(SeqKey(r.Seq))
+	if err != nil {
+		return 0, nil, err
+	}
+
+	connHandle, err := advStart(a.bx, bl, r)
+	if err != nil {
+		a.bx.RemoveListener(bl)
+		if !nmxutil.IsXport(err) {
+			// The transport did not restart; always attempt to cancel the
+			// advertise operation.  In some cases, the host has already stopped
+			// advertising and will respond with an "ealready" error that can be
+			// ignored.
+			if err := a.stopAdvertising(); err != nil {
+				log.Errorf("Failed to cancel advertise in progress: %s",
+					err.Error())
+			}
+		}
+		return 0, nil, err
+	}
+
+	return connHandle, bl, nil
+}
+
+func (a *Advertiser) stopAdvertising() error {
+	r := NewBleAdvStopReq()
+
+	bl, err := a.bx.AddListener(SeqKey(r.Seq))
+	if err != nil {
+		return err
+	}
+	defer a.bx.RemoveListener(bl)
+
+	return advStop(a.bx, bl, r)
+}
+
+func (a *Advertiser) buildSesn(cfg adv.Cfg, connHandle uint16, bl *Listener) (
+	sesn.Sesn, error) {
+
+	// XXX: Build different kinds of sessions.
+	s := NewBleCoapSesn(a.bx, cfg.Ble.SesnCfg)
+	if err := s.OpenConnected(connHandle, bl); err != nil {
+		return nil, err
+	}
+
+	return s, nil
+}
+
+func (a *Advertiser) Start(cfg adv.Cfg) (sesn.Sesn, error) {
+	var advData []byte
+	var rspData []byte
+	var connHandle uint16
+	var bl *Listener
+	var err error
+
+	fns := []func() error{
+		// Convert advertising fields to data.
+		func() error {
+			advData, err = a.fields(cfg.Ble.AdvFields)
+			return err
+		},
+
+		// Set advertising data.
+		func() error {
+			return a.setAdvData(advData)
+		},
+
+		// Convert response fields to data.
+		func() error {
+			rspData, err = a.fields(cfg.Ble.RspFields)
+			return err
+		},
+
+		// Set response data.
+		func() error {
+			return a.setRspData(rspData)
+		},
+
+		// Advertise
+		func() error {
+			connHandle, bl, err = a.advertise(cfg)
+			return err
+		},
+	}
+
+	a.stopChan = make(chan struct{})
+	a.stoppedChan = make(chan struct{})
+
+	defer func() {
+		a.stopChan = nil
+		close(a.stoppedChan)
+	}()
+
+	if err := a.bx.AcquireSlave(a); err != nil {
+		return nil, err
+	}
+	defer a.bx.ReleaseSlave()
+
+	for _, fn := range fns {
+		// Check for abort before each step.
+		select {
+		case <-a.stopChan:
+			return nil, fmt.Errorf("advertise aborted")
+		default:
+		}
+
+		if err := fn(); err != nil {
+			return nil, err
+		}
+	}
+
+	return a.buildSesn(cfg, connHandle, bl)
+}
+
+func (a *Advertiser) Stop() error {
+	stopChan := a.stopChan
+	if stopChan == nil {
+		return fmt.Errorf("advertiser already stopped")
+	}
+	close(stopChan)
+
+	a.bx.StopWaitingForSlave(a, fmt.Errorf("advertise aborted"))
+	a.stopAdvertising()
+
+	// Block until abort is complete.
+	<-a.stoppedChan
+
+	return nil
+}
diff --git a/nmxact/nmble/ble_coap_sesn.go b/nmxact/nmble/ble_coap_sesn.go
new file mode 100644
index 0000000..a96127e
--- /dev/null
+++ b/nmxact/nmble/ble_coap_sesn.go
@@ -0,0 +1,176 @@
+package nmble
+
+import (
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/runtimeco/go-coap"
+
+	"mynewt.apache.org/newt/util"
+	. "mynewt.apache.org/newtmgr/nmxact/bledefs"
+	"mynewt.apache.org/newtmgr/nmxact/nmp"
+	"mynewt.apache.org/newtmgr/nmxact/nmxutil"
+	"mynewt.apache.org/newtmgr/nmxact/oic"
+	"mynewt.apache.org/newtmgr/nmxact/omp"
+	"mynewt.apache.org/newtmgr/nmxact/sesn"
+)
+
+type BleCoapSesn struct {
+	bf           *BleFsm
+	d            *oic.Dispatcher
+	closeTimeout time.Duration
+	onCloseCb    sesn.OnCloseFn
+	wg           sync.WaitGroup
+
+	closeChan chan struct{}
+}
+
+func NewBleCoapSesn(bx *BleXport, cfg sesn.SesnCfg) *BleCoapSesn {
+	bcs := &BleCoapSesn{
+		closeTimeout: cfg.Ble.CloseTimeout,
+		onCloseCb:    cfg.OnCloseCb,
+	}
+
+	bcs.bf = NewBleFsm(BleFsmParams{
+		Bx:          bx,
+		OwnAddrType: cfg.Ble.OwnAddrType,
+		Central: BleFsmParamsCentral{
+			PeerDev:     cfg.PeerSpec.Ble,
+			ConnTries:   cfg.Ble.Central.ConnTries,
+			ConnTimeout: cfg.Ble.Central.ConnTimeout,
+		},
+
+		EncryptWhen: cfg.Ble.EncryptWhen,
+	})
+
+	return bcs
+}
+
+func (bcs *BleCoapSesn) AbortRx(seq uint8) error {
+	return fmt.Errorf("BleCoapSesn.AbortRx unimplemented")
+}
+
+func (bcs *BleCoapSesn) openInit() {
+	// This channel gets closed when the session closes.
+	bcs.closeChan = make(chan struct{})
+	bcs.d = oic.NewDispatcher(true, 3)
+
+	// Listen for disconnect in the background.
+	bcs.wg.Add(1)
+	go func() {
+		// If the session is being closed, unblock the close() call.
+		defer close(bcs.closeChan)
+
+		// Block until disconnect.
+		<-bcs.bf.DisconnectChan()
+		nmxutil.Assert(!bcs.IsOpen())
+		pd := bcs.bf.PrevDisconnect()
+
+		// Signal error to all listeners.
+		bcs.d.ErrorAll(pd.Err)
+		bcs.wg.Done()
+		bcs.wg.Wait()
+
+		// Only execute the client's disconnect callback if the disconnect was
+		// unsolicited.
+		if pd.Dt != FSM_DISCONNECT_TYPE_REQUESTED && bcs.onCloseCb != nil {
+			bcs.onCloseCb(bcs, pd.Err)
+		}
+	}()
+}
+
+func (bcs *BleCoapSesn) OpenConnected(
+	connHandle uint16, eventListener *Listener) error {
+
+	bcs.openInit()
+
+	if err := bcs.bf.StartConnected(connHandle, eventListener); err != nil {
+		close(bcs.closeChan)
+		return err
+	}
+	return nil
+}
+
+func (bcs *BleCoapSesn) Open() error {
+	bcs.openInit()
+
+	if err := bcs.bf.Start(); err != nil {
+		close(bcs.closeChan)
+		return err
+	}
+	return nil
+}
+
+func (bcs *BleCoapSesn) Close() error {
+	err := bcs.bf.Stop()
+	if err != nil {
+		return err
+	}
+
+	// Block until close completes.
+	<-bcs.closeChan
+	return nil
+}
+
+func (bcs *BleCoapSesn) IsOpen() bool {
+	return bcs.bf.IsOpen()
+}
+
+func (bcs *BleCoapSesn) EncodeNmpMsg(m *nmp.NmpMsg) ([]byte, error) {
+	return nil, fmt.Errorf("BleCoapSesn.EncodeNmpMsg unimplemented")
+}
+
+// Blocking.
+func (bcs *BleCoapSesn) TxNmpOnce(m *nmp.NmpMsg, opt sesn.TxOptions) (
+	nmp.NmpRsp, error) {
+
+	return nil, fmt.Errorf("BleCoapSesn.TxNmpOnce unimplemented")
+}
+
+func (bcs *BleCoapSesn) MtuIn() int {
+	return bcs.bf.attMtu -
+		NOTIFY_CMD_BASE_SZ -
+		omp.OMP_MSG_OVERHEAD -
+		nmp.NMP_HDR_SIZE
+}
+
+func (bcs *BleCoapSesn) MtuOut() int {
+	mtu := bcs.bf.attMtu -
+		WRITE_CMD_BASE_SZ -
+		omp.OMP_MSG_OVERHEAD -
+		nmp.NMP_HDR_SIZE
+	return util.IntMin(mtu, BLE_ATT_ATTR_MAX_LEN)
+}
+
+func (bcs *BleCoapSesn) ConnInfo() (BleConnDesc, error) {
+	return bcs.bf.connInfo()
+}
+
+func (bcs *BleCoapSesn) GetResourceOnce(uri string, opt sesn.TxOptions) (
+	[]byte, error) {
+
+	token := nmxutil.NextToken()
+
+	ol, err := bcs.d.AddListener(token)
+	if err != nil {
+		return nil, err
+	}
+	defer bcs.d.RemoveListener(token)
+
+	req, err := oic.EncodeGet(uri, token)
+	if err != nil {
+		return nil, err
+	}
+
+	rsp, err := bcs.bf.TxOic(req, ol, opt.Timeout)
+	if err != nil {
+		return nil, err
+	}
+
+	if rsp.Code != coap.Content {
+		return nil, fmt.Errorf("UNEXPECTED OIC ACK: %#v", rsp)
+	}
+
+	return rsp.Payload, nil
+}
diff --git a/nmxact/nmble/ble_fsm.go b/nmxact/nmble/ble_fsm.go
index 8d2fd1c..f319db5 100644
--- a/nmxact/nmble/ble_fsm.go
+++ b/nmxact/nmble/ble_fsm.go
@@ -54,9 +54,10 @@ const (
 	SESN_STATE_DISCOVER_CHR                 = 4
 	SESN_STATE_SECURITY                     = 5
 	SESN_STATE_SUBSCRIBE                    = 6
-	SESN_STATE_DONE                         = 7
-	SESN_STATE_TERMINATING                  = 8
-	SESN_STATE_CONN_CANCELLING              = 9
+	SESN_STATE_GET_INFO                     = 7
+	SESN_STATE_DONE                         = 8
+	SESN_STATE_TERMINATING                  = 9
+	SESN_STATE_CONN_CANCELLING              = 10
 )
 
 type BleFsmDisconnectType int
@@ -94,10 +95,11 @@ type BleFsm struct {
 	params BleFsmParams
 
 	connHandle     uint16
+	connDesc       BleConnDesc
 	peerDev        BleDev
-	nmpSvc         *BleSvc
-	nmpReqChr      *BleChr
-	nmpRspChr      *BleChr
+	nmpSvc         *BleDiscSvc
+	nmpReqChr      *BleDiscChr
+	nmpRspChr      *BleDiscChr
 	prevDisconnect BleDisconnectEntry
 	attMtu         int
 	state          BleSesnState
@@ -144,15 +146,6 @@ func (bf *BleFsm) connInfo() (BleConnDesc, error) {
 	return ConnFindXact(bf.params.Bx, bf.connHandle)
 }
 
-func (bf *BleFsm) logConnection() {
-	desc, err := bf.connInfo()
-	if err != nil {
-		return
-	}
-
-	log.Debugf("BLE connection attempt succeeded; %s", desc.String())
-}
-
 func fsmErrorLess(a error, b error) bool {
 	aIsXport := nmxutil.IsXport(a)
 	bIsXport := nmxutil.IsXport(b)
@@ -217,7 +210,7 @@ func (bf *BleFsm) listenForError() {
 }
 
 // Listens for events in the background.
-func (bf *BleFsm) eventListen(bl *Listener, seq BleSeq) error {
+func (bf *BleFsm) eventListen(bl *Listener) error {
 	bf.wg.Add(1)
 
 	go func() {
@@ -354,7 +347,7 @@ func (bf *BleFsm) connect() error {
 	}
 
 	// Listen for events in the background.
-	if err := bf.eventListen(bl, r.Seq); err != nil {
+	if err := bf.eventListen(bl); err != nil {
 		return err
 	}
 
@@ -420,7 +413,7 @@ func (bf *BleFsm) connCancel() error {
 	return nil
 }
 
-func (bf *BleFsm) discSvcUuidOnce(uuid BleUuid) (*BleSvc, error) {
+func (bf *BleFsm) discSvcUuidOnce(uuid BleUuid) (*BleDiscSvc, error) {
 	r := NewBleDiscSvcUuidReq()
 	r.ConnHandle = bf.connHandle
 	r.Uuid = uuid
@@ -539,6 +532,12 @@ func (bf *BleFsm) exchangeMtu() error {
 
 	mtu, err := exchangeMtu(bf.params.Bx, bl, r)
 	if err != nil {
+		// If the operation failed because the peer already initiated the
+		// exchange, just pretend it was successful.
+		bhe := nmxutil.ToBleHost(err)
+		if bhe != nil && bhe.Status == ERR_CODE_EALREADY {
+			return nil
+		}
 		return err
 	}
 
@@ -620,31 +619,43 @@ func (bf *BleFsm) executeState() (bool, error) {
 		bf.state = SESN_STATE_DISCOVER_SVC
 
 	case SESN_STATE_DISCOVER_SVC:
-		if err := bf.discSvcUuid(); err != nil {
-			return false, err
+		if len(bf.params.SvcUuids) > 0 {
+			if err := bf.discSvcUuid(); err != nil {
+				return false, err
+			}
 		}
 		bf.state = SESN_STATE_DISCOVER_CHR
 
 	case SESN_STATE_DISCOVER_CHR:
-		if err := bf.discAllChrs(); err != nil {
-			return false, err
-		}
-		if bf.shouldEncrypt() {
-			bf.state = SESN_STATE_SECURITY
-		} else {
-			bf.state = SESN_STATE_SUBSCRIBE
+		if bf.nmpSvc != nil {
+			if err := bf.discAllChrs(); err != nil {
+				return false, err
+			}
 		}
+		bf.state = SESN_STATE_SECURITY
 
 	case SESN_STATE_SECURITY:
-		if err := bf.encInitiate(); err != nil {
-			return false, err
+		if bf.shouldEncrypt() {
+			if err := bf.encInitiate(); err != nil {
+				return false, err
+			}
 		}
 		bf.state = SESN_STATE_SUBSCRIBE
 
 	case SESN_STATE_SUBSCRIBE:
-		if err := bf.subscribe(); err != nil {
+		if bf.nmpRspChr != nil {
+			if err := bf.subscribe(); err != nil {
+				return false, err
+			}
+		}
+		bf.state = SESN_STATE_GET_INFO
+
+	case SESN_STATE_GET_INFO:
+		desc, err := bf.connInfo()
+		if err != nil {
 			return false, err
 		}
+		bf.connDesc = desc
 		bf.state = SESN_STATE_DONE
 
 	case SESN_STATE_DONE:
@@ -670,13 +681,11 @@ func (bf *BleFsm) PrevDisconnect() BleDisconnectEntry {
 	return bf.prevDisconnect
 }
 
-func (bf *BleFsm) startOnce() (bool, error) {
-	if !bf.IsClosed() {
-		return false, nmxutil.NewSesnAlreadyOpenError(fmt.Sprintf(
-			"Attempt to open an already-open BLE session (state=%d)",
-			bf.state))
-	}
+func (bf *BleFsm) IsCentral() bool {
+	return bf.connDesc.Role == BLE_ROLE_MASTER
+}
 
+func (bf *BleFsm) startOnce() (bool, error) {
 	bf.disconnectChan = make(chan struct{})
 	bf.rxNmpChan = make(chan []byte)
 
@@ -705,6 +714,12 @@ func (bf *BleFsm) startOnce() (bool, error) {
 func (bf *BleFsm) Start() error {
 	var err error
 
+	if !bf.IsClosed() {
+		return nmxutil.NewSesnAlreadyOpenError(fmt.Sprintf(
+			"Attempt to open an already-open BLE session (state=%d)",
+			bf.state))
+	}
+
 	for i := 0; i < bf.params.Central.ConnTries; i++ {
 		var retry bool
 		retry, err = bf.startOnce()
@@ -722,6 +737,24 @@ func (bf *BleFsm) Start() error {
 	return nil
 }
 
+func (bf *BleFsm) StartConnected(
+	connHandle uint16, eventListener *Listener) error {
+
+	bf.connHandle = connHandle
+	if err := bf.eventListen(eventListener); err != nil {
+		return err
+	}
+
+	bf.state = SESN_STATE_EXCHANGE_MTU
+	if _, err := bf.startOnce(); err != nil {
+		nmxutil.Assert(!bf.IsOpen())
+		nmxutil.Assert(bf.IsClosed())
+		return err
+	}
+
+	return nil
+}
+
 // @return bool                 true if stop complete;
 //                              false if disconnect is now pending.
 func (bf *BleFsm) Stop() error {
diff --git a/nmxact/nmble/ble_proto.go b/nmxact/nmble/ble_proto.go
index aa8dc9c..0716be2 100644
--- a/nmxact/nmble/ble_proto.go
+++ b/nmxact/nmble/ble_proto.go
@@ -226,6 +226,46 @@ var HciErrCodeStringMap = map[int]string{
 	ERR_CODE_HCI_COARSE_CLK_ADJ:      "coarse clk adj",
 }
 
+const (
+	ERR_CODE_ATT_INVALID_HANDLE         int = 0x01
+	ERR_CODE_ATT_READ_NOT_PERMITTED         = 0x02
+	ERR_CODE_ATT_WRITE_NOT_PERMITTED        = 0x03
+	ERR_CODE_ATT_INVALID_PDU                = 0x04
+	ERR_CODE_ATT_INSUFFICIENT_AUTHEN        = 0x05
+	ERR_CODE_ATT_REQ_NOT_SUPPORTED          = 0x06
+	ERR_CODE_ATT_INVALID_OFFSET             = 0x07
+	ERR_CODE_ATT_INSUFFICIENT_AUTHOR        = 0x08
+	ERR_CODE_ATT_PREPARE_QUEUE_FULL         = 0x09
+	ERR_CODE_ATT_ATTR_NOT_FOUND             = 0x0a
+	ERR_CODE_ATT_ATTR_NOT_LONG              = 0x0b
+	ERR_CODE_ATT_INSUFFICIENT_KEY_SZ        = 0x0c
+	ERR_CODE_ATT_INVALID_ATTR_VALUE_LEN     = 0x0d
+	ERR_CODE_ATT_UNLIKELY                   = 0x0e
+	ERR_CODE_ATT_INSUFFICIENT_ENC           = 0x0f
+	ERR_CODE_ATT_UNSUPPORTED_GROUP          = 0x10
+	ERR_CODE_ATT_INSUFFICIENT_RES           = 0x11
+)
+
+var AttErrCodeStringMap = map[int]string{
+	ERR_CODE_ATT_INVALID_HANDLE:         "invalid handle",
+	ERR_CODE_ATT_READ_NOT_PERMITTED:     "read not permitted",
+	ERR_CODE_ATT_WRITE_NOT_PERMITTED:    "write not permitted",
+	ERR_CODE_ATT_INVALID_PDU:            "invalid pdu",
+	ERR_CODE_ATT_INSUFFICIENT_AUTHEN:    "insufficient authentication",
+	ERR_CODE_ATT_REQ_NOT_SUPPORTED:      "request not supported",
+	ERR_CODE_ATT_INVALID_OFFSET:         "invalid offset",
+	ERR_CODE_ATT_INSUFFICIENT_AUTHOR:    "insufficient authorization",
+	ERR_CODE_ATT_PREPARE_QUEUE_FULL:     "prepare queue full",
+	ERR_CODE_ATT_ATTR_NOT_FOUND:         "attribute not found",
+	ERR_CODE_ATT_ATTR_NOT_LONG:          "attribute not long",
+	ERR_CODE_ATT_INSUFFICIENT_KEY_SZ:    "insufficient key size",
+	ERR_CODE_ATT_INVALID_ATTR_VALUE_LEN: "invalid attribute value length",
+	ERR_CODE_ATT_UNLIKELY:               "unlikely error",
+	ERR_CODE_ATT_INSUFFICIENT_ENC:       "insufficient encryption",
+	ERR_CODE_ATT_UNSUPPORTED_GROUP:      "unsupported group",
+	ERR_CODE_ATT_INSUFFICIENT_RES:       "insufficient resources",
+}
+
 // These values never get transmitted or received, so their precise values
 // don't matter.  We specify them explicitly here to match the blehostd source.
 const (
@@ -262,6 +302,10 @@ const (
 	MSG_TYPE_ADV_SET_DATA              = 23
 	MSG_TYPE_ADV_RSP_SET_DATA          = 24
 	MSG_TYPE_ADV_FIELDS                = 25
+	MSG_TYPE_CLEAR_SVCS                = 26
+	MSG_TYPE_ADD_SVCS                  = 27
+	MSG_TYPE_COMMIT_SVCS               = 28
+	MSG_TYPE_ACCESS_STATUS             = 29
 
 	MSG_TYPE_SYNC_EVT       = 2049
 	MSG_TYPE_CONNECT_EVT    = 2050
@@ -275,6 +319,7 @@ const (
 	MSG_TYPE_SCAN_TMO_EVT   = 2058
 	MSG_TYPE_ENC_CHANGE_EVT = 2059
 	MSG_TYPE_RESET_EVT      = 2060
+	MSG_TYPE_ACCESS_EVT     = 2061
 )
 
 var MsgOpStringMap = map[MsgOp]string{
@@ -307,6 +352,10 @@ var MsgTypeStringMap = map[MsgType]string{
 	MSG_TYPE_ADV_SET_DATA:      "adv_set_data",
 	MSG_TYPE_ADV_RSP_SET_DATA:  "adv_rsp_set_data",
 	MSG_TYPE_ADV_FIELDS:        "adv_fields",
+	MSG_TYPE_CLEAR_SVCS:        "clear_svcs",
+	MSG_TYPE_ADD_SVCS:          "add_svcs",
+	MSG_TYPE_COMMIT_SVCS:       "commit_svcs",
+	MSG_TYPE_ACCESS_STATUS:     "access_status",
 
 	MSG_TYPE_SYNC_EVT:       "sync_evt",
 	MSG_TYPE_CONNECT_EVT:    "connect_evt",
@@ -319,6 +368,7 @@ var MsgTypeStringMap = map[MsgType]string{
 	MSG_TYPE_SCAN_TMO_EVT:   "scan_tmo_evt",
 	MSG_TYPE_ENC_CHANGE_EVT: "enc_change_evt",
 	MSG_TYPE_RESET_EVT:      "reset_evt",
+	MSG_TYPE_ACCESS_EVT:     "access_evt",
 }
 
 type BleHdr struct {
@@ -329,13 +379,13 @@ type BleHdr struct {
 
 type Msg interface{}
 
-type BleSvc struct {
+type BleDiscSvc struct {
 	StartHandle int     `json:"start_handle"`
 	EndHandle   int     `json:"end_handle"`
 	Uuid        BleUuid `json:"uuid"`
 }
 
-type BleChr struct {
+type BleDiscChr struct {
 	DefHandle  int     `json:"def_handle"`
 	ValHandle  int     `json:"val_handle"`
 	Properties int     `json:"properties"`
@@ -469,8 +519,8 @@ type BleDiscSvcEvt struct {
 	Seq  BleSeq  `json:"seq"`
 
 	// Mandatory
-	Status int    `json:"status"`
-	Svc    BleSvc `json:"service"`
+	Status int        `json:"status"`
+	Svc    BleDiscSvc `json:"service"`
 }
 
 type BleDiscChrUuidReq struct {
@@ -546,8 +596,8 @@ type BleDiscChrEvt struct {
 	Seq  BleSeq  `json:"seq"`
 
 	// Mandatory
-	Status int    `json:"status"`
-	Chr    BleChr `json:"characteristic"`
+	Status int        `json:"status"`
+	Chr    BleDiscChr `json:"characteristic"`
 }
 
 type BleWriteCmdReq struct {
@@ -870,7 +920,7 @@ type BleAdvStartReq struct {
 	HighDutyCycle bool               `json:"high_duty_cycle"`
 
 	// Only required for direct advertisements
-	PeerAddr *BleAddr `json:"peer_addr"`
+	PeerAddr *BleAddr `json:"peer_addr,omitempty"`
 }
 
 type BleAdvStartRsp struct {
@@ -907,7 +957,7 @@ type BleAdvSetDataReq struct {
 	Seq  BleSeq  `json:"seq"`
 
 	// Mandatory
-	Data []byte `json:"data"`
+	Data BleBytes `json:"data"`
 }
 
 type BleAdvSetDataRsp struct {
@@ -927,7 +977,7 @@ type BleAdvRspSetDataReq struct {
 	Seq  BleSeq  `json:"seq"`
 
 	// Mandatory
-	Data []byte `json:"data"`
+	Data BleBytes `json:"data"`
 }
 
 type BleAdvRspSetDataRsp struct {
@@ -950,51 +1000,51 @@ type BleAdvFieldsReq struct {
 	Flags *uint8 `json:"flags,omitempty"`
 
 	/*** 0x02,0x03 - 16-bit service class UUIDs. */
-	Uuids16           []BleUuid16 `json:"uuids16"`
+	Uuids16           []BleUuid16 `json:"uuids16,omitempty"`
 	Uuids16IsComplete bool        `json:"uuids16_is_complete"`
 
 	/*** 0x04,0x05 - 32-bit service class UUIDs. */
-	Uuids32           []uint32 `json:"uuids32"`
+	Uuids32           []uint32 `json:"uuids32,omitempty"`
 	Uuids32IsComplete bool     `json:"uuids32_is_complete"`
 
 	/*** 0x06,0x07 - 128-bit service class UUIDs. */
-	Uuids128           []BleUuid128 `json:"uuids128"`
+	Uuids128           []BleUuid128 `json:"uuids128,omitempty"`
 	Uuids128IsComplete bool         `json:"uuids128_is_complete"`
 
 	/*** 0x08,0x09 - Local name. */
-	Name           *string `json:"name,omitempty"`
+	Name           *string `json:"name,omitempty,omitempty"`
 	NameIsComplete bool    `json:"name_is_complete"`
 
 	/*** 0x0a - Tx power level. */
-	TxPwrLvl *int8 `json:"tx_pwr_lvl"`
+	TxPwrLvl *int8 `json:"tx_pwr_lvl,omitempty"`
 
 	/*** 0x0d - Slave connection interval range. */
-	SlaveItvlMin *uint16 `json:"slave_itvl_min"`
-	SlaveItvlMax *uint16 `json:"slave_itvl_max"`
+	SlaveItvlMin *uint16 `json:"slave_itvl_min,omitempty"`
+	SlaveItvlMax *uint16 `json:"slave_itvl_max,omitempty"`
 
 	/*** 0x16 - Service data - 16-bit UUID. */
-	SvcDataUuid16 []byte `json:"svc_data_uuid16"`
+	SvcDataUuid16 BleBytes `json:"svc_data_uuid16,omitempty"`
 
 	/*** 0x17 - Public target address. */
-	PublicTgtAddrs []BleAddr `json:"public_tgt_addrs"`
+	PublicTgtAddrs []BleAddr `json:"public_tgt_addrs,omitempty"`
 
 	/*** 0x19 - Appearance. */
-	Appearance *uint16 `json:"appearance"`
+	Appearance *uint16 `json:"appearance,omitempty"`
 
 	/*** 0x1a - Advertising interval. */
-	AdvItvl *uint16 `json:"adv_itvl"`
+	AdvItvl *uint16 `json:"adv_itvl,omitempty"`
 
 	/*** 0x20 - Service data - 32-bit UUID. */
-	SvcDataUuid32 []byte `json:"svc_data_uuid32"`
+	SvcDataUuid32 BleBytes `json:"svc_data_uuid32,omitempty"`
 
 	/*** 0x21 - Service data - 128-bit UUID. */
-	SvcDataUuid128 []byte `json:"svc_data_uuid128"`
+	SvcDataUuid128 BleBytes `json:"svc_data_uuid128,omitempty"`
 
 	/*** 0x24 - URI. */
 	Uri *string `json:"uri,omitempty"`
 
 	/*** 0xff - Manufacturer specific data. */
-	MfgData []byte `json:"mfg_data"`
+	MfgData BleBytes `json:"mfg_data,omitempty"`
 }
 
 type BleAdvFieldsRsp struct {
@@ -1004,8 +1054,8 @@ type BleAdvFieldsRsp struct {
 	Seq  BleSeq  `json:"seq"`
 
 	// Mandatory
-	Status int    `json:"status"`
-	Data   []byte `json:"data"`
+	Status int      `json:"status"`
+	Data   BleBytes `json:"data"`
 }
 
 type BleResetEvt struct {
@@ -1018,6 +1068,132 @@ type BleResetEvt struct {
 	Reason int `json:"reason"`
 }
 
+type BleClearSvcsReq struct {
+	// Header
+	Op   MsgOp   `json:"op"`
+	Type MsgType `json:"type"`
+	Seq  BleSeq  `json:"seq"`
+}
+
+type BleClearSvcsRsp struct {
+	// Header
+	Op   MsgOp   `json:"op"`
+	Type MsgType `json:"type"`
+	Seq  BleSeq  `json:"seq"`
+
+	// Mandatory
+	Status int `json:"status"`
+}
+
+type BleAddDsc struct {
+	Uuid       BleUuid     `json:"uuid"`
+	AttFlags   BleAttFlags `json:"att_flags"`
+	MinKeySize int         `json:"min_key_size"`
+}
+
+type BleAddChr struct {
+	Uuid       BleUuid     `json:"uuid"`
+	Flags      BleChrFlags `json:"flags"`
+	MinKeySize int         `json:"min_key_size"`
+	Dscs       []BleAddDsc `json:"descriptors,omitempty"`
+}
+
+type BleAddSvc struct {
+	Uuid    BleUuid     `json:"uuid"`
+	SvcType BleSvcType  `json:"type"`
+	Chrs    []BleAddChr `json:"characteristics,omitempty"`
+}
+
+type BleAddSvcsReq struct {
+	// Header
+	Op   MsgOp   `json:"op"`
+	Type MsgType `json:"type"`
+	Seq  BleSeq  `json:"seq"`
+
+	Svcs []BleAddSvc `json:"services"`
+}
+
+type BleAddSvcsRsp struct {
+	// Header
+	Op   MsgOp   `json:"op"`
+	Type MsgType `json:"type"`
+	Seq  BleSeq  `json:"seq"`
+
+	// Mandatory
+	Status int `json:"status"`
+}
+
+type BleRegDsc struct {
+	Uuid   BleUuid `json:"uuid"`
+	Handle uint16  `json:"handle"`
+}
+
+type BleRegChr struct {
+	Uuid      BleUuid     `json:"uuid"`
+	DefHandle uint16      `json:"def_handle"`
+	ValHandle uint16      `json:"val_handle"`
+	Dscs      []BleRegDsc `json:"descriptors"`
+}
+
+type BleRegSvc struct {
+	Uuid   BleUuid     `json:"uuid"`
+	Handle uint16      `json:"handle"`
+	Chrs   []BleRegChr `json:"characteristics"`
+}
+
+type BleCommitSvcsReq struct {
+	// Header
+	Op   MsgOp   `json:"op"`
+	Type MsgType `json:"type"`
+	Seq  BleSeq  `json:"seq"`
+}
+
+type BleCommitSvcsRsp struct {
+	// Header
+	Op   MsgOp   `json:"op"`
+	Type MsgType `json:"type"`
+	Seq  BleSeq  `json:"seq"`
+
+	// Mandatory
+	Status int `json:"status"`
+
+	// Optional
+	Svcs []BleRegSvc `json:"services"`
+}
+
+type BleAccessEvt struct {
+	// Header
+	Op   MsgOp   `json:"op"`
+	Type MsgType `json:"type"`
+	Seq  BleSeq  `json:"seq"`
+
+	// Mandatory
+	GattOp     BleGattOp `json:"gatt_op"`
+	ConnHandle uint16    `json:"conn_handle"`
+	AttHandle  uint16    `json:"att_handle"`
+	Data       BleBytes  `json:"data"`
+}
+
+type BleAccessStatusReq struct {
+	// Header
+	Op   MsgOp   `json:"op"`
+	Type MsgType `json:"type"`
+	Seq  BleSeq  `json:"seq"`
+
+	// Mandatory
+	AttStatus uint8 `json:"att_status"`
+}
+
+type BleAccessStatusRsp struct {
+	// Header
+	Op   MsgOp   `json:"op"`
+	Type MsgType `json:"type"`
+	Seq  BleSeq  `json:"seq"`
+
+	// Mandatory
+	Status int `json:"status"`
+}
+
 func ErrCodeToString(e int) string {
 	var s string
 
diff --git a/nmxact/nmble/ble_util.go b/nmxact/nmble/ble_util.go
index a469a55..aecf4dd 100644
--- a/nmxact/nmble/ble_util.go
+++ b/nmxact/nmble/ble_util.go
@@ -312,6 +312,38 @@ func NewBleAdvStopReq() *BleAdvStopReq {
 	}
 }
 
+func NewBleClearSvcsReq() *BleClearSvcsReq {
+	return &BleClearSvcsReq{
+		Op:   MSG_OP_REQ,
+		Type: MSG_TYPE_CLEAR_SVCS,
+		Seq:  NextSeq(),
+	}
+}
+
+func NewBleAddSvcsReq() *BleAddSvcsReq {
+	return &BleAddSvcsReq{
+		Op:   MSG_OP_REQ,
+		Type: MSG_TYPE_ADD_SVCS,
+		Seq:  NextSeq(),
+	}
+}
+
+func NewBleCommitSvcsReq() *BleCommitSvcsReq {
+	return &BleCommitSvcsReq{
+		Op:   MSG_OP_REQ,
+		Type: MSG_TYPE_COMMIT_SVCS,
+		Seq:  NextSeq(),
+	}
+}
+
+func NewAccessStatusReq() *BleAccessStatusReq {
+	return &BleAccessStatusReq{
+		Op:   MSG_OP_REQ,
+		Type: MSG_TYPE_ACCESS_STATUS,
+		Seq:  NextSeq(),
+	}
+}
+
 func ConnFindXact(x *BleXport, connHandle uint16) (BleConnDesc, error) {
 	r := NewBleConnFindReq()
 	r.ConnHandle = connHandle
@@ -380,6 +412,56 @@ func ResetXact(x *BleXport) error {
 	return reset(x, bl, r)
 }
 
+func ClearSvcsXact(x *BleXport) error {
+	r := NewBleClearSvcsReq()
+
+	bl, err := x.AddListener(SeqKey(r.Seq))
+	if err != nil {
+		return err
+	}
+	defer x.RemoveListener(bl)
+
+	return clearSvcs(x, bl, r)
+}
+
+func AddSvcsXact(x *BleXport, svcs []BleAddSvc) error {
+	r := NewBleAddSvcsReq()
+	r.Svcs = svcs
+
+	bl, err := x.AddListener(SeqKey(r.Seq))
+	if err != nil {
+		return err
+	}
+	defer x.RemoveListener(bl)
+
+	return addSvcs(x, bl, r)
+}
+
+func CommitSvcsXact(x *BleXport) ([]BleRegSvc, error) {
+	r := NewBleCommitSvcsReq()
+
+	bl, err := x.AddListener(SeqKey(r.Seq))
+	if err != nil {
+		return nil, err
+	}
+	defer x.RemoveListener(bl)
+
+	return commitSvcs(x, bl, r)
+}
+
+func AccessStatusXact(x *BleXport, attStatus uint8) error {
+	r := NewAccessStatusReq()
+	r.AttStatus = attStatus
+
+	bl, err := x.AddListener(SeqKey(r.Seq))
+	if err != nil {
+		return err
+	}
+	defer x.RemoveListener(bl)
+
+	return accessStatus(x, bl, r)
+}
+
 func DiscoverDeviceWithName(
 	bx *BleXport,
 	ownAddrType BleAddrType,
@@ -392,3 +474,59 @@ func DiscoverDeviceWithName(
 
 	return DiscoverDevice(bx, ownAddrType, timeout, advPred)
 }
+
+func BleAdvFieldsToReq(f BleAdvFields) *BleAdvFieldsReq {
+	r := NewBleAdvFieldsReq()
+
+	r.Flags = f.Flags
+	r.Uuids16 = f.Uuids16
+	r.Uuids16IsComplete = f.Uuids16IsComplete
+	r.Uuids32 = f.Uuids32
+	r.Uuids32IsComplete = f.Uuids32IsComplete
+	r.Uuids128 = f.Uuids128
+	r.Uuids128IsComplete = f.Uuids128IsComplete
+	r.Name = f.Name
+	r.NameIsComplete = f.NameIsComplete
+	r.TxPwrLvl = f.TxPwrLvl
+	r.SlaveItvlMin = f.SlaveItvlMin
+	r.SlaveItvlMax = f.SlaveItvlMax
+	r.SvcDataUuid16 = BleBytes{f.SvcDataUuid16}
+	r.PublicTgtAddrs = f.PublicTgtAddrs
+	r.Appearance = f.Appearance
+	r.AdvItvl = f.AdvItvl
+	r.SvcDataUuid32 = BleBytes{f.SvcDataUuid32}
+	r.SvcDataUuid128 = BleBytes{f.SvcDataUuid128}
+	r.Uri = f.Uri
+	r.MfgData = BleBytes{f.MfgData}
+
+	return r
+}
+
+func BleSvcToAddSvc(svc BleSvc) BleAddSvc {
+	as := BleAddSvc{
+		Uuid:    svc.Uuid,
+		SvcType: svc.SvcType,
+	}
+
+	for _, chr := range svc.Chrs {
+		ac := BleAddChr{
+			Uuid:       chr.Uuid,
+			Flags:      chr.Flags,
+			MinKeySize: chr.MinKeySize,
+		}
+
+		for _, dsc := range chr.Dscs {
+			ad := BleAddDsc{
+				Uuid:       dsc.Uuid,
+				AttFlags:   dsc.AttFlags,
+				MinKeySize: dsc.MinKeySize,
+			}
+
+			ac.Dscs = append(ac.Dscs, ad)
+		}
+
+		as.Chrs = append(as.Chrs, ac)
+	}
+
+	return as
+}
diff --git a/nmxact/nmble/ble_xport.go b/nmxact/nmble/ble_xport.go
index 0e622da..071f3e0 100644
--- a/nmxact/nmble/ble_xport.go
+++ b/nmxact/nmble/ble_xport.go
@@ -29,6 +29,7 @@ import (
 	log "github.com/Sirupsen/logrus"
 
 	"mynewt.apache.org/newt/util/unixchild"
+	"mynewt.apache.org/newtmgr/nmxact/adv"
 	. "mynewt.apache.org/newtmgr/nmxact/bledefs"
 	"mynewt.apache.org/newtmgr/nmxact/nmxutil"
 	"mynewt.apache.org/newtmgr/nmxact/scan"
@@ -115,6 +116,8 @@ type BleXport struct {
 	randAddr     *BleAddr
 	stateMtx     sync.Mutex
 	scanner      *BleScanner
+	advertiser   *Advertiser
+	cm           ChrMgr
 }
 
 func NewBleXport(cfg XportCfg) (*BleXport, error) {
@@ -168,6 +171,16 @@ func (bx *BleXport) BuildScanner() (scan.Scanner, error) {
 	return bx.scanner, nil
 }
 
+func (bx *BleXport) BuildAdvertiser() (adv.Advertiser, error) {
+	// The transport only allows a single advertiser.  This is because the
+	// slave privileges need to managed among all the advertise operations.
+	if bx.advertiser == nil {
+		bx.advertiser = NewAdvertiser(bx)
+	}
+
+	return bx.advertiser, nil
+}
+
 func (bx *BleXport) BuildSesn(cfg sesn.SesnCfg) (sesn.Sesn, error) {
 	switch cfg.MgmtProto {
 	case sesn.MGMT_PROTO_NMP:
@@ -193,6 +206,12 @@ func (bx *BleXport) addResetListener() (*Listener, error) {
 	return bx.AddListener(key)
 }
 
+func (bx *BleXport) addAccessListener() (*Listener, error) {
+	key := TchKey(MSG_TYPE_ACCESS_EVT, -1)
+	nmxutil.LogAddListener(3, key, 0, "access")
+	return bx.AddListener(key)
+}
+
 func (bx *BleXport) querySyncStatus() (bool, error) {
 	req := &BleSyncReq{
 		Op:   MSG_OP_REQ,
@@ -432,12 +451,16 @@ func (bx *BleXport) startOnce() error {
 					"Timeout waiting for host <-> controller sync")
 				bx.shutdown(true, err)
 				return err
+			case <-bx.stopChan:
+				return nmxutil.NewXportError("Transport startup aborted")
 			}
 		}
 	}
 
-	// Host and controller are synced.  Listen for sync loss and stack reset in
-	// the background.
+	// Host and controller are synced.  Listen for events in the background:
+	//     * sync loss
+	//     * stack reset
+	//     * GATT access
 	go func() {
 		resetl, err := bx.addResetListener()
 		if err != nil {
@@ -446,6 +469,13 @@ func (bx *BleXport) startOnce() error {
 		}
 		defer bx.RemoveListener(resetl)
 
+		accessl, err := bx.addAccessListener()
+		if err != nil {
+			bx.shutdown(true, err)
+			return
+		}
+		defer bx.RemoveListener(accessl)
+
 		for {
 			select {
 			case err := <-syncl.ErrChan:
@@ -481,6 +511,18 @@ func (bx *BleXport) startOnce() error {
 					}
 				}
 
+			case err := <-accessl.ErrChan:
+				bx.shutdown(true, err)
+				return
+			case bm := <-accessl.MsgChan:
+				switch msg := bm.(type) {
+				case *BleAccessEvt:
+					if err := bx.cm.Access(bx, msg); err != nil {
+						log.Debugf("Error sending access status: %s",
+							err.Error())
+					}
+				}
+
 			case <-bx.stopChan:
 				return
 			}
@@ -583,6 +625,10 @@ func (bx *BleXport) Tx(data []byte) error {
 	return bx.txNoSync(data)
 }
 
+func (bx *BleXport) SetServices(svcs []BleSvc) error {
+	return bx.cm.SetServices(bx, svcs)
+}
+
 func (bx *BleXport) AddListener(key ListenerKey) (*Listener, error) {
 	listener := NewListener()
 	if err := bx.d.AddListener(key, listener); err != nil {
diff --git a/nmxact/nmble/chrmgr.go b/nmxact/nmble/chrmgr.go
new file mode 100644
index 0000000..24eacf1
--- /dev/null
+++ b/nmxact/nmble/chrmgr.go
@@ -0,0 +1,122 @@
+package nmble
+
+import (
+	"fmt"
+
+	. "mynewt.apache.org/newtmgr/nmxact/bledefs"
+)
+
+type chrMgrElem struct {
+	AttHandle uint16
+	SvcUuid   BleUuid
+	ChrUuid   BleUuid
+	Cb        BleGattAccessFn
+}
+
+type ChrMgr struct {
+	chrs map[uint16]chrMgrElem
+}
+
+func (cm *ChrMgr) Clear() {
+	cm.chrs = map[uint16]chrMgrElem{}
+}
+
+func (cm *ChrMgr) add(chr chrMgrElem) error {
+	if _, ok := cm.chrs[chr.AttHandle]; ok {
+		return fmt.Errorf("Characteristic with duplicate ATT handle: %d",
+			chr.AttHandle)
+	}
+
+	cm.chrs[chr.AttHandle] = chr
+	return nil
+}
+
+func (cm *ChrMgr) SetServices(x *BleXport, svcs []BleSvc) error {
+	if err := ClearSvcsXact(x); err != nil {
+		return err
+	}
+	cm.Clear()
+
+	addSvcs := make([]BleAddSvc, len(svcs))
+	for i, svc := range svcs {
+		addSvcs[i] = BleSvcToAddSvc(svc)
+	}
+
+	if err := AddSvcsXact(x, addSvcs); err != nil {
+		return err
+	}
+
+	regSvcs, err := CommitSvcsXact(x)
+	if err != nil {
+		return err
+	}
+
+	//               [uuid => svc]
+	//              /             \
+	// [uuid => chr]               [uuid => chr]
+	svcMap := map[BleUuid]map[BleUuid]BleChr{}
+
+	for _, svc := range svcs {
+		m := map[BleUuid]BleChr{}
+		svcMap[svc.Uuid] = m
+
+		for _, chr := range svc.Chrs {
+			m[chr.Uuid] = chr
+		}
+	}
+
+	for _, rs := range regSvcs {
+		srcSvc, ok := svcMap[rs.Uuid]
+		if !ok {
+			// XXX: Log
+			continue
+		}
+
+		for _, rc := range rs.Chrs {
+			srcChr, ok := srcSvc[rc.Uuid]
+			if !ok {
+				// XXX: Log
+				continue
+			}
+
+			cm.add(chrMgrElem{
+				AttHandle: rc.ValHandle,
+				SvcUuid:   rs.Uuid,
+				ChrUuid:   rc.Uuid,
+				Cb:        srcChr.AccessCb,
+			})
+		}
+	}
+
+	return nil
+}
+
+func (cm *ChrMgr) findByAttHandle(handle uint16) *chrMgrElem {
+	chr, ok := cm.chrs[handle]
+	if !ok {
+		return nil
+	} else {
+		return &chr
+	}
+}
+
+func (cm *ChrMgr) Access(x *BleXport, evt *BleAccessEvt) error {
+	chr := cm.findByAttHandle(evt.AttHandle)
+	if chr == nil {
+		return AccessStatusXact(x, uint8(ERR_CODE_ATT_INVALID_HANDLE))
+	}
+
+	if chr.Cb == nil {
+		return AccessStatusXact(x, 0)
+	}
+
+	access := BleGattAccess{
+		Op:      evt.GattOp,
+		SvcUuid: chr.SvcUuid,
+		ChrUuid: chr.ChrUuid,
+		Data:    evt.Data.Bytes,
+	}
+
+	status := chr.Cb(access)
+	return AccessStatusXact(x, status)
+}
diff --git a/nmxact/nmble/dispatch.go b/nmxact/nmble/dispatch.go
index 10e6fee..1e8f767 100644
--- a/nmxact/nmble/dispatch.go
+++ b/nmxact/nmble/dispatch.go
@@ -71,6 +71,15 @@ func setPreferredMtuRspCtor() Msg  { return &BleSetPreferredMtuRsp{} }
 func securityInitiateRspCtor() Msg { return &BleSecurityInitiateRsp{} }
 func connFindRspCtor() Msg         { return &BleConnFindRsp{} }
 func resetRspCtor() Msg            { return &BleResetRsp{} }
+func advStartRspCtor() Msg         { return &BleAdvStartRsp{} }
+func advStopRspCtor() Msg          { return &BleAdvStopRsp{} }
+func advSetDataRspCtor() Msg       { return &BleAdvSetDataRsp{} }
+func advRspSetDataRspCtor() Msg    { return &BleAdvRspSetDataRsp{} }
+func advFieldsRspCtor() Msg        { return &BleAdvFieldsRsp{} }
+func clearSvcsRspCtor() Msg        { return &BleClearSvcsRsp{} }
+func addSvcsRspCtor() Msg          { return &BleAddSvcsRsp{} }
+func commitSvcsRspCtor() Msg       { return &BleCommitSvcsRsp{} }
+func accessStatusRspCtor() Msg     { return &BleAccessStatusRsp{} }
 
 func syncEvtCtor() Msg       { return &BleSyncEvt{} }
 func connectEvtCtor() Msg    { return &BleConnectEvt{} }
@@ -83,6 +92,7 @@ func scanEvtCtor() Msg       { return &BleScanEvt{} }
 func scanTmoEvtCtor() Msg    { return &BleScanTmoEvt{} }
 func encChangeEvtCtor() Msg  { return &BleEncChangeEvt{} }
 func resetEvtCtor() Msg      { return &BleResetEvt{} }
+func accessEvtCtor() Msg     { return &BleAccessEvt{} }
 
 var msgCtorMap = map[OpTypePair]msgCtor{
 	{MSG_OP_RSP, MSG_TYPE_ERR}:               errRspCtor,
@@ -103,6 +113,15 @@ var msgCtorMap = map[OpTypePair]msgCtor{
 	{MSG_OP_RSP, MSG_TYPE_SECURITY_INITIATE}: securityInitiateRspCtor,
 	{MSG_OP_RSP, MSG_TYPE_CONN_FIND}:         connFindRspCtor,
 	{MSG_OP_RSP, MSG_TYPE_RESET}:             resetRspCtor,
+	{MSG_OP_RSP, MSG_TYPE_ADV_START}:         advStartRspCtor,
+	{MSG_OP_RSP, MSG_TYPE_ADV_STOP}:          advStopRspCtor,
+	{MSG_OP_RSP, MSG_TYPE_ADV_SET_DATA}:      advSetDataRspCtor,
+	{MSG_OP_RSP, MSG_TYPE_ADV_RSP_SET_DATA}:  advRspSetDataRspCtor,
+	{MSG_OP_RSP, MSG_TYPE_ADV_FIELDS}:        advFieldsRspCtor,
+	{MSG_OP_RSP, MSG_TYPE_CLEAR_SVCS}:        clearSvcsRspCtor,
+	{MSG_OP_RSP, MSG_TYPE_ADD_SVCS}:          addSvcsRspCtor,
+	{MSG_OP_RSP, MSG_TYPE_COMMIT_SVCS}:       commitSvcsRspCtor,
+	{MSG_OP_RSP, MSG_TYPE_ACCESS_STATUS}:     accessStatusRspCtor,
 
 	{MSG_OP_EVT, MSG_TYPE_SYNC_EVT}:       syncEvtCtor,
 	{MSG_OP_EVT, MSG_TYPE_CONNECT_EVT}:    connectEvtCtor,
@@ -115,6 +134,7 @@ var msgCtorMap = map[OpTypePair]msgCtor{
 	{MSG_OP_EVT, MSG_TYPE_SCAN_TMO_EVT}:   scanTmoEvtCtor,
 	{MSG_OP_EVT, MSG_TYPE_ENC_CHANGE_EVT}: encChangeEvtCtor,
 	{MSG_OP_EVT, MSG_TYPE_RESET_EVT}:      resetEvtCtor,
+	{MSG_OP_EVT, MSG_TYPE_ACCESS_EVT}:     accessEvtCtor,
 }
 
 func NewDispatcher() *Dispatcher {
diff --git a/nmxact/nmserial/serial_xport.go b/nmxact/nmserial/serial_xport.go
index 1c8cb98..9d6dca9 100644
--- a/nmxact/nmserial/serial_xport.go
+++ b/nmxact/nmserial/serial_xport.go
@@ -32,6 +32,7 @@ import (
 	"github.com/tarm/serial"
 
 	"mynewt.apache.org/newt/util"
+	"mynewt.apache.org/newtmgr/nmxact/adv"
 	"mynewt.apache.org/newtmgr/nmxact/nmxutil"
 	"mynewt.apache.org/newtmgr/nmxact/scan"
 	"mynewt.apache.org/newtmgr/nmxact/sesn"
@@ -239,3 +240,7 @@ func (sx *SerialXport) Rx() ([]byte, error) {
 	}
 	return nil, err
 }
+
+func (sx *SerialXport) BuildAdvertiser() (adv.Advertiser, error) {
+	return nil, fmt.Errorf("SerialXport#BuildAdvertiser unsupported")
+}
diff --git a/nmxact/nmxutil/nmxutil.go b/nmxact/nmxutil/nmxutil.go
index dc6b462..72bd00e 100644
--- a/nmxact/nmxutil/nmxutil.go
+++ b/nmxact/nmxutil/nmxutil.go
@@ -84,6 +84,10 @@ func NextNmpSeq() uint8 {
 	return val
 }
 
+func SeqToToken(seq uint8) []byte {
+	return []byte{seq}
+}
+
 func NextToken() []byte {
 	seqMutex.Lock()
 	defer seqMutex.Unlock()
@@ -93,7 +97,7 @@ func NextToken() []byte {
 		oicSeqBeenRead = true
 	}
 
-	token := []byte{nextOicSeq}
+	token := SeqToToken(nextOicSeq)
 	nextOicSeq++
 
 	return token
diff --git a/nmxact/scan/scan.go b/nmxact/scan/scan.go
index aa56930..db1ade2 100644
--- a/nmxact/scan/scan.go
+++ b/nmxact/scan/scan.go
@@ -61,7 +61,6 @@ type Scanner interface {
 func BleOmpScanCfg(ScanCb ScanFn) Cfg {
 	sc := sesn.NewSesnCfg()
 	sc.MgmtProto = sesn.MGMT_PROTO_OMP
-	sc.Ble.IsCentral = true
 	sc.Ble.EncryptWhen = bledefs.BLE_ENCRYPT_PRIV_ONLY
 	sc.Ble.OwnAddrType = bledefs.BLE_ADDR_TYPE_RANDOM
 
diff --git a/nmxact/sesn/sesn.go b/nmxact/sesn/sesn.go
index 4716413..b13fa66 100644
--- a/nmxact/sesn/sesn.go
+++ b/nmxact/sesn/sesn.go
@@ -87,6 +87,8 @@ type Sesn interface {
 	// separate thread, as sesn receive operations are blocking.
 	AbortRx(nmpSeq uint8) error
 
+	// XXX AbortResource(seq uint8) error
+
 	////// Internal to nmxact:
 
 	EncodeNmpMsg(msg *nmp.NmpMsg) ([]byte, error)
diff --git a/nmxact/sesn/sesn_cfg.go b/nmxact/sesn/sesn_cfg.go
index da84b06..7c11d33 100644
--- a/nmxact/sesn/sesn_cfg.go
+++ b/nmxact/sesn/sesn_cfg.go
@@ -45,31 +45,14 @@ type SesnCfgBleCentral struct {
 	// XXX: Missing fields.
 }
 
-type SesnCfgBlePeriph struct {
-	Duration      time.Duration
-	ConnMode      bledefs.BleAdvConnMode
-	DiscMode      bledefs.BleAdvDiscMode
-	ItvlMin       uint16
-	ItvlMax       uint16
-	ChannelMap    uint8
-	FilterPolicy  bledefs.BleAdvFilterPolicy
-	HighDutyCycle bool
-	AdvFields     bledefs.BleAdvFields
-	RspFields     bledefs.BleAdvFields
-}
-
 type SesnCfgBle struct {
 	// General configuration.
-	IsCentral    bool
 	OwnAddrType  bledefs.BleAddrType
 	EncryptWhen  bledefs.BleEncryptWhen
 	CloseTimeout time.Duration
 
 	// Central configuration.
 	Central SesnCfgBleCentral
-
-	// Peripheral configuration.
-	Periph SesnCfgBlePeriph
 }
 
 type SesnCfg struct {
@@ -88,7 +71,6 @@ func NewSesnCfg() SesnCfg {
 		// future, there will need to be some global default, or something that
 		// gets read from blehostd.
 		Ble: SesnCfgBle{
-			IsCentral:    true,
 			OwnAddrType:  bledefs.BLE_ADDR_TYPE_RANDOM,
 			CloseTimeout: 30 * time.Second,
 
@@ -96,7 +78,6 @@ func NewSesnCfg() SesnCfg {
 				ConnTries:   3,
 				ConnTimeout: 10 * time.Second,
 			},
-			Periph: SesnCfgBlePeriph{},
 		},
 	}
 }
diff --git a/nmxact/udp/udp_xport.go b/nmxact/udp/udp_xport.go
index 2147dcc..954ab9f 100644
--- a/nmxact/udp/udp_xport.go
+++ b/nmxact/udp/udp_xport.go
@@ -22,6 +22,7 @@ package udp
 import (
 	"fmt"
 
+	"mynewt.apache.org/newtmgr/nmxact/adv"
 	"mynewt.apache.org/newtmgr/nmxact/nmxutil"
 	"mynewt.apache.org/newtmgr/nmxact/scan"
 	"mynewt.apache.org/newtmgr/nmxact/sesn"
@@ -71,3 +72,7 @@ func (ux *UdpXport) Stop() error {
 func (ux *UdpXport) Tx(bytes []byte) error {
 	return fmt.Errorf("unsupported")
 }
+
+func (ux *UdpXport) BuildAdvertiser() (adv.Advertiser, error) {
+	return nil, fmt.Errorf("UdpXport#BuildAdvertiser unsupported")
+}
diff --git a/nmxact/xport/xport.go b/nmxact/xport/xport.go
index 865bfb2..0cca2a4 100644
--- a/nmxact/xport/xport.go
+++ b/nmxact/xport/xport.go
@@ -20,6 +20,7 @@
 package xport
 
 import (
+	"mynewt.apache.org/newtmgr/nmxact/adv"
 	"mynewt.apache.org/newtmgr/nmxact/scan"
 	"mynewt.apache.org/newtmgr/nmxact/sesn"
 )
@@ -32,6 +33,7 @@ type Xport interface {
 
 	BuildSesn(cfg sesn.SesnCfg) (sesn.Sesn, error)
 	BuildScanner() (scan.Scanner, error)
+	BuildAdvertiser() (adv.Advertiser, error)
 
 	Tx(data []byte) error
 }

-- 
To stop receiving notification emails like this one, please contact
"commits@mynewt.apache.org" <co...@mynewt.apache.org>.