|
|
|
@@ -1,123 +1,150 @@
|
|
|
|
|
#include "radon_eye_rd200.h"
|
|
|
|
|
#include "esphome/components/esp32_ble/ble_uuid.h"
|
|
|
|
|
|
|
|
|
|
#ifdef USE_BLE_CLIENT_GATT_NODES
|
|
|
|
|
|
|
|
|
|
#include "esphome/core/helpers.h"
|
|
|
|
|
#include "esphome/core/log.h"
|
|
|
|
|
|
|
|
|
|
#include <cstring>
|
|
|
|
|
|
|
|
|
|
#ifdef USE_ESP32
|
|
|
|
|
|
|
|
|
|
namespace esphome::radon_eye_rd200 {
|
|
|
|
|
|
|
|
|
|
static const char *const TAG = "radon_eye_rd200";
|
|
|
|
|
|
|
|
|
|
static const esp32_ble_tracker::ESPBTUUID SERVICE_UUID_V1 =
|
|
|
|
|
esp32_ble_tracker::ESPBTUUID::from_raw("00001523-1212-efde-1523-785feabcd123");
|
|
|
|
|
static const esp32_ble_tracker::ESPBTUUID WRITE_CHARACTERISTIC_UUID_V1 =
|
|
|
|
|
esp32_ble_tracker::ESPBTUUID::from_raw("00001524-1212-efde-1523-785feabcd123");
|
|
|
|
|
static const esp32_ble_tracker::ESPBTUUID READ_CHARACTERISTIC_UUID_V1 =
|
|
|
|
|
esp32_ble_tracker::ESPBTUUID::from_raw("00001525-1212-efde-1523-785feabcd123");
|
|
|
|
|
using ble_device_base::ESPBTUUID;
|
|
|
|
|
|
|
|
|
|
// V1 (RD200 firmware < 2.0) exposes a vendor service; V2 (>= 2.0) moved to
|
|
|
|
|
// Bluetooth-base (16-bit) UUIDs with a different command byte and payload
|
|
|
|
|
// layout.
|
|
|
|
|
static const char *const SERVICE_UUID_V1 = "00001523-1212-efde-1523-785feabcd123";
|
|
|
|
|
static const char *const WRITE_CHARACTERISTIC_UUID_V1 = "00001524-1212-efde-1523-785feabcd123";
|
|
|
|
|
static const char *const READ_CHARACTERISTIC_UUID_V1 = "00001525-1212-efde-1523-785feabcd123";
|
|
|
|
|
static const uint8_t WRITE_COMMAND_V1 = 0x50;
|
|
|
|
|
|
|
|
|
|
static const esp32_ble_tracker::ESPBTUUID SERVICE_UUID_V2 =
|
|
|
|
|
esp32_ble_tracker::ESPBTUUID::from_raw("00001523-0000-1000-8000-00805f9b34fb");
|
|
|
|
|
static const esp32_ble_tracker::ESPBTUUID WRITE_CHARACTERISTIC_UUID_V2 =
|
|
|
|
|
esp32_ble_tracker::ESPBTUUID::from_raw("00001524-0000-1000-8000-00805f9b34fb");
|
|
|
|
|
static const esp32_ble_tracker::ESPBTUUID READ_CHARACTERISTIC_UUID_V2 =
|
|
|
|
|
esp32_ble_tracker::ESPBTUUID::from_raw("00001525-0000-1000-8000-00805f9b34fb");
|
|
|
|
|
static const uint16_t SERVICE_UUID_V2 = 0x1523;
|
|
|
|
|
static const uint16_t WRITE_CHARACTERISTIC_UUID_V2 = 0x1524;
|
|
|
|
|
static const uint16_t READ_CHARACTERISTIC_UUID_V2 = 0x1525;
|
|
|
|
|
static const uint8_t WRITE_COMMAND_V2 = 0x40;
|
|
|
|
|
|
|
|
|
|
void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
|
|
|
|
esp_ble_gattc_cb_param_t *param) {
|
|
|
|
|
switch (event) {
|
|
|
|
|
case ESP_GATTC_OPEN_EVT: {
|
|
|
|
|
if (param->open.status == ESP_GATT_OK) {
|
|
|
|
|
ESP_LOGI(TAG, "Connected successfully!");
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// Minimum notification payload carrying all three measurements.
|
|
|
|
|
static const uint16_t MESSAGE_MIN_LEN_V1 = 20;
|
|
|
|
|
static const uint16_t MESSAGE_MIN_LEN_V2 = 68;
|
|
|
|
|
|
|
|
|
|
case ESP_GATTC_DISCONNECT_EVT: {
|
|
|
|
|
ESP_LOGW(TAG, "Disconnected!");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
RadonEyeRD200::RadonEyeRD200() : PollingComponent(10000) {}
|
|
|
|
|
|
|
|
|
|
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
|
|
|
|
if (this->parent()->get_service(SERVICE_UUID_V1) != nullptr) {
|
|
|
|
|
service_uuid_ = SERVICE_UUID_V1;
|
|
|
|
|
sensors_write_characteristic_uuid_ = WRITE_CHARACTERISTIC_UUID_V1;
|
|
|
|
|
sensors_read_characteristic_uuid_ = READ_CHARACTERISTIC_UUID_V1;
|
|
|
|
|
write_command_ = WRITE_COMMAND_V1;
|
|
|
|
|
} else if (this->parent()->get_service(SERVICE_UUID_V2) != nullptr) {
|
|
|
|
|
service_uuid_ = SERVICE_UUID_V2;
|
|
|
|
|
sensors_write_characteristic_uuid_ = WRITE_CHARACTERISTIC_UUID_V2;
|
|
|
|
|
sensors_read_characteristic_uuid_ = READ_CHARACTERISTIC_UUID_V2;
|
|
|
|
|
write_command_ = WRITE_COMMAND_V2;
|
|
|
|
|
} else {
|
|
|
|
|
ESP_LOGW(TAG, "No supported device has been found, disconnecting");
|
|
|
|
|
parent()->set_enabled(false);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this->read_handle_ = 0;
|
|
|
|
|
auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_read_characteristic_uuid_);
|
|
|
|
|
if (chr == nullptr) {
|
|
|
|
|
char service_buf[esp32_ble::UUID_STR_LEN];
|
|
|
|
|
char char_buf[esp32_ble::UUID_STR_LEN];
|
|
|
|
|
ESP_LOGW(TAG, "No sensor read characteristic found at service %s char %s", service_uuid_.to_str(service_buf),
|
|
|
|
|
sensors_read_characteristic_uuid_.to_str(char_buf));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
this->read_handle_ = chr->handle;
|
|
|
|
|
|
|
|
|
|
auto *write_chr = this->parent()->get_characteristic(service_uuid_, sensors_write_characteristic_uuid_);
|
|
|
|
|
if (write_chr == nullptr) {
|
|
|
|
|
char service_buf[esp32_ble::UUID_STR_LEN];
|
|
|
|
|
char char_buf[esp32_ble::UUID_STR_LEN];
|
|
|
|
|
ESP_LOGW(TAG, "No sensor write characteristic found at service %s char %s", service_uuid_.to_str(service_buf),
|
|
|
|
|
sensors_write_characteristic_uuid_.to_str(char_buf));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
this->write_handle_ = write_chr->handle;
|
|
|
|
|
|
|
|
|
|
esp_err_t status =
|
|
|
|
|
esp_ble_gattc_register_for_notify(gattc_if, this->parent()->get_remote_bda(), this->read_handle_);
|
|
|
|
|
if (status) {
|
|
|
|
|
ESP_LOGW(TAG, "Error registering for sensor notify, status=%d", status);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case ESP_GATTC_WRITE_DESCR_EVT: {
|
|
|
|
|
if (param->write.status != ESP_GATT_OK) {
|
|
|
|
|
ESP_LOGE(TAG, "write descr failed, error status = %x", param->write.status);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
ESP_LOGV(TAG, "Write descr success, writing 0x%02X at write_handle=%d", this->write_command_,
|
|
|
|
|
this->write_handle_);
|
|
|
|
|
esp_err_t status =
|
|
|
|
|
esp_ble_gattc_write_char(gattc_if, this->parent()->get_conn_id(), this->write_handle_, sizeof(write_command_),
|
|
|
|
|
(uint8_t *) &write_command_, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
|
|
|
|
if (status) {
|
|
|
|
|
ESP_LOGW(TAG, "Error writing 0x%02x command, status=%d", write_command_, status);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case ESP_GATTC_NOTIFY_EVT: {
|
|
|
|
|
if (param->notify.is_notify) {
|
|
|
|
|
ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT, receive notify value, %d bytes", param->notify.value_len);
|
|
|
|
|
} else {
|
|
|
|
|
ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT, receive indicate value, %d bytes", param->notify.value_len);
|
|
|
|
|
}
|
|
|
|
|
read_sensors_(param->notify.value, param->notify.value_len);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
void RadonEyeRD200::update() {
|
|
|
|
|
if (this->parent()->connected())
|
|
|
|
|
return;
|
|
|
|
|
if (!this->parent()->enabled) {
|
|
|
|
|
ESP_LOGW(TAG, "Reconnecting to device");
|
|
|
|
|
this->parent()->set_enabled(true);
|
|
|
|
|
} else {
|
|
|
|
|
ESP_LOGW(TAG, "Connection in progress");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RadonEyeRD200::read_sensors_(uint8_t *value, uint16_t value_len) {
|
|
|
|
|
void RadonEyeRD200::on_connected(const ble_device_base::GattServiceTable &table) {
|
|
|
|
|
if (!this->resolve_handles_(table)) {
|
|
|
|
|
// Retried on the next poll (update() re-enables the client).
|
|
|
|
|
this->parent()->set_enabled(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Local notification registration; the CCCD write follows in
|
|
|
|
|
// on_notify_state (the contract leaves the CCCD to the node).
|
|
|
|
|
if (this->parent()->notify_characteristic(this->read_handle_, true) != 0) {
|
|
|
|
|
this->parent()->set_enabled(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool RadonEyeRD200::resolve_handles_(const ble_device_base::GattServiceTable &table) {
|
|
|
|
|
struct Variant {
|
|
|
|
|
ESPBTUUID service;
|
|
|
|
|
ESPBTUUID write_chr;
|
|
|
|
|
ESPBTUUID read_chr;
|
|
|
|
|
uint8_t command;
|
|
|
|
|
};
|
|
|
|
|
// Built on the stack per (cold) discovery so the UUID objects stay out of
|
|
|
|
|
// static RAM; the V1 strings live in flash.
|
|
|
|
|
const Variant variants[] = {
|
|
|
|
|
{ESPBTUUID::from_raw(SERVICE_UUID_V1), ESPBTUUID::from_raw(WRITE_CHARACTERISTIC_UUID_V1),
|
|
|
|
|
ESPBTUUID::from_raw(READ_CHARACTERISTIC_UUID_V1), WRITE_COMMAND_V1},
|
|
|
|
|
{ESPBTUUID::from_uint16(SERVICE_UUID_V2), ESPBTUUID::from_uint16(WRITE_CHARACTERISTIC_UUID_V2),
|
|
|
|
|
ESPBTUUID::from_uint16(READ_CHARACTERISTIC_UUID_V2), WRITE_COMMAND_V2},
|
|
|
|
|
};
|
|
|
|
|
for (const auto &variant : variants) {
|
|
|
|
|
const auto *service = ble_device_base::find_service(table, variant.service);
|
|
|
|
|
if (service == nullptr) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const auto *read_chr = ble_device_base::find_characteristic(table, *service, variant.read_chr);
|
|
|
|
|
const auto *write_chr = ble_device_base::find_characteristic(table, *service, variant.write_chr);
|
|
|
|
|
if (read_chr == nullptr || write_chr == nullptr) {
|
|
|
|
|
ESP_LOGW(TAG, "Service found but a sensor characteristic is missing");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
this->cccd_handle_ = ble_device_base::find_cccd(table, *read_chr);
|
|
|
|
|
if (this->cccd_handle_ == 0) {
|
|
|
|
|
ESP_LOGW(TAG, "Sensor read characteristic has no CCCD");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
this->read_handle_ = read_chr->value_handle;
|
|
|
|
|
this->write_handle_ = write_chr->value_handle;
|
|
|
|
|
this->write_command_ = variant.command;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
ESP_LOGW(TAG, "No supported device has been found, disconnecting");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RadonEyeRD200::on_notify_state(uint16_t handle, bool enabled, int error) {
|
|
|
|
|
if (handle != this->read_handle_) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (error != 0) {
|
|
|
|
|
ESP_LOGW(TAG, "Error registering for sensor notify, status=%d", error);
|
|
|
|
|
this->parent()->set_enabled(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!enabled) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
static const uint8_t ENABLE_NOTIFY[2] = {0x01, 0x00};
|
|
|
|
|
if (this->parent()->write_descriptor(this->cccd_handle_, ENABLE_NOTIFY, sizeof(ENABLE_NOTIFY)) != 0) {
|
|
|
|
|
this->parent()->set_enabled(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RadonEyeRD200::on_write_result(uint16_t handle, int error) {
|
|
|
|
|
// The command write (no response) also lands here; only the CCCD
|
|
|
|
|
// completion advances the sequence.
|
|
|
|
|
if (handle != this->cccd_handle_) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (error != 0) {
|
|
|
|
|
ESP_LOGE(TAG, "write descr failed, error status = %x", error);
|
|
|
|
|
this->parent()->set_enabled(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
ESP_LOGV(TAG, "Write descr success, writing 0x%02X at write_handle=%d", this->write_command_, this->write_handle_);
|
|
|
|
|
if (this->parent()->write_characteristic(this->write_handle_, &this->write_command_, sizeof(this->write_command_),
|
|
|
|
|
false) != 0) {
|
|
|
|
|
ESP_LOGW(TAG, "Error writing 0x%02x command", this->write_command_);
|
|
|
|
|
this->parent()->set_enabled(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RadonEyeRD200::on_notify(uint16_t handle, const uint8_t *data, uint16_t len) {
|
|
|
|
|
if (handle != this->read_handle_) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
ESP_LOGV(TAG, "Received notify value, %d bytes", len);
|
|
|
|
|
this->read_sensors_(data, len);
|
|
|
|
|
// This instance must not stay connected so other clients can connect to it
|
|
|
|
|
// (e.g. the mobile app).
|
|
|
|
|
this->parent()->set_enabled(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RadonEyeRD200::read_sensors_(const uint8_t *value, uint16_t value_len) {
|
|
|
|
|
if (value_len < 1) {
|
|
|
|
|
ESP_LOGW(TAG, "Unexpected empty message");
|
|
|
|
|
return;
|
|
|
|
@@ -125,7 +152,8 @@ void RadonEyeRD200::read_sensors_(uint8_t *value, uint16_t value_len) {
|
|
|
|
|
|
|
|
|
|
uint8_t command = value[0];
|
|
|
|
|
|
|
|
|
|
if ((command == WRITE_COMMAND_V1 && value_len < 20) || (command == WRITE_COMMAND_V2 && value_len < 68)) {
|
|
|
|
|
if ((command == WRITE_COMMAND_V1 && value_len < MESSAGE_MIN_LEN_V1) ||
|
|
|
|
|
(command == WRITE_COMMAND_V2 && value_len < MESSAGE_MIN_LEN_V2)) {
|
|
|
|
|
ESP_LOGW(TAG, "Unexpected command 0x%02X message length %d", command, value_len);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -134,8 +162,11 @@ void RadonEyeRD200::read_sensors_(uint8_t *value, uint16_t value_len) {
|
|
|
|
|
// 501085EBB9400000000000000000220025000000
|
|
|
|
|
// Example data V2:
|
|
|
|
|
// 4042323230313033525532303338330652443230304e56322e302e3200014a00060a00080000000300010079300000e01108001c00020000003822005c8f423fa4709d3f
|
|
|
|
|
ESP_LOGV(TAG, "radon sensors raw bytes");
|
|
|
|
|
ESP_LOG_BUFFER_HEX_LEVEL(TAG, value, value_len, ESP_LOG_VERBOSE);
|
|
|
|
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
|
|
|
|
// Sized for the longest supported message; format_hex_to truncates longer.
|
|
|
|
|
char hex_buf[format_hex_size(MESSAGE_MIN_LEN_V2)];
|
|
|
|
|
ESP_LOGV(TAG, "radon sensors raw bytes: %s", format_hex_to(hex_buf, value, value_len));
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// Convert from pCi/L to Bq/m³
|
|
|
|
|
constexpr float convert_to_bwpm3 = 37.0;
|
|
|
|
@@ -185,22 +216,6 @@ void RadonEyeRD200::read_sensors_(uint8_t *value, uint16_t value_len) {
|
|
|
|
|
" Measurements (pCi/L) now: %0.03f, day: %0.03f, month: %0.03f",
|
|
|
|
|
radon_now, radon_day, radon_month, radon_now / convert_to_bwpm3, radon_day / convert_to_bwpm3,
|
|
|
|
|
radon_month / convert_to_bwpm3);
|
|
|
|
|
|
|
|
|
|
// This instance must not stay connected
|
|
|
|
|
// so other clients can connect to it (e.g. the
|
|
|
|
|
// mobile app).
|
|
|
|
|
parent()->set_enabled(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RadonEyeRD200::update() {
|
|
|
|
|
if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
|
|
|
|
|
if (!parent()->enabled) {
|
|
|
|
|
ESP_LOGW(TAG, "Reconnecting to device");
|
|
|
|
|
parent()->set_enabled(true);
|
|
|
|
|
} else {
|
|
|
|
|
ESP_LOGW(TAG, "Connection in progress");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RadonEyeRD200::dump_config() {
|
|
|
|
@@ -208,8 +223,6 @@ void RadonEyeRD200::dump_config() {
|
|
|
|
|
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RadonEyeRD200::RadonEyeRD200() : PollingComponent(10000) {}
|
|
|
|
|
|
|
|
|
|
} // namespace esphome::radon_eye_rd200
|
|
|
|
|
|
|
|
|
|
#endif // USE_ESP32
|
|
|
|
|
#endif // USE_BLE_CLIENT_GATT_NODES
|
|
|
|
|