Привет, карады-датагорцы! Эта моя статья открывает серию, посвящённую обмену данными между ESP32 и смартфоном. Сегодня мы рассмотрим одностороннюю передачу данных на Android по протоколу GAP Bluetooth Low Energy.
Содержание статьи / Table Of Contents
↑ Видео-демонстрация проекта
↑ Приложение для ESP32
Программа написана на языке Си в VS Code с плагином Espressif IDF. За основу взят пример ble_ibeacon из папки esp-idf\examples\bluetooth\bluedroid\ble.Передаваемые в составе широковещательного пакета данные — значение температуры, измеряемое датчиком DS18B20, контроль за которым обеспечивают две представленные ниже библиотеки.
↑ OneWire.h
#ifndef ONEWIRE_H_
#define ONEWIRE_H_
#include <rom/ets_sys.h>
#include "driver/gpio.h"
#define OWI_BUS 23
#define OWI_BUS_HIGH gpio_set_direction(OWI_BUS, GPIO_MODE_INPUT)
#define OWI_BUS_LOW gpio_set_direction(OWI_BUS, GPIO_MODE_OUTPUT); gpio_set_level(OWI_BUS, 0)
#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
void owiInit();
void owiReset();
void owiWriteBit_0();
void owiWriteBit_1();
unsigned char owiReadBit();
void owiWriteByte(unsigned char data);
unsigned char owiReadByte();
#endif
↑ OneWire.c
#include "oneWire.h"
void owiInit()
{
OWI_BUS_HIGH;
ets_delay_us(DELAY_H);
}
void owiReset()
{
OWI_BUS_LOW;
ets_delay_us(DELAY_H);
OWI_BUS_HIGH;
ets_delay_us(DELAY_I);
ets_delay_us(DELAY_J);
}
void owiWriteBit_0()
{
OWI_BUS_LOW;
ets_delay_us(DELAY_C);
OWI_BUS_HIGH;
ets_delay_us(DELAY_D);
}
void owiWriteBit_1()
{
OWI_BUS_LOW;
ets_delay_us(DELAY_A);
OWI_BUS_HIGH;
ets_delay_us(DELAY_B);
}
unsigned char owiReadBit()
{
unsigned char data = 0;
OWI_BUS_LOW;
ets_delay_us(DELAY_A);
OWI_BUS_HIGH;
ets_delay_us(DELAY_E);
int owiBusLevel = gpio_get_level(OWI_BUS);
if(owiBusLevel)
data = 1;
ets_delay_us(DELAY_F);
return data;
}
void owiWriteByte(unsigned char data)
{
for (unsigned char bitNum = 0; bitNum < 8; bitNum++)
{
if (data & LSBit)
{
owiWriteBit_1();
}
else
{
owiWriteBit_0();
}
data >>= 1;
}
}
unsigned char owiReadByte()
{
unsigned char data = 0;
for (unsigned char bitNum = 0; bitNum < 8; bitNum++)
{
data >>= 1;
if (owiReadBit())
{
data |= MSBit;
}
}
return data;
}
↑ DS18B20.h
#ifndef DS18B20_H_
#define DS18B20_H_
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "oneWire.h"
#define SKIP_ROM 0xCC
#define READ_SCRATCH_PAD 0xBE
#define CONVERT_T 0x44
#define MINUS 1
#define PLUS 0
#define SIGN_BITS 0xf8
#define FREE_STATE 0
#define CONVERSION_STATE 1
void ds18b20_Init();
void ds18b20_StartConversion();
float ds18b20_ReadTemperature();
extern unsigned char dataBuff[2], temperatureSign, ds18b20_timeCounter, ds18b20_state;
#endif
↑ DS18B20.c
#include "ds18b20.h"
unsigned char dataBuff[2], temperatureSign, ds18b20_timeCounter, ds18b20_state = FREE_STATE;
void ds18b20_Init()
{
owiInit();
}
void ds18b20_StartConversion()
{
ds18b20_state = CONVERSION_STATE;
owiReset();
owiWriteByte(SKIP_ROM);
owiWriteByte(CONVERT_T);
}
float ds18b20_ReadTemperature()
{
owiReset();
owiWriteByte(SKIP_ROM);
owiWriteByte(READ_SCRATCH_PAD);
for(int i = 0; i < 2; i++)
{
dataBuff[i] = owiReadByte();
}
owiReset();
if(dataBuff[1] & SIGN_BITS)
{
temperatureSign = MINUS;
}
else
{
temperatureSign = PLUS;
}
float temperature = (dataBuff[1] << 8 | dataBuff[0]) / 16.0;
return temperature;
}
Библиотека esp_ibeacon_api ответственна за инициализацию составляющих широковещательного пакета: UUID, тип и длину данных, ID компании, RSSI и пр.
↑ esp_ibeacon_api.h
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#define IBEACON_SENDER 0
#define IBEACON_RECEIVER 1
#define IBEACON_MODE CONFIG_IBEACON_MODE
#define ENDIAN_CHANGE_U16(x) ((((x)&0xFF00)>>8) + (((x)&0xFF)<<8))
#define ESP_UUID {0xFD, 0xA5, 0x06, 0x93, 0xA4, 0xE2, 0x4F, 0xB1, 0xAF, 0xCF, 0xC6, 0xEB, 0x07, 0x64, 0x78, 0x25}
#define ESP_MAJOR 10167
#define ESP_MINOR 61958
typedef struct
{
uint8_t flags[3];
uint8_t length;
uint8_t type;
uint16_t company_id;
uint16_t beacon_type;
}__attribute__((packed)) esp_ble_ibeacon_head_t;
typedef struct
{
uint8_t proximity_uuid[16];
uint16_t major;
uint16_t minor;
int8_t measured_power;
}__attribute__((packed)) esp_ble_ibeacon_vendor_t;
typedef struct
{
esp_ble_ibeacon_head_t ibeacon_head;
esp_ble_ibeacon_vendor_t ibeacon_vendor;
}__attribute__((packed)) esp_ble_ibeacon_t;
extern esp_ble_ibeacon_head_t ibeacon_common_head;
bool esp_ble_is_ibeacon_packet (uint8_t *adv_data, uint8_t adv_data_len);
esp_err_t esp_ble_config_ibeacon_data (esp_ble_ibeacon_vendor_t *vendor_config, esp_ble_ibeacon_t *ibeacon_adv_data);
↑ esp_ibeacon_api.c
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "esp_gap_ble_api.h"
#include "esp_ibeacon_api.h"
const uint8_t uuid_zeros[ESP_UUID_LEN_128] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
esp_ble_ibeacon_head_t ibeacon_common_head =
{
.flags = {0x02, 0x01, 0x06},
.length = 0x1A,
.type = 0xFF,
.company_id = 0x004C,
.beacon_type = 0x1502
};
esp_ble_ibeacon_vendor_t vendor_config =
{
.proximity_uuid = ESP_UUID,
.major = ENDIAN_CHANGE_U16(ESP_MAJOR), //Major=ESP_MAJOR
.minor = ENDIAN_CHANGE_U16(ESP_MINOR), //Minor=ESP_MINOR
.measured_power = 0xC5
};
bool esp_ble_is_ibeacon_packet (uint8_t *adv_data, uint8_t adv_data_len)
{
bool result = false;
if ((adv_data != NULL) && (adv_data_len == 0x1E))
{
if (!memcmp(adv_data, (uint8_t*)&ibeacon_common_head, sizeof(ibeacon_common_head)))
{
result = true;
}
}
return result;
}
esp_err_t esp_ble_config_ibeacon_data (esp_ble_ibeacon_vendor_t *vendor_config, esp_ble_ibeacon_t *ibeacon_adv_data)
{
if ((vendor_config == NULL) || (ibeacon_adv_data == NULL) || (!memcmp(vendor_config->proximity_uuid, uuid_zeros, sizeof(uuid_zeros))))
{
return ESP_ERR_INVALID_ARG;
}
memcpy(&ibeacon_adv_data->ibeacon_head, &ibeacon_common_head, sizeof(esp_ble_ibeacon_head_t));
memcpy(&ibeacon_adv_data->ibeacon_vendor, vendor_config, sizeof(esp_ble_ibeacon_vendor_t));
return ESP_OK;
}
Основная функция main():
1. Инициализирует и включает стек проколов Bluedroid.
2. Инициализирует и включает контроллер BLE.
3. Инициализирует датчик DS18B20.
4. В цикле c периодичностью 800 мс считывает текущее значение температуры и включает его в состав широковещательного пакета.
↑ main.c
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_gatt_defs.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#include "esp_ibeacon_api.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "DS18B20.h"
unsigned int tensCurrent = 0x00, unitsCurrent = 0x00, fractionCurrent = 0x00;
static const char* TAG = "Temperature";
extern esp_ble_ibeacon_vendor_t vendor_config;
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
static esp_ble_adv_params_t ble_adv_params =
{
.adv_int_min = 0x20,
.adv_int_max = 0x40,
.adv_type = ADV_TYPE_NONCONN_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
esp_err_t err;
switch (event)
{
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
esp_ble_gap_start_advertising(&ble_adv_params);
break;
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
if ((err = param->scan_start_cmpl.status) != ESP_BT_STATUS_SUCCESS)
{
ESP_LOGE(TAG, "Scan start failed: %s", esp_err_to_name(err));
}
break;
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
if ((err = param->adv_start_cmpl.status) != ESP_BT_STATUS_SUCCESS)
{
ESP_LOGE(TAG, "Adv start failed: %s", esp_err_to_name(err));
}
break;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
if ((err = param->adv_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS)
{
ESP_LOGE(TAG, "Adv stop failed: %s", esp_err_to_name(err));
}
break;
default:
break;
}
}
void ble_ibeacon_appRegister(void)
{
esp_err_t status;
ESP_LOGI(TAG, "register callback");
if ((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK)
{
ESP_LOGE(TAG, "gap register error: %s", esp_err_to_name(status));
return;
}
}
void ble_ibeacon_init(void)
{
esp_bluedroid_init();
esp_bluedroid_enable();
ble_ibeacon_appRegister();
}
void app_main(void)
{
ds18b20_Init();
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
esp_bt_controller_init(&bt_cfg);
esp_bt_controller_enable(ESP_BT_MODE_BLE);
ble_ibeacon_init();
esp_ble_ibeacon_t ibeacon_adv_data;
esp_err_t status = esp_ble_config_ibeacon_data (&vendor_config, &ibeacon_adv_data);
if (status == ESP_OK)
{
esp_ble_gap_config_adv_data_raw((uint8_t*)&ibeacon_adv_data, sizeof(ibeacon_adv_data));
}
else
{
ESP_LOGE(TAG, "Config iBeacon data failed: %s\n", esp_err_to_name(status));
}
while(1)
{
if(ds18b20_state == FREE_STATE)
{
ds18b20_StartConversion();
}
else if(ds18b20_state == CONVERSION_STATE)
{
ds18b20_state = FREE_STATE;
float currentTemperature = ds18b20_ReadTemperature();
tensCurrent = (unsigned int)(currentTemperature / 10);
unitsCurrent = (unsigned int)currentTemperature - tensCurrent * 10;
fractionCurrent = (unsigned int)(currentTemperature * 10.0) - tensCurrent * 100 - unitsCurrent * 10;
ESP_LOGE(TAG, "%3.1f", currentTemperature);
esp_ble_gap_stop_advertising();
vendor_config.proximity_uuid[13] = (unsigned char)tensCurrent;
vendor_config.proximity_uuid[14] = (unsigned char)unitsCurrent;
vendor_config.proximity_uuid[15] = (unsigned char)fractionCurrent;
esp_ble_config_ibeacon_data (&vendor_config, &ibeacon_adv_data);
esp_ble_gap_config_adv_data_raw((uint8_t*)&ibeacon_adv_data, sizeof(ibeacon_adv_data));
esp_ble_gap_start_advertising(&ble_adv_params);
}
vTaskDelay(800 / portTICK_PERIOD_MS);
}
}
↑ Android-приложение
написано на языке Java в Android Stidio. В манифесте прописаны разрешения на работу с 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.Gap"
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-устройств,
• текстового поля значения температуры.
↑ 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/scanButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="124dp"
android:onClick="scan"
android:text="Scan"
android:textSize="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/temperatureTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="+00.0℃"
android:textSize="60dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/scanButton"
app:layout_constraintVertical_bias="0.208" />
</androidx.constraintlayout.widget.ConstraintLayout>
Java-код:
а) По нажатию кнопки сканирует BLE-устройства.
б) Отражает в текстовом поле текущее значение температуры.
↑ MainActivity.java
package com.punchthrough.gap;
import android.Manifest;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
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.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
String[] peripheralAddresses = new String[]{"3C:71:BF:9D:70:96"};
List<ScanFilter> filters = null;
BluetoothDevice device;
Button scanButton;
TextView temperatureTextView;
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.scanButton);
temperatureTextView = findViewById(R.id.temperatureTextView);
if (peripheralAddresses != null) {
filters = new ArrayList<>();
for (String address : peripheralAddresses) {
ScanFilter filter = new ScanFilter.Builder()
.setDeviceAddress(address)
.build();
filters.add(filter);
}
}
}
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();
}
}
private final ScanCallback scanCallback = new ScanCallback() {
@SuppressLint("SetTextI18n")
@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);
}
byte[] data = result.getScanRecord().getManufacturerSpecificData(76);
temperatureTextView.setText("+" + data[15] + data[16] + "." + data[17] + "\u2103");
}
};
}
↑ Файлы
🎁Все файлы проекта, исходники: ayan-esp32.7z 45.64 Mb ⇣ 20Спасибо за внимание!
Продолжение следует.