Re: [BlueZ PATCH 3/3] android: Enable multiadvertising

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



Hi Martin,

On Tue, Oct 10, 2017 at 12:27 PM, Martin Fuzzey <mfuzzey@xxxxxxxxxxx> wrote:
> This is required for custom advertising data.
>
> The HAL entry points related to multiadvertising are now implemented and
> map to the mgmnt "add advertising" operation.
>
> Signed-off-by: Martin Fuzzey <mfuzzey@xxxxxxxxxxx>
> ---
>  android/bluetooth.c |  124 +++++++++++++++++
>  android/bluetooth.h |   32 ++++
>  android/gatt.c      |  370 ++++++++++++++++++++++++++++++++++++++++++++++++++-
>  3 files changed, 516 insertions(+), 10 deletions(-)
>
> diff --git a/android/bluetooth.c b/android/bluetooth.c
> index ba8f405..b610b6c 100644
> --- a/android/bluetooth.c
> +++ b/android/bluetooth.c
> @@ -4027,6 +4027,130 @@ bool bt_le_set_advertising(bool advertising, bt_le_set_advertising_done cb,
>         return false;
>  }
>
> +struct addrm_adv_user_data {
> +       bt_le_addrm_advertising_done cb;
> +       void *user_data;
> +};
> +
> +static void add_advertising_cb(uint8_t status, uint16_t length,
> +                       const void *param, void *user_data)
> +{
> +       struct addrm_adv_user_data *data = user_data;
> +
> +       DBG("");
> +
> +       if (status)
> +               error("Failed to add advertising %s (0x%02x))",
> +                                               mgmt_errstr(status), status);
> +
> +       data->cb(status, data->user_data);
> +}
> +
> +bool bt_le_add_advertising(struct adv_instance *adv,
> +               bt_le_addrm_advertising_done cb, void *user_data)
> +{
> +       struct mgmt_cp_add_advertising *cp;
> +       struct addrm_adv_user_data *cb_data;
> +       size_t len = sizeof(*cp);
> +       uint8_t *dst;
> +       bool ok;
> +
> +       if (adv->adv_data)
> +               len += adv->adv_data->len;
> +       if (adv->sr_data)
> +               len += adv->sr_data->len;
> +
> +       cp = malloc0(len);
> +       if (!cp)
> +               return false;
> +
> +       cp->instance = adv->instance;
> +       cp->timeout = adv->timeout;
> +       /* XXX: how should we set duration? (kernel will default to 2s as not set) */
> +
> +       switch(adv->type) {
> +       case ANDROID_ADVERTISING_EVENT_TYPE_CONNECTABLE:
> +               cp->flags |= MGMT_ADV_FLAG_CONNECTABLE;
> +               break;
> +
> +       defualt:
> +               break;
> +       }
> +
> +       if (adv->include_tx_power)
> +               cp->flags |= MGMT_ADV_FLAG_TX_POWER;
> +
> +       dst = cp->data;
> +       if (adv->adv_data) {
> +               cp->adv_data_len = adv->adv_data->len;
> +               if (cp->adv_data_len) {
> +                       memcpy(dst, adv->adv_data->data, cp->adv_data_len);
> +                       dst += cp->adv_data_len;
> +               }
> +       }
> +
> +       if (adv->sr_data) {
> +               cp->scan_rsp_len = adv->sr_data->len;
> +               if (cp->scan_rsp_len) {
> +                       memcpy(dst, adv->sr_data->data, cp->scan_rsp_len);
> +                       dst += cp->scan_rsp_len;
> +               }
> +       }
> +
> +       DBG("lens: adv=%d sr=%d total=%d",
> +               cp->adv_data_len, cp->scan_rsp_len, len);
> +
> +       cb_data = new0(typeof(*cb_data), 1);
> +       cb_data->cb = cb;
> +       cb_data->user_data = user_data;
> +
> +       ok = (mgmt_send(mgmt_if, MGMT_OP_ADD_ADVERTISING, adapter.index,
> +                       len, cp, add_advertising_cb, cb_data, free) > 0);
> +
> +       if (!ok)
> +               free(cb_data);
> +
> +       free(cp);
> +
> +       return ok;
> +}
> +
> +static void remove_advertising_cb(uint8_t status, uint16_t length,
> +                       const void *param, void *user_data)
> +{
> +       struct addrm_adv_user_data *data = user_data;
> +
> +       DBG("");
> +
> +       if (status)
> +               error("Failed to remove advertising %s (0x%02x))",
> +                                               mgmt_errstr(status), status);
> +
> +       data->cb(status, data->user_data);
> +}
> +
> +bool bt_le_remove_advertising(struct adv_instance *adv,
> +                               bt_le_addrm_advertising_done cb, void *user_data)
> +{
> +       struct mgmt_cp_remove_advertising cp = {
> +               .instance = adv->instance,
> +       };
> +       struct addrm_adv_user_data *cb_data;
> +       bool ok;
> +
> +       cb_data = new0(typeof(*cb_data), 1);
> +       cb_data->cb = cb;
> +       cb_data->user_data = user_data;
> +
> +       ok = (mgmt_send(mgmt_if, MGMT_OP_REMOVE_ADVERTISING, adapter.index,
> +                       sizeof(cp), &cp, remove_advertising_cb, cb_data, free) > 0);
> +
> +       if (!ok)
> +               free(cb_data);
> +
> +       return ok;
> +}
> +
>  bool bt_le_register(bt_le_device_found cb)
>  {
>         if (gatt_device_found_cb)
> diff --git a/android/bluetooth.h b/android/bluetooth.h
> index 4b17209..d3e9214 100644
> --- a/android/bluetooth.h
> +++ b/android/bluetooth.h
> @@ -88,3 +88,35 @@ typedef void (*bt_paired_device_cb)(const bdaddr_t *addr);
>  bool bt_paired_register(bt_paired_device_cb cb);
>  void bt_paired_unregister(bt_paired_device_cb cb);
>  bool bt_is_pairing(const bdaddr_t *addr);
> +
> +/* Advertising data (for AD and SRD packets)
> + * In binary format including GAP headers (length, type)
> + */
> +struct adv_data {
> +       uint8_t len;
> +       uint8_t data[0];        /* 0-N GAP records */
> +};
> +
> +struct adv_instance {
> +       uint8_t instance;
> +       int32_t timeout;
> +       int32_t type;
> +       struct adv_data *adv_data;
> +       struct adv_data *sr_data;
> +       unsigned include_tx_power:1;
> +};
> +
> +/* Values below have no C API definition - only in Java (AdvertiseManager.java) and bluedroid */
> +enum android_adv_type {
> +       ANDROID_ADVERTISING_EVENT_TYPE_CONNECTABLE = 0,
> +       ANDROID_ADVERTISING_EVENT_TYPE_SCANNABLE = 2,
> +       ANDROID_ADVERTISING_EVENT_TYPE_NON_CONNECTABLE = 3,
> +};
> +
> +typedef void (*bt_le_addrm_advertising_done)(uint8_t status, void *user_data);
> +bool bt_le_add_advertising(struct adv_instance *adv,
> +               bt_le_addrm_advertising_done cb, void *user_data);
> +bool bt_le_remove_advertising(struct adv_instance *adv,
> +               bt_le_addrm_advertising_done cb, void *user_data);
> +
> +
> diff --git a/android/gatt.c b/android/gatt.c
> index 28635ed..46dfc82 100644
> --- a/android/gatt.c
> +++ b/android/gatt.c
> @@ -110,6 +110,8 @@ struct gatt_app {
>         struct queue *notifications;
>
>         gatt_conn_cb_t func;
> +
> +       struct adv_instance *adv;
>  };
>
>  struct element_id {
> @@ -192,6 +194,7 @@ static struct ipc *hal_ipc = NULL;
>  static bdaddr_t adapter_addr;
>  static bool scanning = false;
>  static unsigned int advertising_cnt = 0;
> +static uint32_t adv_inst_bits = 0;
>
>  static struct queue *gatt_apps = NULL;
>  static struct queue *gatt_devices = NULL;
> @@ -650,6 +653,13 @@ static void connection_cleanup(struct gatt_device *device)
>                 bt_auto_connect_remove(&device->bdaddr);
>  }
>
> +static void free_adv_instance(struct adv_instance *adv)
> +{
> +       if (adv->instance)
> +               adv_inst_bits &= ~(1 << (adv->instance - 1));
> +       free(adv);
> +}
> +
>  static void destroy_gatt_app(void *data)
>  {
>         struct gatt_app *app = data;
> @@ -674,6 +684,9 @@ static void destroy_gatt_app(void *data)
>
>         queue_destroy(app->notifications, free);
>
> +       if (app->adv)
> +               free_adv_instance(app->adv);
> +
>         free(app);
>  }
>
> @@ -5586,19 +5599,228 @@ static void handle_client_set_scan_param(const void *buf, uint16_t len)
>                                         HAL_STATUS_UNSUPPORTED);
>  }
>
> +static struct adv_instance *find_adv_instance(uint32_t client_if)
> +{
> +       struct gatt_app *app;
> +       struct adv_instance *adv;
> +       uint8_t inst = 0;
> +       unsigned int i;
> +
> +       app = find_app_by_id(client_if);
> +       if (!app)
> +               return NULL;
> +
> +       if (app->adv)
> +               return app->adv;
> +
> +       /* Assume that kernel supports <= 32 advertising instances (5 today)
> +        * We have already indicated the number to the android framework layers
> +        * via the LE features so we don't check again here.
> +        * The kernel will detect the error if needed
> +        */
> +       for (i=0; i < sizeof(adv_inst_bits) * 8; i++) {
> +               uint32_t mask = 1 << i;
> +               if (!(adv_inst_bits & mask)) {
> +                       inst = i + 1;
> +                       adv_inst_bits |= mask;
> +                       break;
> +               }
> +       }
> +       if (!inst)
> +               return NULL;
> +
> +       adv = new0(typeof(*adv), 1);
> +       adv->instance = inst;
> +       app->adv = adv;
> +
> +       DBG("Assigned advertising instance %d for client %d", inst, client_if);
> +
> +       return adv;
> +};
> +
> +/* Add UUIDS of requested size, converting from android 128 to appropriate format */
> +static uint8_t *add_adv_svc_uuids(
> +       uint8_t *dst, const uint8_t *src,
> +       int selected_uuids, int total_uuids, unsigned type)
> +{
> +       int i;
> +
> +       if (!selected_uuids)
> +               return dst;
> +
> +       /* Add TL header for complete list */
> +       switch(type) {
> +               case BT_UUID16:
> +                       *dst++ = (selected_uuids * 2) + 1;
> +                       *dst++ = 0x3;  /* complete list of 16 bit service uuids */
> +                       break;
> +
> +               case BT_UUID128:
> +                       *dst++ = (selected_uuids * 16) + 1;
> +                       *dst++ = 0x7;  /* complete list of 128 bit service uuids */
> +                       break;
> +       }
> +
> +       for (i = 0; i  < total_uuids; i++) {
> +               bt_uuid_t bt_uuid;
> +
> +               android2uuid(src, &bt_uuid);
> +
> +               if (bt_uuid.type != type)
> +                       continue;
> +
> +               bt_uuid_to_le(&bt_uuid, dst);
> +               dst += bt_uuid_len(&bt_uuid);
> +               src += 16;
> +       }
> +
> +       return dst;
> +}
> +
> +/* Build advertising data in TLV format from a data buffer containing
> + * manufacturer_data, service_data, service uuids (in that order)
> + * The input data is raw with no TLV structure and the service uuids are 128 bit
> + */

Have you look at bt_ad:

https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/src/shared/ad.h

You can find how we use in our advertising D-Bus API:

https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/src/advertising.c#n547

Id expect you can reuse most of it even if we have to create a
different helper function to deal with the format used by Android HAL
all the code dealing with LTV should be pretty generic.

> +static struct adv_data *build_adv_data(
> +       int32_t manufacturer_data_len,
> +       int32_t service_data_len,
> +       int32_t service_uuid_len,
> +       const uint8_t *data_in)
> +{
> +       const int one_svc_uuid_len = 128 / 8;  /* Android always sends 128bit UUIDs */
> +       struct adv_data *adv;
> +       uint32_t len = 0;
> +       uint8_t *dst;
> +       const uint8_t *src;
> +       unsigned num_svc_uuids, i, num_uuid16 = 0, num_uuid128 = 0;
> +
> +       if (manufacturer_data_len > 0)
> +               len += (manufacturer_data_len + 2);
> +
> +       if (service_data_len > 0)
> +               len += (service_data_len + 2);
> +
> +       if (service_uuid_len % one_svc_uuid_len) {
> +               error("Service UUIDs not multiple of %d bytes (%d)",
> +                       one_svc_uuid_len, service_uuid_len);
> +               num_svc_uuids = 0;
> +       } else {
> +               num_svc_uuids = service_uuid_len / one_svc_uuid_len;
> +       }
> +
> +       src = data_in + manufacturer_data_len + service_data_len;
> +       for (i = 0; i  < num_svc_uuids; i++) {
> +               bt_uuid_t bt_uuid;
> +
> +               android2uuid(src, &bt_uuid);
> +
> +               switch (bt_uuid.type) {
> +               case BT_UUID16:
> +                       num_uuid16++;
> +                       len += 2;
> +                       break;
> +
> +               case BT_UUID128:
> +                       num_uuid128++;
> +                       len += 16;
> +                       break;
> +
> +               default:
> +                       error("Unsupported UUID length");
> +                       break;
> +               }
> +
> +               src += one_svc_uuid_len;
> +       }
> +
> +       DBG("num svc uuids: 16bit=%d 128bit=%d total=%d\n",
> +               num_uuid16, num_uuid128, num_svc_uuids);
> +
> +       /* UUIDs of same size are grouped with 2 byte GAP header per list */
> +       if (num_uuid16)
> +               len +=2;
> +       if (num_uuid128)
> +               len += 2;
> +
> +       DBG("adv data size = %d", len);
> +
> +       if (len > 0xff) { /* Kernel limit is lower but it will complain if so */
> +               error("Advertising data too big");
> +               return NULL;
> +       }
> +
> +       adv = malloc0(sizeof(*adv) + len);
> +       if (!adv)
> +               return NULL;
> +
> +       adv->len = len;
> +       dst = &adv->data[0];
> +       src = data_in;
> +       if (manufacturer_data_len > 0) {
> +               *dst++ = manufacturer_data_len + 1;
> +               *dst++ = 0xff;
> +               memcpy(dst, src, manufacturer_data_len);
> +               dst += manufacturer_data_len;
> +               src += manufacturer_data_len;
> +       }
> +
> +       if (service_data_len > 0) {
> +               *dst++ = service_data_len + 1;
> +               *dst++ = 0x16; /* Service data, 16 bit UUID */
> +               memcpy(dst, src, service_data_len);
> +               dst += service_data_len;
> +               src += service_data_len;
> +       }
> +
> +       dst = add_adv_svc_uuids(dst, src, num_uuid16, num_svc_uuids, BT_UUID16);
> +       dst = add_adv_svc_uuids(dst, src, num_uuid128, num_svc_uuids, BT_UUID128);
> +
> +       return adv;
> +}
> +
> +
>  static void handle_client_setup_multi_adv(const void *buf, uint16_t len)
>  {
>         const struct hal_cmd_gatt_client_setup_multi_adv *cmd = buf;
> +       struct hal_ev_gatt_client_multi_adv_enable ev;
> +       struct adv_instance *adv;
> +       uint8_t status;
>
> -       DBG("client_if %d", cmd->client_if);
> +       DBG("client_if %d min_interval=%d max_interval=%d type=%d channel_map=0x%x tx_power=%d timeout=%d",
> +               cmd->client_if,
> +               cmd->min_interval,
> +               cmd->max_interval,
> +               cmd->type,
> +               cmd->channel_map,
> +               cmd->tx_power,
> +               cmd->timeout);
> +
> +       adv = find_adv_instance(cmd->client_if);
> +       if (!adv) {
> +               status = HAL_STATUS_FAILED;
> +               goto out;
> +       }
>
> -       /* TODO */
> +       status = HAL_STATUS_SUCCESS;
> +       adv->timeout = cmd->timeout;
> +       adv->type = cmd->type;
> +       if (adv->type != ANDROID_ADVERTISING_EVENT_TYPE_SCANNABLE) {
> +               free(adv->sr_data);
> +               adv->sr_data = NULL;
> +       }
>
> +out:
>         ipc_send_rsp(hal_ipc, HAL_SERVICE_ID_GATT,
>                                         HAL_OP_GATT_CLIENT_SETUP_MULTI_ADV,
> -                                       HAL_STATUS_UNSUPPORTED);
> +                                       status);
> +
> +       ev.client_if = cmd->client_if;
> +       ev.status = status;
> +       ipc_send_notif(hal_ipc, HAL_SERVICE_ID_GATT,
> +                       HAL_EV_GATT_CLIENT_MULTI_ADV_ENABLE, sizeof(ev), &ev);
>  }
>
> +/* This is not currently called by Android 5.1 */
>  static void handle_client_update_multi_adv(const void *buf, uint16_t len)
>  {
>         const struct hal_cmd_gatt_client_update_multi_adv *cmd = buf;
> @@ -5612,30 +5834,158 @@ static void handle_client_update_multi_adv(const void *buf, uint16_t len)
>                                         HAL_STATUS_UNSUPPORTED);
>  }
>
> +struct addrm_adv_cb_data {
> +       int32_t client_if;
> +       struct adv_instance *adv;
> +};
> +
> +static void add_advertising_cb(uint8_t status, void *user_data)
> +{
> +       struct addrm_adv_cb_data *cb_data = user_data;
> +       struct hal_ev_gatt_client_multi_adv_data ev = {
> +               .status = status,
> +               .client_if = cb_data->client_if,
> +       };
> +
> +       ipc_send_notif(hal_ipc, HAL_SERVICE_ID_GATT,
> +                               HAL_EV_GATT_CLIENT_MULTI_ADV_DATA,
> +                               sizeof(ev), &ev);
> +
> +       free(cb_data);
> +}
> +
>  static void handle_client_setup_multi_adv_inst(const void *buf, uint16_t len)
>  {
>         const struct hal_cmd_gatt_client_setup_multi_adv_inst *cmd = buf;
> +       struct adv_instance *adv;
> +       struct adv_data *adv_data;
> +       struct addrm_adv_cb_data *cb_data = NULL;
> +       uint8_t status = HAL_STATUS_FAILED;
> +
> +       DBG("client_if %d set_scan_rsp=%d include_name=%d include_tx_power=%d appearance=%d manuf_data_len=%d svc_data_len=%d svc_uuid_len=%d",
> +               cmd->client_if,
> +               cmd->set_scan_rsp,
> +               cmd->include_name,
> +               cmd->include_tx_power,
> +               cmd->appearance,
> +               cmd->manufacturer_data_len,
> +               cmd->service_data_len,
> +               cmd->service_uuid_len
> +       );
> +
> +       adv = find_adv_instance(cmd->client_if);
> +       if (!adv)
> +               goto out;
> +
> +       adv->include_tx_power = cmd->include_tx_power ? 1 : 0;
> +
> +       adv_data = build_adv_data(
> +                       cmd->manufacturer_data_len,
> +                       cmd->service_data_len,
> +                       cmd->service_uuid_len,
> +                       cmd->data_service_uuid);
> +       if (!adv_data)
> +               goto out;
> +
> +       if (cmd->set_scan_rsp) {
> +               free(adv->sr_data);
> +               adv->sr_data = adv_data;
> +       } else {
> +               free(adv->adv_data);
> +               adv->adv_data = adv_data;
> +       }
>
> -       DBG("client_if %d", cmd->client_if);
> +       cb_data = new0(typeof(*cb_data), 1);
> +       cb_data->client_if = cmd->client_if;
> +       cb_data->adv = adv;
>
> -       /* TODO */
> +       if (!bt_le_add_advertising(adv, add_advertising_cb, cb_data)) {
> +               error("gatt: Could not add advertising");
> +               free(cb_data);
> +               goto out;
> +       }
>
> +       status = HAL_STATUS_SUCCESS;
> +
> +out:
>         ipc_send_rsp(hal_ipc, HAL_SERVICE_ID_GATT,
> -                                       HAL_OP_GATT_CLIENT_SETUP_MULTI_ADV_INST,
> -                                       HAL_STATUS_UNSUPPORTED);
> +                               HAL_OP_GATT_CLIENT_SETUP_MULTI_ADV_INST,
> +                               status);
> +
> +       if (status != HAL_STATUS_SUCCESS) {
> +               struct hal_ev_gatt_client_multi_adv_data ev = {
> +                       .status = status,
> +                       .client_if = cmd->client_if,
> +               };
> +
> +               ipc_send_notif(hal_ipc, HAL_SERVICE_ID_GATT,
> +                               HAL_EV_GATT_CLIENT_MULTI_ADV_DATA,
> +                               sizeof(ev), &ev);
> +       }
> +}
> +
> +static void remove_advertising_cb(uint8_t status, void *user_data)
> +{
> +       struct addrm_adv_cb_data *cb_data = user_data;
> +       struct hal_ev_gatt_client_multi_adv_data ev = {
> +               .status = status,
> +               .client_if = cb_data->client_if,
> +       };
> +
> +       free_adv_instance(cb_data->adv);
> +
> +       ipc_send_notif(hal_ipc, HAL_SERVICE_ID_GATT,
> +                               HAL_EV_GATT_CLIENT_MULTI_ADV_DISABLE,
> +                               sizeof(ev), &ev);
> +
> +       free(cb_data);
>  }
>
>  static void handle_client_disable_multi_adv_inst(const void *buf, uint16_t len)
>  {
>         const struct hal_cmd_gatt_client_disable_multi_adv_inst *cmd = buf;
> +       struct adv_instance *adv;
> +       struct gatt_app *app;
> +       struct addrm_adv_cb_data *cb_data = NULL;
> +       uint8_t status = HAL_STATUS_FAILED;
>
>         DBG("client_if %d", cmd->client_if);
>
> -       /* TODO */
> +       adv = find_adv_instance(cmd->client_if);
> +       if (!adv)
> +               goto out;
> +
> +       cb_data = new0(typeof(*cb_data), 1);
> +       cb_data->client_if = cmd->client_if;
> +       cb_data->adv = adv;
> +
> +       if (!bt_le_remove_advertising(adv, remove_advertising_cb, cb_data)) {
> +               error("gatt: Could not remove advertising");
> +               free(cb_data);
> +               goto out;
> +       }
>
> +       app = find_app_by_id(cmd->client_if);
> +       if (app)
> +               app->adv = NULL;
> +
> +       status = HAL_STATUS_SUCCESS;
> +
> +out:
>         ipc_send_rsp(hal_ipc, HAL_SERVICE_ID_GATT,
>                                 HAL_OP_GATT_CLIENT_DISABLE_MULTI_ADV_INST,
> -                               HAL_STATUS_UNSUPPORTED);
> +                               status);
> +
> +       if (status != HAL_STATUS_SUCCESS) {
> +               struct hal_ev_gatt_client_multi_adv_data ev = {
> +                       .status = status,
> +                       .client_if = cmd->client_if,
> +               };
> +
> +               ipc_send_notif(hal_ipc, HAL_SERVICE_ID_GATT,
> +                               HAL_EV_GATT_CLIENT_MULTI_ADV_DISABLE,
> +                               sizeof(ev), &ev);
> +       }
>  }
>
>  static void handle_client_configure_batchscan(const void *buf, uint16_t len)
> @@ -5824,7 +6174,7 @@ static const struct ipc_handler cmd_handlers[] = {
>         { handle_client_update_multi_adv, false,
>                 sizeof(struct hal_cmd_gatt_client_update_multi_adv) },
>         /* HAL_OP_GATT_CLIENT_SETUP_MULTI_ADV_INST */
> -       { handle_client_setup_multi_adv_inst, false,
> +       { handle_client_setup_multi_adv_inst, true,
>                 sizeof(struct hal_cmd_gatt_client_setup_multi_adv_inst) },
>         /* HAL_OP_GATT_CLIENT_DISABLE_MULTI_ADV_INST */
>         { handle_client_disable_multi_adv_inst, false,
>
> --
> To unsubscribe from this list: send the line "unsubscribe linux-bluetooth" in
> the body of a message to majordomo@xxxxxxxxxxxxxxx
> More majordomo info at  http://vger.kernel.org/majordomo-info.html



-- 
Luiz Augusto von Dentz
--
To unsubscribe from this list: send the line "unsubscribe linux-bluetooth" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html



[Index of Archives]     [Bluez Devel]     [Linux Wireless Networking]     [Linux Wireless Personal Area Networking]     [Linux ATH6KL]     [Linux USB Devel]     [Linux Media Drivers]     [Linux Audio Users]     [Linux Kernel]     [Linux SCSI]     [Big List of Linux Books]

  Powered by Linux