diff --git a/include/nuttx/wireless/bt_ioctl.h b/include/nuttx/wireless/bt_ioctl.h
index 02f2b92375..7e4ac97a14 100644
--- a/include/nuttx/wireless/bt_ioctl.h
+++ b/include/nuttx/wireless/bt_ioctl.h
@@ -49,6 +49,7 @@
 #include <nuttx/wireless/wireless.h>
 #include <nuttx/wireless/bt_core.h>
 #include <nuttx/wireless/bt_hci.h>
+#include <nuttx/wireless/bt_gatt.h>
 
 /****************************************************************************
  * Pre-processor Definitions
@@ -59,7 +60,7 @@
 
 /* Bluetooth network device IOCTL commands. */
 
-#if !defined(WL_BLUETOOTHCMDS) || WL_BLUETOOTHCMDS != 16
+#if !defined(WL_BLUETOOTHCMDS) || WL_BLUETOOTHCMDS != 18
 #  error Incorrect setting for number of Bluetooth IOCTL commands
 #endif
 
@@ -162,6 +163,18 @@
 
 #define SIOCBTSECURITY         _WLIOC(WL_BLUETOOTHFIRST + 15)
 
+/* GATT
+ *
+ * SIOCBTDISCOVER
+ *   Starts GATT discovery
+ * SIOCBTDISCGET
+ *   Return discovery results buffered since the call time that the
+ *   SIOCBTDISCGET command was invoked.
+ */
+
+#define SIOCBTDISCOVER         _WLIOC(WL_BLUETOOTHFIRST + 16)
+#define SIOCBTDISCGET          _WLIOC(WL_BLUETOOTHFIRST + 17)
+
 /* Definitions associated with struct btreg_s *******************************/
 /* struct btreq_s union field accessors */
 
@@ -193,6 +206,15 @@
 #define btr_secaddr            btru.btrse.btrse_secaddr
 #define btr_seclevel           btru.btrse.btrse_seclevel
 
+#define btr_dtype              btru.btrds.btrds_dtype
+#define btr_dpeer              btru.btrds.btrds_dpeer
+#define btr_duuid16            btru.btrds.btrds_duuid16
+#define btr_dstart             btru.btrds.btrds_dstart
+#define btr_dend               btru.btrds.btrds_dend
+
+#define btr_gnrsp              btru.btrdg.btrdg_gnrsp
+#define btr_grsp               btru.btrdg.btrdg_grsp
+
 #define btr_stats              btru.btrs
 
 /* btr_flags */
@@ -213,6 +235,15 @@
  * Public Types
  ****************************************************************************/
 
+/* Type of GATT discovery command */
+
+enum bt_gatt_discover_e
+{
+  GATT_DISCOVER = 0,                /* Discover */
+  GATT_DISCOVER_DESC,               /* Discover descriptor */
+  GATT_DISCOVER_CHAR,               /* Discover characteristic */
+};
+
 /* Write-able data that accompanies the SIOCBTSCANGET IOCTL command */
 
 struct bt_scanresponse_s
@@ -224,6 +255,14 @@ struct bt_scanresponse_s
   uint8_t sr_data[CONFIG_BLUETOOTH_MAXSCANDATA];
 };
 
+/* Write-able data that accompanies the SIOCBTDISCGET IOCTL command */
+
+struct bt_discresonse_s
+{
+  uint16_t dr_handle;               /* Discovered handled */
+  uint8_t dr_perm;                  /* Permissions */
+};
+
 /* Bluetooth statistics */
 
 struct bt_stats_s
@@ -323,6 +362,32 @@ struct btreq_s
         enum bt_security_e btrse_seclevel; /* Security level */
       } btrse;
 
+      /* Read-only data that accompanies SIOCBTDISCOVER command */
+
+      struct
+      {
+        uint8_t btrds_dtype;            /* Discovery type (see enum
+                                         * bt_gatt_discover_e) */
+        bt_addr_le_t btrds_dpeer;       /* Peer address */
+        uint16_t btrds_duuid16;         /* Discover UUID type */
+        uint16_t btrds_dstart;          /* Discover start handle */
+        uint16_t btrds_dend;            /* Discover end handle */
+      } btrds;
+
+      /* Write-able structure that accompanies SIOCBTDISCGET command. */
+
+      struct
+      {
+        uint8_t btrdg_gnrsp;            /* Input:  Max number of responses
+                                         * Return: Actual number of responses */
+
+        /* Reference to a beginning of an array in user memory in which to
+         * return the discovered data.  The size of the array is btrdg_gnrsp.
+         */
+
+        FAR struct bt_discresonse_s *btrdg_grsp;
+      } btrdg;
+
       struct bt_stats_s btrs;           /* Unit statistics */
    } btru;
 };
diff --git a/include/nuttx/wireless/wireless.h b/include/nuttx/wireless/wireless.h
index f4a0454a3e..922d255e3c 100644
--- a/include/nuttx/wireless/wireless.h
+++ b/include/nuttx/wireless/wireless.h
@@ -160,7 +160,7 @@
 /* Reserved for Bluetooth network devices (see bt_ioctls.h) */
 
 #define WL_BLUETOOTHFIRST     (WL_NETFIRST + WL_NNETCMDS)
-#define WL_BLUETOOTHCMDS      (16)
+#define WL_BLUETOOTHCMDS      (18)
 #define WL_IBLUETOOTHCMD(cmd) (_WLIOCVALID(cmd) && \
                               _IOC_NR(cmd) >= WL_BLUETOOTHFIRST && \
                               _IOC_NR(cmd) < (WL_BLUETOOTHFIRST + WL_BLUETOOTHCMDS))
diff --git a/wireless/bluetooth/Kconfig b/wireless/bluetooth/Kconfig
index 83527b6fa2..7b0b0ba136 100644
--- a/wireless/bluetooth/Kconfig
+++ b/wireless/bluetooth/Kconfig
@@ -98,6 +98,15 @@ config BLUETOOTH_MAXSCANRESULT
 		This contributes to a static memory allocation that will be greater
 		than CONFIG_BLUETOOTH_MAXSCANDATA * CONFIG_BLUETOOTH_MAXSCANRESULT
 
+config BLUETOOTH_MAXDISCOVER
+	int "Max GATT discovery results"
+	default 8
+	range 1 255
+	---help---
+		GATT discovery results will be buffered in memory until the user
+		requests the results.  This parameter specifies the maximum results
+		that can be buffered before discovery results are lost.
+
 config BLUETOOTH_BUFFER_PREALLOC
 	int "Number of pre-allocated buffer structures"
 	default 20
diff --git a/wireless/bluetooth/bt_gatt.c b/wireless/bluetooth/bt_gatt.c
index 6f466bf7a4..3437b89869 100644
--- a/wireless/bluetooth/bt_gatt.c
+++ b/wireless/bluetooth/bt_gatt.c
@@ -702,6 +702,15 @@ done:
     }
 }
 
+/****************************************************************************
+ * Name: bt_gatt_discover
+ *
+ * Description:
+ *   This function implements the SIOCBTDISCOVER ioctl command for the
+ *   GATT discovery.
+ *
+ ****************************************************************************/
+
 int bt_gatt_discover(FAR struct bt_conn_s *conn,
                      FAR struct bt_gatt_discover_params_s *params)
 {
@@ -867,6 +876,15 @@ done:
     }
 }
 
+/****************************************************************************
+ * Name: bt_gatt_discover_characteristic
+ *
+ * Description:
+ *   This function implements the SIOCBTDISCOVER ioctl command for the
+ *   GATT discover characteristics type.
+ *
+ ****************************************************************************/
+
 int bt_gatt_discover_characteristic(FAR struct bt_conn_s *conn,
                                     FAR struct bt_gatt_discover_params_s *params)
 {
@@ -1014,6 +1032,15 @@ done:
     }
 }
 
+/****************************************************************************
+ * Name: bt_gatt_discover_descriptor
+ *
+ * Description:
+ *   This function implements the SIOCBTDISCOVER ioctl command for the
+ *   GATT discover descriptor type.
+ *
+ ****************************************************************************/
+
 int bt_gatt_discover_descriptor(FAR struct bt_conn_s *conn,
                                 FAR struct bt_gatt_discover_params_s *params)
 {
diff --git a/wireless/bluetooth/bt_ioctl.c b/wireless/bluetooth/bt_ioctl.c
index f3011f377d..2f4fb0c6af 100644
--- a/wireless/bluetooth/bt_ioctl.c
+++ b/wireless/bluetooth/bt_ioctl.c
@@ -62,7 +62,7 @@
  * Public Types
  ****************************************************************************/
 
-/* This structure encapsulates all globals used by the IOCTL logic */
+/* These structures encapsulate all globals used by the IOCTL logic. */
 
 struct btnet_scanstate_s
 {
@@ -74,15 +74,28 @@ struct btnet_scanstate_s
   struct bt_scanresponse_s bs_rsp[CONFIG_BLUETOOTH_MAXSCANRESULT];
 };
 
+struct btnet_discoverstate_s
+{
+  sem_t bd_exclsem;                 /* Manages exclusive access */
+  bool bd_discovering;              /* True:  Discovery in progress */
+  uint8_t bd_head;                  /* Head of circular list (for removal) */
+  uint8_t bd_tail;                  /* Tail of circular list (for addition) */
+
+  struct bt_discresonse_s bd_rsp[CONFIG_BLUETOOTH_MAXDISCOVER];
+};
+
 /****************************************************************************
  * Private Data
  ****************************************************************************/
 
 /* At present only a single Bluetooth device is supported.  So we can simply
- * maintain the scan state as a global.
+ * maintain the scan and the discovery state as globals.
+ * NOTE: This limits to one concurrent scan action and one concurrent
+ * discovery action.
  */
 
-static struct btnet_scanstate_s g_scanstate;
+static struct btnet_scanstate_s     g_scanstate;
+static struct btnet_discoverstate_s g_discoverstate;
 
 /****************************************************************************
  * Private Functions
@@ -179,9 +192,8 @@ static void btnet_scan_callback(FAR const bt_addr_le_t *addr, int8_t rssi,
  * Name: btnet_scan_result
  *
  * Description:
- *   This is an HCI callback function.  HCI provides scan result data via
- *   this callback function.  The scan result data will be added to the
- *   cached scan results.
+ *   This function implements the SIOCBTSCANGET IOCTL command.  It returns
+ *   the current, buffered discovered handles.
  *
  * Input Parameters:
  *   result - Location to return the scan result data
@@ -214,8 +226,8 @@ static int btnet_scan_result(FAR struct bt_scanresponse_s *result,
 
   /* Copy all available results */
 
-  head   = g_scanstate.bs_head;
-  tail   = g_scanstate.bs_tail;
+  head = g_scanstate.bs_head;
+  tail = g_scanstate.bs_tail;
 
   for (nrsp = 0; nrsp < maxrsp && head != tail; nrsp++)
     {
@@ -241,6 +253,177 @@ static int btnet_scan_result(FAR struct bt_scanresponse_s *result,
   return nrsp;
 }
 
+/****************************************************************************
+ * Name: bt_discover_func
+ *
+ * Description:
+ *   GATT discovery callback.  This function is called when a new handle is
+ *   discovered
+ *
+ * Input Parameters:
+ *   attr - The discovered attributes
+ *   arg  - The original discovery parameters
+ *
+ * Returned Value:
+ *   BT_GATT_ITER_CONTINUE meaning to continue the iteration.
+ *
+ ****************************************************************************/
+
+static uint8_t bt_discover_func(FAR const struct bt_gatt_attr_s *attr,
+                                FAR void *arg)
+{
+  uint8_t nexttail;
+  uint8_t head;
+  uint8_t tail;
+  int ret;
+
+  wlinfo("Discovered handle %u\n", attr->handle);
+
+  if (!g_discoverstate.bd_discovering)
+    {
+      wlerr("ERROR:  Results received while not discovering\n");
+      return BT_GATT_ITER_STOP;
+    }
+
+  /* Get exclusive access to the discovered data */
+
+  while ((ret = nxsem_wait(&g_discoverstate.bd_exclsem)) < 0)
+    {
+      DEBUGASSERT(ret == -EINTR || ret == -ECANCELED);
+      if (ret != -EINTR)
+        {
+          return BT_GATT_ITER_STOP;
+        }
+    }
+
+  /* Add the discovered data to the cache */
+
+  tail     = g_discoverstate.bd_tail;
+  nexttail = tail + 1;
+
+  if (nexttail >= CONFIG_BLUETOOTH_MAXSCANRESULT)
+    {
+      nexttail = 0;
+    }
+
+  /* Is the circular buffer full? */
+
+  head = g_discoverstate.bd_head;
+  if (nexttail == head)
+    {
+      wlerr("ERROR: Too many handles discovered.  Data lost.\n");
+
+      if (++head >= CONFIG_BLUETOOTH_MAXSCANRESULT)
+        {
+          head = 0;
+        }
+
+      g_discoverstate.bd_head = head;
+    }
+
+  /* Save the newly discovered handle */
+
+  g_discoverstate.bd_rsp[tail].dr_handle = attr->handle;
+  g_discoverstate.bd_rsp[tail].dr_perm   = attr->perm;
+  g_discoverstate.bd_tail                = nexttail;
+
+  nxsem_post(&g_discoverstate.bd_exclsem);
+  return BT_GATT_ITER_CONTINUE;
+}
+
+/****************************************************************************
+ * Name: bt_discover_destroy
+ *
+ * Description:
+ *   GATT destroy callback.  This function is called when a discovery
+ *   completes.
+ *
+ * Input Parameters:
+ *   arg  - The original discovery parameters
+ *
+ * Returned Value:
+ *   None
+ *
+ ****************************************************************************/
+
+static void bt_discover_destroy(FAR void *arg)
+{
+  FAR struct bt_gatt_discover_params *params = arg;
+
+  /* There is nothing that needs to be down here.  The parameters were
+   * allocated on the stack and are long gone.
+   */
+
+  wlinfo("Discover destroy.  params %p\n", params);
+  DEBUGASSERT(params != NULL);
+  UNUSED(params);
+
+  DEBUGASSERT(g_discoverstate.bd_discovering);
+  nxsem_destroy(&g_discoverstate.bd_exclsem);
+  g_discoverstate.bd_discovering = false;
+}
+
+/****************************************************************************
+ * Name: btnet_discover_result
+ *
+ * Description:
+ *   This function implements the SIOCBTDISCGET IOCTL command.  It returns
+ *   the current, buffered discovered handles.
+ *
+ * Input Parameters:
+ *   result - Location to return the discovery result data
+ *   maxrsp - The maximum number of responses that can be returned.
+ *
+ * Returned Value:
+ *   On success, the actual number of discovery results obtain is returned.  A
+ *   negated errno value is returned on any failure.
+ *
+ ****************************************************************************/
+
+static int btnet_discover_result(FAR struct bt_discresonse_s *result,
+                                 uint8_t maxrsp)
+{
+  uint8_t head;
+  uint8_t tail;
+  uint8_t nrsp;
+  int ret;
+
+  wlinfo("Discovering? %s\n", g_discoverstate.bd_discovering ? "YES" : "NO");
+
+  /* Get exclusive access to the discovery data */
+
+  ret = nxsem_wait(&g_discoverstate.bd_exclsem);
+  if (ret < 0)
+    {
+      DEBUGASSERT(ret == -EINTR || ret == -ECANCELED);
+      return ret;
+    }
+
+  /* Copy all available results */
+
+  head = g_discoverstate.bd_head;
+  tail = g_discoverstate.bd_tail;
+
+  for (nrsp = 0; nrsp < maxrsp && head != tail; nrsp++)
+    {
+      /* Copy data from the head index into the user buffer */
+
+      result[nrsp].dr_handle = g_discoverstate.bd_rsp[head].dr_handle;
+      result[nrsp].dr_perm   = g_discoverstate.bd_rsp[head].dr_perm;
+
+      /* Increment the head index */
+
+      if (++head >= CONFIG_BLUETOOTH_MAXDISCOVER)
+        {
+          head = 0;
+        }
+    }
+
+  g_discoverstate.bd_head = head;
+  nxsem_post(&g_discoverstate.bd_exclsem);
+  return nrsp;
+}
+
 /****************************************************************************
  * Public Functions
  ****************************************************************************/
@@ -363,6 +546,7 @@ int btnet_ioctl(FAR struct net_driver_s *netdev, int cmd, unsigned long arg)
 
           if (g_scanstate.bs_scanning)
             {
+              wlwarn("WARNING: Already scanning\n");
               ret = -EBUSY;
             }
           else
@@ -445,6 +629,103 @@ int btnet_ioctl(FAR struct net_driver_s *netdev, int cmd, unsigned long arg)
         }
         break;
 
+      /* SIOCBTDISCOVER:  Starts GATT discovery */
+
+      case SIOCBTDISCOVER:
+        {
+          FAR struct bt_conn_s *conn;
+
+          /* Check if discovery is already in progress */
+
+          if (g_discoverstate.bd_discovering)
+            {
+              wlwarn("WARNING:  Discovery is already in progress\n");
+              ret = -EBUSY;
+            }
+          else
+            {
+              /* Get the connection associated with the provided LE address */
+
+              conn = bt_conn_lookup_addr_le(&btreq->btr_dpeer);
+              if (conn == NULL)
+                {
+                  wlwarn("WARNING:  Peer not connected\n");
+                  ret = -ENOTCONN;
+                }
+              else
+                {
+                  struct bt_gatt_discover_params_s params;
+                  struct bt_uuid_s uuid;
+
+
+                  /* Set up the query */
+
+                  nxsem_init(&g_discoverstate.bd_exclsem, 0, 1);
+                  g_discoverstate.bd_discovering = true;
+                  g_discoverstate.bd_head        = 0;
+                  g_discoverstate.bd_tail        = 0;
+
+                  /* Start the query */
+
+                  uuid.type           = BT_UUID_16;
+                  uuid.u.u16          = btreq->btr_duuid16;
+
+                  params.uuid         = &uuid;
+                  params.func         = bt_discover_func;
+                  params.destroy      = bt_discover_destroy;
+                  params.start_handle = btreq->btr_dstart;
+                  params.end_handle   = btreq->btr_dend;
+
+                  switch (btreq->btr_dtype)
+                    {
+                      case GATT_DISCOVER:
+                        ret = bt_gatt_discover(conn, &params);
+                        break;
+
+                      case GATT_DISCOVER_DESC:
+                        ret = bt_gatt_discover_descriptor(conn, &params);
+                        break;
+
+                      case GATT_DISCOVER_CHAR:
+                        ret = bt_gatt_discover_characteristic(conn, &params);
+                        break;
+
+                      default:
+                        wlerr("ERROR: Unrecognized GATT discover type: %u\n",
+                              btreq->btr_dtype);
+                        ret = -EINVAL;
+                    }
+
+                  if (ret < 0)
+                    {
+                      wlerr("ERROR: Failed to start discovery: %d\n", ret);
+                      nxsem_destroy(&g_discoverstate.bd_exclsem);
+                      g_discoverstate.bd_discovering = false;
+                    }
+
+                  bt_conn_release(conn);
+                }
+            }
+        }
+        break;
+
+      /* SIOCBTDISCGET:  Return discovered results buffered since the call time
+       * that the SIOCBTDISCGET command was invoked.
+       */
+
+      case SIOCBTDISCGET:
+        {
+          ret = btnet_discover_result(btreq->btr_grsp, btreq->btr_gnrsp);
+          wlinfo("Get discovery results: %d\n", ret);
+
+          if (ret >= 0)
+            {
+              btreq->btr_nrsp = ret;
+              ret = OK;
+            }
+        }
+        break;
+
       default:
         wlwarn("WARNING: Unrecognized IOCTL command: %02x\n", cmd);
         ret = -ENOTTY;