В начало | Зарегистрироваться | Заказать наши киты почтой
 
 
 
 

Обмен данными между Android-приложением и nRF52832. Часть 2. BLE

📆8 мая 2023   ✒️ayan   🔎608   💬0  
Обмен данными между Android-приложением и nRF52832. Часть 2. BLE

Приветствую вас, уважаемые датагорцы!
Сегодня — обмен данными между Android-приложением и nRF52832 посредством низко-энергетического Bluetooth.

Видео-демонстрация проекта



Код для nRF52832

Программа для nRF52832 написана на языке Си в Visual Studio Code и обеспечивает следующие функции:
1. Измерение температуры посредством датчика DS18B20.

2. До соединения Android, выступающего в качестве центрального устройства — широковещание (advertising) с передачей в эфир следующих GAP-данных:
• адрес устройства,
• тип устройства (LE only),
• флаги (General Discoverable Mode, BR/EDR Not Supported),
• UUID и имя сервиса.

3. После соединения с центральным устройством — обмен с последним GATT-данными, в т.ч.:
• уведомления (notifications) о текущем значении температуры,
• изменение значения характеристики (состояния светодиода) при поступлении соответствующей команды от Android.

Контроль за светодиодом

Управление светодиодом, подключённого к пину P0.17, осуществляется через одноимённую библиотеку:
led.h
#ifndef LED_H_
#define LED_H_

#include "nrf.h"

#define LED        17

#define LED_OFF    NRF_GPIO->OUT |= (1UL << LED)
#define LED_ON     NRF_GPIO->OUT &= ~(1UL << LED)

void ledInit();

#endif

led.c
#include "led.h"

void ledInit()
{
  NRF_GPIO->PIN_CNF[LED] |= (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);
  LED_OFF;
}

Контроль за DS18B20

В подробно описанный в в моей статье "Электронные часы-термометр с беспроводным датчиком через радиомодуль nRF24L01" код управления температурным датчиком внесено лишь одно изменение: измерение и чтение значения температуры выделены в отдельные функции (DS18B20StartConversion и DS18B20ReadTemperature), что обусловлено таймингом BLE-соединения:
oneWire.h
#ifndef _OWI_H_
#define _OWI_H_

#include "nrf.h"
#include "nrf_delay.h"

#define DELAY_A             6
#define DELAY_B             64
#define DELAY_C             60
#define DELAY_D             10
#define DELAY_E             9
#define DELAY_F             55
#define DELAY_G             0
#define DELAY_H             480
#define DELAY_I             70
#define DELAY_J             410

#define MSBit               0x80
#define LSBit               0x01

#define OWI_BUS             4
#define DIR                 0 
#define PULL                2 

#define OWI_RELEASE_BUS     NRF_GPIO->PIN_CNF[OWI_BUS] = 0 
#define OWI_PULL_BUS_LOW    NRF_GPIO->PIN_CNF[OWI_BUS] = 1 << DIR

void owiInit(void);
void owiReset(void);
void owiWriteBit0(void);
void owiWriteBit1(void);
unsigned char owiReadBit(void);
void owiSendByte(unsigned char data);
unsigned char owiReceiveByte(void);

#endif

oneWire.c
#include "oneWire.h"

void owiInit(void) 
{
  OWI_RELEASE_BUS;
  nrf_delay_us(DELAY_H);
}

void owiReset(void) 
{ 
  OWI_PULL_BUS_LOW;
  nrf_delay_us(DELAY_H);  

  OWI_RELEASE_BUS;
  nrf_delay_us(DELAY_I);  
  
  nrf_delay_us(DELAY_J);  
}

void owiWriteBit0(void) 
{
  OWI_PULL_BUS_LOW;
  nrf_delay_us(DELAY_C);
    
  OWI_RELEASE_BUS;
  nrf_delay_us(DELAY_D);  
}

void owiWriteBit1(void) 
{
  OWI_PULL_BUS_LOW;
  nrf_delay_us(DELAY_A);
    
  OWI_RELEASE_BUS;
  nrf_delay_us(DELAY_B);
}

unsigned char owiReadBit(void) 
{  
  uint32_t bitsRead = 0;

  OWI_PULL_BUS_LOW;
  nrf_delay_us(DELAY_A);  

  OWI_RELEASE_BUS;
  nrf_delay_us(DELAY_E);  

  if(NRF_GPIO->IN & (1 << OWI_BUS)) 
    bitsRead = 1;  
  nrf_delay_us(DELAY_F); 
  
  return bitsRead;
}

void owiSendByte(unsigned char data) 
{
  unsigned char temp, currentBit;
    
  for (currentBit = 0; currentBit < 8; currentBit++) 
  {
    temp = data & 0x01;
    if (temp) 
    {
      owiWriteBit1();
    } 
    else 
    {
      owiWriteBit0();
    }
    data >>= 1;
  }  
}

unsigned char owiReceiveByte(void) 
{
  unsigned char data = 0, currentBit = 0;  

  for (currentBit = 0; currentBit < 8; currentBit++) 
  {  
    data >>= 1;
    if (owiReadBit()) 
    {
      data |= MSBit;
    }
  }
  return data;
}

DS18B20.h
#ifndef _DS18B20_H_
#define _DS18B20_H_

#include <stdint.h>
#include "oneWire.h"

#define SKIP_ROM            0xCC
#define READ_SCRATCH_PAD    0xBE
#define CONVERT_T           0x44

#define MINUS               1
#define PLUS                0

#define FREE_STATE          0
#define CONVERSION_STATE    1

void DS18B20Init();
void DS18B20StartConversion() ;
void DS18B20ReadTemperature();

#endif

DS18B20.c
#include "DS18B20.h"

uint8_t temperature[4] = {0}, DS18B20_state = FREE_STATE;


void DS18B20Init() 
{
  owiInit();
}

void DS18B20StartConversion() 
{  
  DS18B20_state = CONVERSION_STATE;
  owiReset();
  owiSendByte(SKIP_ROM);
  owiSendByte(CONVERT_T);
}

void DS18B20ReadTemperature(void) 
{
  unsigned char ds18b20_data[2]; 
  
  owiReset();
  owiSendByte(SKIP_ROM);
  owiSendByte(READ_SCRATCH_PAD);
  for(int i = 0; i < 2; i++)
    {
      ds18b20_data[i] = owiReceiveByte();
    }
  owiReset();
  
  float currentTemperature = (ds18b20_data[1] << 8 | ds18b20_data[0])/16.0;

  if(currentTemperature >= 0)
  {
    temperature[0] = PLUS;
  }
  else if(currentTemperature < 0)
  {
    temperature[0] = MINUS;
  }
            
  temperature[1] = (uint8_t)(currentTemperature / 10);
  temperature[2] = (uint8_t)(currentTemperature - temperature[1] * 10);
  temperature[3] = (uint8_t)(currentTemperature * 10 - temperature[1] * 100 - temperature[2] * 10);  
  DS18B20_state = FREE_STATE;
}

Контроль за BLE

За основу принят шаблон ble_app_template из SDK 17.0.2.

Далее, добавлена библиотека myService, которая обеспечивает работу сервиса посредством функций, имеющими следующее назначение:
myServiceInit — инициализация сервиса.
on_connect и on_disconnect — соединение и разъединение с центральным устройством, соответственно.
on_write — изменение состояния светодиода и разрешение уведомлений при получении соответствующих данных от Android.
custom_value_char_add — добавление характеристики в сервис.
ble_cus_custom_value_update — обновление значения температуры в уведомлении.
ble_cus_on_ble_evt — обработка событий BLE.

myService.h
#ifndef MY_SERVICE_
#define MY_SERVICE_

#include <stdint.h>
#include <stdbool.h>
#include "ble.h"
#include "ble_srv_common.h"
#include "led.h"

#define CUSTOM_SERVICE_UUID_BASE        {0xBC, 0x8A, 0xBF, 0x45, 0xCA, 0x05, 0x50, 0xBA, \
                                         0x40, 0x42, 0xB0, 0x00, 0xC9, 0xAD, 0x64, 0xF3}
#define MY_SERVICE_UUID                  0x1400
#define MY_VALUE_CHAR_UUID               0x1401

#define BLE_CUS_DEF(_name)                                                                          \
static ble_cus_t _name;                                                                             \
NRF_SDH_BLE_OBSERVER(_name ## _obs,                                                                 \
                     BLE_HRS_BLE_OBSERVER_PRIO,                                                     \
                     ble_cus_on_ble_evt, &_name)

typedef enum
{
  BLE_CUS_EVT_NOTIFICATION_ENABLED,                             
  BLE_CUS_EVT_NOTIFICATION_DISABLED,                            
  BLE_CUS_EVT_DISCONNECTED,
  BLE_CUS_EVT_CONNECTED
} ble_cus_evt_type_t;

typedef struct
{
  ble_cus_evt_type_t evt_type;                                  
} ble_cus_evt_t;

typedef struct ble_cus_s ble_cus_t;
typedef void (*ble_cus_evt_handler_t) (ble_cus_t * p_bas, ble_cus_evt_t * p_evt);

typedef struct
{
  ble_cus_evt_handler_t         evt_handler;   
  uint8_t                       initial_custom_value;           
  ble_srv_cccd_security_mode_t  custom_value_char_attr_md;     
} ble_cus_init_t;

struct ble_cus_s
{
  ble_cus_evt_handler_t         evt_handler; 
  uint16_t                      service_handle;                 
  ble_gatts_char_handles_t      custom_value_handles;           
  uint16_t                      conn_handle;                    
  uint8_t                       uuid_type; 
};

uint32_t myServiceInit(ble_cus_t * p_cus, const ble_cus_init_t * p_cus_init);
void ble_cus_on_ble_evt( ble_evt_t const * p_ble_evt, void * p_context);
uint32_t ble_cus_custom_value_update(ble_cus_t * p_cus, uint8_t temperature[4]);

#endif

myService.c
#include "sdk_common.h"
#include "ble_srv_common.h"
#include "myService.h"
#include <string.h>
#include "nrf_log.h"

static void on_connect(ble_cus_t * p_cus, ble_evt_t const * p_ble_evt)
{
  p_cus->conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
  ble_cus_evt_t evt;
  evt.evt_type = BLE_CUS_EVT_CONNECTED;
  p_cus->evt_handler(p_cus, &evt);    
}

static void on_disconnect(ble_cus_t * p_cus, ble_evt_t const * p_ble_evt)
{
  UNUSED_PARAMETER(p_ble_evt);
  p_cus->conn_handle = BLE_CONN_HANDLE_INVALID;
}

static void on_write(ble_cus_t * p_cus, ble_evt_t const * p_ble_evt)
{
  ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;
    
  if (p_evt_write->handle == p_cus->custom_value_handles.value_handle)
  {
    if(p_evt_write->data[0] == 1)
    {
      NRF_LOG_INFO("Led ON"); 
      LED_ON;
    }
    else if(p_evt_write->data[0] == 2)
    {
      NRF_LOG_INFO("Led OFF");
      LED_OFF; 
    }        
  }

  if ((p_evt_write->handle == p_cus->custom_value_handles.cccd_handle) && (p_evt_write->len == 2))
  {
    if (p_cus->evt_handler != NULL)
    {
      ble_cus_evt_t evt;

      if (ble_srv_is_notification_enabled(p_evt_write->data))
      {
        evt.evt_type = BLE_CUS_EVT_NOTIFICATION_ENABLED;
      }
      else
      {
        evt.evt_type = BLE_CUS_EVT_NOTIFICATION_DISABLED;
      }
      p_cus->evt_handler(p_cus, &evt);
    }
  }    
}

static uint32_t custom_value_char_add(ble_cus_t * p_cus, const ble_cus_init_t * p_cus_init)
{
  uint32_t            err_code;
  ble_gatts_char_md_t char_md;
  ble_gatts_attr_md_t cccd_md;
  ble_gatts_attr_t    attr_char_value;
  ble_uuid_t          ble_uuid;
  ble_gatts_attr_md_t attr_md;

  ble_uuid.type = p_cus->uuid_type;
  ble_uuid.uuid = MY_SERVICE_UUID;

  memset(&char_md, 0, sizeof(char_md));
  char_md.char_props.read   = 1;
  char_md.char_props.write  = 1;
  char_md.char_props.notify = 1; 
  char_md.p_char_user_desc  = NULL;
  char_md.p_char_pf         = NULL;
  char_md.p_user_desc_md    = NULL;
  char_md.p_cccd_md         = &cccd_md; 
  char_md.p_sccd_md         = NULL;
		
  memset(&attr_md, 0, sizeof(attr_md));
  attr_md.read_perm  = p_cus_init->custom_value_char_attr_md.read_perm;
  attr_md.write_perm = p_cus_init->custom_value_char_attr_md.write_perm;
  attr_md.vloc       = BLE_GATTS_VLOC_STACK;
  attr_md.rd_auth    = 0;
  attr_md.wr_auth    = 0;
  attr_md.vlen       = 0;

  memset(&attr_char_value, 0, sizeof(attr_char_value));
  attr_char_value.p_uuid    = &ble_uuid;
  attr_char_value.p_attr_md = &attr_md;
  attr_char_value.init_len  = 4; 
  attr_char_value.init_offs = 0;
  attr_char_value.max_len   = 4; 

  memset(&cccd_md, 0, sizeof(cccd_md));
  BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.read_perm);
  BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.write_perm);
    
  cccd_md.vloc = BLE_GATTS_VLOC_STACK;

  err_code = sd_ble_gatts_characteristic_add(p_cus->service_handle, &char_md, &attr_char_value, &p_cus->custom_value_handles);
  if (err_code != NRF_SUCCESS)
  {
    return err_code;
  }

  return NRF_SUCCESS;
}

uint32_t ble_cus_custom_value_update(ble_cus_t * p_cus, uint8_t temperature[4])
{
  if (p_cus == NULL)
  {
    return NRF_ERROR_NULL;
  }

  uint32_t err_code = NRF_SUCCESS;
  ble_gatts_value_t gatts_value;

  memset(&gatts_value, 0, sizeof(gatts_value));
  gatts_value.len     = 4; 
  gatts_value.offset  = 0;
  gatts_value.p_value = &temperature[0];

  err_code = sd_ble_gatts_value_set(p_cus->conn_handle, p_cus->custom_value_handles.value_handle, &gatts_value);
                                      
  if (err_code != NRF_SUCCESS)
  {
    return err_code;
  }

  if ((p_cus->conn_handle != BLE_CONN_HANDLE_INVALID)) 
  {
    ble_gatts_hvx_params_t hvx_params;

    memset(&hvx_params, 0, sizeof(hvx_params));
    hvx_params.handle = p_cus->custom_value_handles.value_handle;
    hvx_params.type   = BLE_GATT_HVX_NOTIFICATION;
    hvx_params.offset = gatts_value.offset;
    hvx_params.p_len  = &gatts_value.len;
    hvx_params.p_data = gatts_value.p_value;

    err_code = sd_ble_gatts_hvx(p_cus->conn_handle, &hvx_params);
  }
  else
  {
    err_code = NRF_ERROR_INVALID_STATE;
  }

  return err_code;
}

uint32_t myServiceInit(ble_cus_t * p_cus, const ble_cus_init_t * p_cus_init)
{
  if (p_cus == NULL || p_cus_init == NULL)
  {
    return NRF_ERROR_NULL;
  }

  uint32_t   err_code;
  ble_uuid_t ble_uuid;    

  p_cus->evt_handler               = p_cus_init->evt_handler;
  p_cus->conn_handle               = BLE_CONN_HANDLE_INVALID;

  ble_uuid128_t base_uuid = {custom_SERVICE_UUID_BASE};
  err_code =  sd_ble_uuid_vs_add(&base_uuid, &p_cus->uuid_type);
  VERIFY_SUCCESS(err_code);

  ble_uuid.type = p_cus->uuid_type;
  ble_uuid.uuid = MY_SERVICE_UUID;    

  err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_cus->service_handle);
  if (err_code != NRF_SUCCESS)
  {
    return err_code;
  }    

  return custom_value_char_add(p_cus, p_cus_init);
}

void ble_cus_on_ble_evt( ble_evt_t const * p_ble_evt, void * p_context)
{
  ble_cus_t * p_cus = (ble_cus_t *) p_context;
    
  if (p_cus == NULL || p_ble_evt == NULL)
  {
    return;
  }

  switch (p_ble_evt->header.evt_id)
  {
    case BLE_GAP_EVT_CONNECTED:
      on_connect(p_cus, p_ble_evt);
    break;

    case BLE_GAP_EVT_DISCONNECTED:
      on_disconnect(p_cus, p_ble_evt);
    break;

    case BLE_GATTS_EVT_WRITE:
      on_write(p_cus, p_ble_evt);
    break;

    default:        
    break;
  }    
}

Кроме того, в main.c добавлены следующие фрагменты:
1. Функция notification_timeout_handler измеряет температуру и обновляет её значение в уведомлении.
static void notification_timeout_handler(void * p_context)
{
  UNUSED_PARAMETER(p_context);
  ret_code_t err_code;    

  if(DS18B20_state == FREE_STATE)
  {
    DS18B20StartConversion();
  }
  else if(DS18B20_state == CONVERSION_STATE)
  {
    DS18B20ReadTemperature();
    NRF_LOG_INFO("%d%d.%d", temperature[1], temperature[2], temperature[3]);
    err_code = ble_cus_custom_value_update(&m_cus, &temperature[0]);
    APP_ERROR_CHECK(err_code);        
  }        
}

2. Функция timers_init настраивает один из таймеров, по прерыванию которого каждые 750 мс процессор переходит в к предыдущей функции.
static void timers_init(void)
{
  ret_code_t err_code = app_timer_init();
  APP_ERROR_CHECK(err_code);

  err_code = app_timer_create(&m_notification_timer_id, APP_TIMER_MODE_REPEATED, notification_timeout_handler);
  APP_ERROR_CHECK(err_code);
}

3. Обработчик on_cus_evt включает/выключает упомянутый выше таймер при поступлении от Android разрешения/запрета на уведомления, соответственно.
static void on_cus_evt(ble_cus_t * p_cus_service, ble_cus_evt_t * p_evt)
{
  ret_code_t err_code;

  switch(p_evt->evt_type)
  {
    case BLE_CUS_EVT_NOTIFICATION_ENABLED:
      err_code = app_timer_start(m_notification_timer_id, NOTIFICATION_INTERVAL, NULL);
      APP_ERROR_CHECK(err_code);        
    break;

    case BLE_CUS_EVT_NOTIFICATION_DISABLED:
      err_code = app_timer_stop(m_notification_timer_id);
      APP_ERROR_CHECK(err_code);        
    break;

    case BLE_CUS_EVT_CONNECTED:
    break;

    case BLE_CUS_EVT_DISCONNECTED:
    break;

    default:
    break;
  }
}

4. Функция services_init инициализирует сервис и определяет on_cus_evt в качестве обработчика BLE-событий.
static void services_init(void)
{
  ret_code_t         err_code;
  nrf_ble_qwr_init_t qwr_init = {0};
  ble_cus_init_t     cus_init = {0};

  qwr_init.error_handler = nrf_qwr_error_handler;

  err_code = nrf_ble_qwr_init(&m_qwr, &qwr_init);
  APP_ERROR_CHECK(err_code);
    
  cus_init.evt_handler                = on_cus_evt;

  BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cus_init.custom_value_char_attr_md.cccd_write_perm);
  BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cus_init.custom_value_char_attr_md.read_perm);
  BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cus_init.custom_value_char_attr_md.write_perm);	

  err_code = myServiceInit(&m_cus, &cus_init);
  APP_ERROR_CHECK(err_code);	
}

В завершение, внесены соответствующие дополнения в основную функцию.
int main(void)
{
  ledInit();
  DS18B20Init();
  log_init();
  timers_init();
  power_management_init();
  ble_stack_init();
  gap_params_init();
  gatt_init();
  services_init();    
  advertising_init();
  conn_params_init();
  peer_manager_init();
  advertising_start(true);

  while(1)
  {
    idle_state_handle();
  }
}

Код проекта для nRF52832 выложен в архив статьи.

Android-приложение

Приложение написано на языке Java в Android Studio.

В манифесте прописаны разрешения на работу с Bluetooth.
AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.Ds18b20"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

В xml-файле определены параметры:
• кнопки сканирования BLE-устройств,
• кнопок соединения/разъединения с заданным периферийным устройством,
• кнопок включения/выключения светодиода, входящего в состав nRF52832,
• текстового поля для отражения значения температуры.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/scanBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="scan"
        android:text="scan"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.049"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.046" />

    <Button
        android:id="@+id/connectBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="connect"
        android:text="connect"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.046" />

    <Button
        android:id="@+id/disconnectBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="disconnect"
        android:text="disconnect"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.046" />

    <Button
        android:id="@+id/ledOnbtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="ledOn"
        android:text="on"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.95"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.046" />

    <Button
        android:id="@+id/ledOffbtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="ledOff"
        android:text="off"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.949"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.046" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="402dp"
        android:layout_height="146dp"
        android:gravity="center"
        android:text="+00.0℃"
        android:textSize="50sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.444"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.256" />

</androidx.constraintlayout.widget.ConstraintLayout>

Код Java-файла по нажатию соответствующей кнопки:
а) Осуществляет сканирование и, в случае обнаружения BLE-устройства, выдаёт сообщение с указанием его адреса.
б) Производит соединение с заданным устройством и разрешает уведомления от последнего.
в) Передаёт команду на запись значения характеристики, ответственного за состояние светодиода.

Кроме того, по получению уведомления от nRF52832 обновляется значение температуры в текстовом поле.
MainActivity.java
package com.punchthrough.ds18b20;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Trace;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;

public class MainActivity extends AppCompatActivity {

    private String TAG = "MainActivity";

    public final static String ACTION_DATA_AVAILABLE =
            "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";

    public static final UUID SERVICE_UUID = UUID.fromString("f3641400-00b0-4240-ba50-05ca45bf8abc");
    public static final UUID CHARACTERISTIC_UUID = UUID.fromString("f3641400-00b0-4240-ba50-05ca45bf8abc");

    public static final UUID DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
    BluetoothDevice device;

    BluetoothGatt bluetoothGatt;

    Button scanButton, connectBtn, disconnectBtn, ledOnBtn, ledOffBtn;

    TextView textView;

    String[] names = new String[]{"myService"};
    List<ScanFilter> filters = null;

    BluetoothGattCharacteristic gattCharacteristic;

    BluetoothGattDescriptor descriptor;

    private final static int REQUEST_ENABLE_BT = 1;
    private static final int PERMISSION_REQUEST_COARSE_LOCATION = 1;


    ScanSettings scanSettings = new ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
            .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
            .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
            .setReportDelay(0L)
            .build();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        scanButton = findViewById(R.id.scanBtn);
        connectBtn = findViewById(R.id.connectBtn);
        connectBtn.setEnabled(false);
        disconnectBtn = findViewById(R.id.disconnectBtn);
        disconnectBtn.setEnabled(false);
        disconnectBtn.setVisibility(View.INVISIBLE);
        ledOnBtn = findViewById(R.id.ledOnbtn);
        ledOnBtn.setEnabled(false);
        ledOffBtn = findViewById(R.id.ledOffbtn);
        ledOffBtn.setVisibility(View.INVISIBLE);
        textView = findViewById(R.id.textView);

        if (names != null) {
            filters = new ArrayList<>();
            for (String name : names) {
                ScanFilter filter = new ScanFilter.Builder()
                        .setDeviceName(name)
                        .build();
                filters.add(filter);
            }
        }

        if (adapter != null && !adapter.isEnabled()) {
            if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
            }
            Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
        }

        if (this.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            final AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("This app needs location access");
            builder.setMessage("Please grant location access so this app can detect peripherals.");
            builder.setPositiveButton(android.R.string.ok, null);
            builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
                @Override
                public void onDismiss(DialogInterface dialog) {
                    requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_COARSE_LOCATION);
                }
            });
            builder.show();
        }
    }

    public void scan(View view) {
        if (scanner != null) {
            if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
            }
            scanner.startScan(filters, scanSettings, scanCallback);
            Toast.makeText(this, "scanning...", Toast.LENGTH_LONG).show();
        } else {
            Toast.makeText(this, "could not get scanner object", Toast.LENGTH_LONG).show();
        }
    }

    public void connect(View view) {
        Toast.makeText(this, "connecting...", Toast.LENGTH_LONG).show();
        if (scanner != null) {
            if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
            }
            bluetoothGatt = device.connectGatt(MainActivity.this, false, btleGattCallback);
            ledOnBtn.setEnabled(true);
            ledOffBtn.setEnabled(true);
        } else {
            Toast.makeText(this, "could not connect", Toast.LENGTH_LONG).show();
        }
    }

    public void disconnect(View view) {
        if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
        }
        bluetoothGatt.disconnect();

        ledOnBtn.setEnabled(false);
        ledOffBtn.setEnabled(false);
        connectBtn.setEnabled(false);
    }

    private final ScanCallback scanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            device = result.getDevice();
            Toast.makeText(MainActivity.this, "device with address" + "\n" + device.getAddress() + "\n" + "found", Toast.LENGTH_LONG).show();
            if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
            }
            scanner.stopScan(scanCallback);

            connectBtn.setEnabled(true);
            disconnectBtn.setEnabled(true);
            ledOnBtn.setEnabled(true);
        }
    };


    private final BluetoothGattCallback btleGattCallback = new BluetoothGattCallback() {

        @Override
        public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {
            System.out.println(newState);
            switch (newState) {
                case 0:
                    MainActivity.this.runOnUiThread(new Runnable() {
                        public void run() {
                            Toast.makeText(MainActivity.this, "device disconnected", Toast.LENGTH_SHORT).show();
                            connectBtn.setVisibility(View.VISIBLE);
                            disconnectBtn.setVisibility(View.INVISIBLE);
                        }
                    });
                    break;
                case 2:
                    MainActivity.this.runOnUiThread(new Runnable() {
                        public void run() {
                            Toast.makeText(MainActivity.this, "device connected", Toast.LENGTH_SHORT).show();
                            connectBtn.setVisibility(View.INVISIBLE);
                            disconnectBtn.setVisibility(View.VISIBLE);
                        }
                    });

                    if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
                        ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
                    }
                    bluetoothGatt.discoverServices();

                    break;
                default:
                    MainActivity.this.runOnUiThread(new Runnable() {
                        public void run() {
                            Log.i(TAG, "we encounterned an unknown state, uh oh\n");
                        }
                    });
                    break;
            }
        }

        @Override
        public void onServicesDiscovered(final BluetoothGatt gatt, final int status) {
            MainActivity.this.runOnUiThread(new Runnable() {
                public void run() {
                    Log.i(TAG, "device services have been discovered\n");
                }
            });

            if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
            }
            gattCharacteristic = gatt.getService(SERVICE_UUID).getCharacteristic(CHARACTERISTIC_UUID);
            gatt.setCharacteristicNotification(gattCharacteristic, true);

            descriptor = gattCharacteristic.getDescriptor(DESCRIPTOR_UUID);
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);

            gatt.writeDescriptor(descriptor);
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {

            byte[] data = characteristic.getValue();

            MainActivity.this.runOnUiThread(new Runnable() {
                @SuppressLint("SetTextI18n")
                public void run() {
                    if ((data[0] == 0)) {
                        textView.setText("+" + data[1] + data[2] + "." + data[3] + "\u2103");
                    }
                    if ((data[0] == 1)) {
                        textView.setText("-" + + data[1] + data[2] + "." + data[3] + "\u2103");
                    }
                }
            });
        }
    };

    public void ledOn(View view) {
        byte[] value = new byte[1];
        value[0] = (byte) (0x01);
        BluetoothGattService mCustomService = bluetoothGatt.getService(UUID.fromString("f3641400-00b0-4240-ba50-05ca45bf8abc"));
        if(mCustomService == null){
            Log.i(TAG, "Custom BLE Service not found");
            return;
        }
        BluetoothGattCharacteristic characteristic = mCustomService.getCharacteristic(UUID.fromString("f3641400-00b0-4240-ba50-05ca45bf8abc"));
        characteristic.setValue(value);

        if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
        }
        bluetoothGatt.writeCharacteristic(characteristic);

        ledOnBtn.setVisibility(View.INVISIBLE);
        ledOffBtn.setVisibility(View.VISIBLE);
    }

    public void ledOff(View view) {
        byte[] value = new byte[1];
        value[0] = (byte) (0x02);
        BluetoothGattService mCustomService = bluetoothGatt.getService(UUID.fromString("f3641400-00b0-4240-ba50-05ca45bf8abc"));
        if(mCustomService == null){
            Log.i(TAG, "Custom BLE Service not found");
            return;
        }
        BluetoothGattCharacteristic characteristic = mCustomService.getCharacteristic(UUID.fromString("f3641400-00b0-4240-ba50-05ca45bf8abc"));
        characteristic.setValue(value);

        if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
        }
        bluetoothGatt.writeCharacteristic(characteristic);

        ledOnBtn.setVisibility(View.VISIBLE);
        ledOffBtn.setVisibility(View.INVISIBLE);
    }
}

Спасибо за внимание!

Файлы

🎁android.zip  10.64 Mb ⇣ 18
🎁nrf52832.zip  50.36 Kb ⇣ 16
 

Читательское голосование

Нравится

Статью одобрили 10 читателей.

Для участия в голосовании зарегистрируйтесь и войдите на сайт с вашими логином и паролем.
 

Поделись с друзьями!

 

 

Связанные материалы

 

Схема на Датагоре. Новая статья Обмен данными между Android-приложением и nRF52832. Часть 1. Serial... Здравствуйте! Представляю вашему вниманию проект обмена данными между Android-приложением и SoC...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 4. Система адресации памяти, назначение выводов, тактирование и прерывания МК... Привет датагорцам! Сегодня мы остановимся на следующих вопросах касательно рассматриваемых нами...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 5. Периферия МК.... Сегодня мы рассмотрим работу следующих модулей периферии: • порта ввода-вывода, • таймера •...
Схема на Датагоре. Новая статья Обмен данными между Java-приложением и МК. Часть 2. По воздуху, Bluetooth... Привет всем датагорчанам-любителям МК! Сегодня мы рассмотрим двусторонний обмен между...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 6. Протоколы обмена данными I2C и SPI... В проекте из предыдущей части нашей ассемблерной эпопеи мы подключали к микроконтроллеру светодиод...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 7. Компиляция, отладка, загрузка... Привет датагорцам и гостям нашего кибер-города! В предыдущих частях материала по Ассемблеру...
Схема на Датагоре. Новая статья Визуализация для микроконтроллера. Часть 4. Android... Вообще то я планировал рассказать сегодня про дисплей на базе ILI9481. Однако, он настолько похож...
Схема на Датагоре. Новая статья Программирование микроконтроллеров на языке C. Часть 2... Добрый день, уважаемые камрады-датагорцы! Сегодня, рассмотрев некоторые общие моменты, мы займёмся...
Схема на Датагоре. Новая статья Машинка с пропорциональным управлением по Bluetooth из-под Android... Не будем покупать плохие игрушки у Китайцев, а купим у них дешевый конструктор-шасси, несколько...
Схема на Датагоре. Новая статья Обмен данными между Java-приложением и МК. Часть 1. По проводу, USB-UART... Приветствую всех жителей и гостей кибер-города Датагор! Работа устройства на базе микроконтроллера...
Схема на Датагоре. Новая статья На русском: Bluetooth Low Energy (BLE). Bluetooth Core Specification 4.2. GATT... Приветствую всех жителей и гостей Датагор.ру! Давно планировал написать статью по Bluetooth Low...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 2. Шаблонные файлы и инструкции МК... В предыдущей части статьи мы провели подготовительную работу и вкратце разобрали принципы работы...
 

Комментарии, вопросы, ответы, дополнения, отзывы

 

Добавить комментарий, вопрос, отзыв 💬

Камрады, будьте дружелюбны, соблюдайте правила!

  • Смайлы и люди
    Животные и природа
    Еда и напитки
    Активность
    Путешествия и места
    Предметы
    Символы
    Флаги
 
 
В начало | Зарегистрироваться | Заказать наши киты почтой