Xiaomi датчик температуры и влажности не работает автоматизация

Содержание
Xiaomi mi LED Smart bulb работает с Алисой

Не так давно, удалось мне обзавестись известными датчиками температуры и влажности от Xiaomi. Эти датчики заслуженно приобрели широкую известность, так как при своей достаточно низкой цене, достаточно удобны в использовании, а также умеют передавать свои показания по протоколу BLE в тот же Mi Home. К тому же весь Интернет завален вариантами подключения этих сенсоров к Home Assistant, MajorDoMo и другим системам.

Но мне этого показалось мало и захотелось все сделать по-своему (не спрашивайте меня зачем и почему, просто захотелось). А именно, захотелось прочитать данные с датчиков, которые развешены по всему дому и как-нибудь интересно с ними поработать. Потому я покопался в своих электронных закромах и нашел там модуль ESP32.

Быстрое гугление показало: ESP32 — это то, что мне нужно. Он умеет Bluetooth и WiFi, программируется из Arduino IDE и позволит мне получить показания с датчика и отправить их по WiFi куда нужно (хоть на домашний сервер, хоть в облако). К тому же, очень быстро нашелся простой и понятный туториал, который как раз решал мою задачу. Но как выяснилось, не все так просто.

Xiaomi Mi Home — Сценарии управления увлажнителем воздуха.

Первые проблемы

Как часто бывает с примерами из Интернета, код не заработал. А ведь так хотелось… Очевидно, что нужно разбираться с этим дальше.

Не смотря на то, что у меня в закромах лежат всякие ESP32, по основному роду деятельности я прикладной разработчик. Ковыряюсь с железками (как и многие, я полагаю) только в качестве хобби. Потому достаточно быстро пришло понимание того, что без закапывания в детали дальше продвинуться не получится. Потому пришлось изучить код, немного спецификацию BLE и понять как это устроено. По результатам разбирательств пришло некоторое понимание того, как оно работает, ну и сразу же захотелось этим с кем-нибудь поделиться.

Как оно работает

Обычно устройства BLE умеют работать в 2-х режимах. Назовем их широковещательный (discover mode) и подключенный (connection mode). В широковещательном режиме устройство может рассылать пакеты, позволяющие другим Bluetooth устройствам обнаружить его и установить соединение при необходимости. При дальнейшем установлении соединения устройства могут обмениваться данными и командами.

Некоторые устройства упаковывают какие-то данные о себе прямо в широковещательные пакеты. Это некоторым образом упрощает взамодействие с устройством, а также в числе прочих средств позволяет экономить энергию.

Сенсор Xiaomi умеет работать в двух режимах, и в Интернетах можно найти примеры работы как с широковещательными пакетами так и в режиме соединения. В найденном ранее руководстве используется вариант подслушивания широковещательных пакетов. Достаточно просто чтобы можно было быстро разобраться. Осталось только выяснить, что же не так.

Так что все-таки сломалось?

Код примера работает достаточно просто. При старте устройства инициализируется процесс сканирования устройств и устанавливается класс, функции которого будут вызываться при получении пакетов от устройств (advertising пакеты).

ОТКЛЮЧАЕТСЯ ДАТЧИК ТЕМПЕРАТУРЫ И ВЛАЖНОСТИ AQARA / РЕШЕНИЕ / Умный дом XIAOMI


void initBluetooth() < BLEDevice::init(«»); pBLEScan = BLEDevice::getScan(); //create new scan pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster pBLEScan->setInterval(0x50); pBLEScan->setWindow(0x30); >

Пакеты от устройств обрабатываются в этой функции:

void onResult(BLEAdvertisedDevice advertisedDevice) < if (advertisedDevice.haveName() advertisedDevice.haveServiceData() !advertisedDevice.getName().compare(«MJ_HT_V1»)) < std::string strServiceData = advertisedDevice.getServiceData(); uint8_t cServiceData[100]; char charServiceData[100]; strServiceData.copy((char *)cServiceData, strServiceData.length(), 0); Serial.printf(«nnAdvertised Device: %sn», advertisedDevice.toString().c_str()); for (int i=0;istd::stringstream ss; ss ; switch (cServiceData[11]) < case 0x04: sprintf(charValue, «%02X%02X», cServiceData[15], cServiceData[14]); value = strtol(charValue, 0, 16); if(METRIC) < current_temperature = (float)value/10; >else < current_temperature = CelciusToFahrenheit((float)value/10); >displayTemperature(); break; case 0x06: sprintf(charValue, «%02X%02X», cServiceData[15], cServiceData[14]); value = strtol(charValue, 0, 16); current_humidity = (float)value/10; displayHumidity(); Serial.printf(«HUMIDITY_EVENT: %s, %dn», charValue, value); break; case 0x0A: sprintf(charValue, «%02X», cServiceData[14]); value = strtol(charValue, 0, 16); Serial.printf(«BATTERY_EVENT: %s, %dn», charValue, value); break; case 0x0D: sprintf(charValue, «%02X%02X», cServiceData[15], cServiceData[14]); value = strtol(charValue, 0, 16); if(METRIC) < current_temperature = (float)value/10; >else < current_temperature = CelciusToFahrenheit((float)value/10); >displayTemperature(); Serial.printf(«TEMPERATURE_EVENT: %s, %dn», charValue, value); sprintf(charValue, «%02X%02X», cServiceData[17], cServiceData[16]); value2 = strtol(charValue, 0, 16); current_humidity = (float)value2/10; displayHumidity(); Serial.printf(«HUMIDITY_EVENT: %s, %dn», charValue, value2); break; > > >

Очевидно, проблема где-то здесь.

Основное действие в этом коде происходит в конструкции switch, где проверяется значение 11го байта в service data массиве. Проблема только в том, что в моем случае массив данных был меньше 11 байт. Осталось выяснить почему.

Каждый advertising пакет помимо информации о возможности соединения с устройством может содержать пакет данных (payload). Этот пакет содержит расширенные данные об устройстве, также данные о сервисах, которые поддерживает устройство. В одном пакете может быть информация о нескольких сервисах. Типичный payload моих устройств выглядит так (это отдельные байты в шестнадцатиричной системе счисления):

020106121695fe5020aa01ab9f0231342d580a10014309094d4a5f48545f563105030f180a180916ffffc8b33f8a48db

Информация здесь кодируется достаточно просто. Первый байт (в примере 0x02) задает размер блока в байтах. За ним следует байт, который указыает назначение блока (подробно о типах блоков здесь). Затем следуют данные в зависимости от типа блока.

Ну и дальше все повторяется (опять появляется длина блока) пока не закончится пакет данных.

Xiaomi 13 ultra размеры

Нас больше всего интересют блоки с типом 0x16, которые отвеают за service data, т.е. за данные, описывающие отдельные функции устройства. В нашем примере таких блоков 2:

121695fe5020aa01ab9f0231342d580a100143 0916ffffc8b33f8a48db

Если присмотреться поближе, то можно заметить, что 11й байт в первом блоке очень похож, на тот, что ожидает наш switch (0x0A). А второй блок как раз похож на тот, слишком короткий блок, на который мы ссылались в начале. Похоже здесь и порылась собака. Похоже, что наш код ожидает видеть первый блок, а получает второй.

Почему так вышло?

Может у нас какие-то не такие устройства, а может у автора кода другие, но факт остается фактом, у нас оно так не работает. Самое время посмотреть в исходники библиотеки ESP32 для Arduino. Не будем вдаваться в подробности, но по этому коду видно, что getServiceData должен иметь параметр с индексом блока данных, который найден в пакете.

Т.е. в библиотеке предусмотрена возможность того, что payload может содержать несколько блоков service data. Однако, не все так просто. Оказывается, что эта ветка изменений на момент написания этой заметки еще не опубликована (текущая версия релиза 1.0.4). И просто так скачав в Arduino IDE все необходимое для ESP32 через Boards Manager будет получена более старая версия библиотеки.

И как раз в этой версии функция getServiceData() всегда возвращает последний блок service data. Это не очень приятно, но всегда можно использовать последнюю версию библиотеки. Главное, что мы смогли понять в чем была проблема.

Финальный код

С новой библиотекой решить проблему можно будет очень просто. Но не очень хочется создавать зависимость от новой версии библиотеки. Мы можем добавить простой код, который сделает то, что нужно нам нужно и так. Для этого нам нужен код, который в payload найдет нужный нам блок service data (в примере ниже функция findServiceData).

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks < uint8_t* findServiceData(uint8_t* data, size_t length, uint8_t* foundBlockLength) < // пэйлоад у состоит из блоков [байт длины][тип блока][данные] // нам нужен блок с типом 0x16, следом за которым будет 0x95 0xfe // поэтому считываем длину, проверяем следующий байт и если что пропускаем // вернуть надо указатель на нужный блок и длину блока uint8_t* rightBorder = data + length; while (data < rightBorder) < uint8_t blockLength = *data; if (blockLength < 5) < // нам точно такие блоки не нужны data += (blockLength+1); continue; >uint8_t blockType = *(data+1); uint16_t serviceType = *(uint16_t*)(data + 2); if (blockType == 0x16 serviceType == 0xfe95) < // мы нашли что искали *foundBlockLength = blockLength-3; // вычитаем длину типа сервиса return data+4; // пропускаем длину и тип сервиса >data += (blockLength+1); > return nullptr; > void onResult(BLEAdvertisedDevice advertisedDevice) < if (!advertisedDevice.haveName() || advertisedDevice.getName().compare(«MJ_HT_V1»)) return; // нас интересуют только устройства, которые транслируют нужное нам имя uint8_t* payload = advertisedDevice.getPayload(); size_t payloadLength = advertisedDevice.getPayloadLength(); Serial.printf(«nnAdvertised Device: %sn», advertisedDevice.toString().c_str()); printBuffer(payload, payloadLength); uint8_t serviceDataLength=0; uint8_t* serviceData = findServiceData(payload, payloadLength, if (serviceData == nullptr) < return; // нам этот пакет больше не интересен >Serial.printf(«Found service data len: %dn», serviceDataLength); printBuffer(serviceData, serviceDataLength); // 11й байт в пакете означает тип события // 0x0D — температура и влажность // 0x0A — батарейка // 0x06 — влажность // 0x04 — температура switch (serviceData[11]) < case 0x0D: < float temp = *(uint16_t*)(serviceData + 11 + 3) / 10.0; float humidity = *(uint16_t*)(serviceData + 11 + 5) / 10.0; Serial.printf(«Temp: %f Humidity: %fn», temp, humidity); >break; case 0x04: < float temp = *(uint16_t*)(serviceData + 11 + 3) / 10.0; Serial.printf(«Temp: %fn», temp); >break; case 0x06: < float humidity = *(uint16_t*)(serviceData + 11 + 3) / 10.0; Serial.printf(«Humidity: %fn», humidity); >break; case 0x0A: < int battery = *(serviceData + 11 + 3); Serial.printf(«Battery: %dn», battery); >break; default: break; > > >;

Вывод

Вся проделанная работа в очередной раз показыает, что не всегда код из Интернета хорошо работает. Будь-то пример для ESP32 или кусок кода со StackOverflow, крайне желательно все же понимать как оно работает. Всегда могут появиться не самые стандартные случаи, которые заставят код развалиться. Хорошо, когда это происходит в хобби-проектах, но, очевидно, никому не хотелось бы наталкиваться на подобные случаи в боевом коде. Давайте будем осторожны с использованием чужого кода, ну или по крайней мере попытаемся в нем разбираться.

Как-то длинновато получилось, но надеюсь, что кому-то это будет полезно. Со своей же стороны, надеюсь, что этому эксперименту будет продолжение, и данные температуры все же будут отправлены дальше.

Полный код примера можно скачать здесь.

UPD: Продолжение экспериментов тут

Источник: habr.com

Термометр и гигрометр Xiaomi MiJia Humidity умного дома» в условиях обычной квартиры в среде технарей всплывает довольно часто. То ли мы не наигрались в детстве в конструкторы, то ли это фатальное желание раскормить свою лень до фатальных размеров, но умный дом у нас хотят все.

Шуточки по поводу того, что все умные, одни люди не очень, при таком раскладе очень популярны.

Но многое меня раньше останавливало, и цены, и необходимость осваивать прогерство под Ардуинки (на что при моем режиме работы до декрета катастрофически не было времени, надо было хоть как-то разгребаться с работой), и даже просто моменты из серии «вот, надо было раскидывать провода до ремонта, а я еще не знала чего точно хочу».
Но на помощь чайнику всегда придет прогресс. И прогрессивным помощником в нашем случае стала компания XIaoMi со своим концептом простого умного дома, доступного каждому.

И вот мечта начала исполняться, в нашем доме появился замечательный сосед. Всеведующий тиран и командир, и имя ему XiaoMi MiJia Humidity ожидание/реальность» дело не благодарное. Радует, что датчик хорошо закреплён и не болтается по коробочке. Не радует, что пока его достанешь из нее -вспомнишь матерей всех народов и ещё несколько крепких выражений (особенно когда коробочка в процессе порвётся и потеряет симпатичный вид для Айрека)

Комплектация:

Как всегда, минималистично подходит Сяоми к комплектации. В комплекте с датчиком у нас идет инструкция (оунли китайский), дополнительная липучка для крепления датчика и предустановленная батарейка. Все. Хотя, а что еще надо?

Сам датчик:

Сам датчик идеально шифруется своим дизайном под остальных представителей системы умного дома. Для определения размеров рядом болтаются 5 рублей (да, им чуть-чуть плохо). Матовый белый пластик довольно крепкий, в руках не скользит. Рисунок, хоть и выполнен серым цветом, довольно четкий и не расплывается

Smile

На обратной стороне у нас имеется первая наклейка для крепления, и прорезь, чтобы можно было заменить батарейку. Открывается она с помощью монетки, а такое состояние у меня имеет от варварских попыток вскрыть датчик мелкой отверткой. Как всегда, перед использованием надо попытаться сломать

Под крышкой у нас батарейка типа CR2032. в комплекте идет предустановленная батарейка. Кстати, чтобы закрыть надо расположить крышку строго определенным образом, пазы расположены не симметрично.

На передней поверхности кроме изображения термометра еще есть мелкое отверстие, через которое моргает синий светодиод. А на грани есть кнопочка, которая поможет нам подключить этот датчик к общей системе.

Данный сенсор работает только по протоколу ZigBee. ТОЛЬКО ПО ПРОТОКОЛУ ZIGBEE. Не по Блютузу, не по ВайФай, не по Ап и Фм частотам. Только ZigBee. А это значит, что без шлюза этот датчик — обыкновенный кусок безмолвной пластмассы, которые может разве что неспешно жевать батарейку и иногда помаргивать синей лампочкой.

И все. Вся магия умного беспроводного дома завязана на вот это шлюз.

Smile

Почему я так жестко пишу про крайнюю необходимость шлюза? да потому что муж (который вообще тоже физик) сперва решил заказать один датчик. Ну правильно, зачем читать инструкцию, пока оно не сломалось? А потом сидел и думал, что же пошло не так и почему не работает ничего.

Кстати, протокол ZigBee — низкопотребляемый, поэтому для долгой работы датчиков нам будет хватать обычных батареек, а особенность технологии соединения позволит раскинуть сеть с помошью ретрансляторов хоть на маленькую квартиру, хоть на огромный офис. Но пока у нас сеть не большая, так что хватит одного шлюза. Но без него никак. Sad but true.

Подключение датчика:

Чтобы подключить датчик надо скачать приложение MiHome, ссылка на него дается с помошью QR-кода. Но есть одно не самое приятное но: приложение имеет русификацию, но только что называется «первого слоя». Если копнуть глубже (т.е. залезть в самые интересные разделы автоматизации) то мы увидим сплошные иероглифы.

Можно найти в интернете английскую версию программы,но насколько я поняла она менее стабильная по сравнению с китайской. Так что нам только и остается что вооружиться словариком и спокойненько прорваться через дебри накрученные китайскими программистами.

Для начала надо подключить шлюз к домашнему вайфаю, и затем через шлюз уже подключать термометр.
Для модерации: Скрины сделаны мной лично и без них информация о датчике из системы умного дома будет неполной.

Через меню шлюза мы находим необходимый для получения датчик (благо есть не только надписи, но и картиночки, они как всегда безмерно облегчают жизнь) и тыкаем на него. Нам вылезает инструкция с необходимыми дальнейшими манипуляциями и временем на их выполнение.

В частности надо нажать на единственную кнопку у гигрометра и подержать необходимое время.

Дальше будет возможность переименовать сенсор, но внезапно оказалось, что проще это сделать позднее, потому что довольно тяжело заставить после переименования свернуться клавиатуру и таки ткнуть заветную надпись принять.

И все! На самом деле не так страшны китайские иероглифы — первый датчик подключаешь с трясущимися лапками и мыслями «ААААААА, почему я ничего не понимаю, где тут что, кому нужно продать душу чтобы понимать эту клинопись?! Почему я так плохо училась в школе и ничего из китайского не помню?!», а все последующие уже без всякой истерики, практически на автомате.

После подключения можно прилепить датчик куда захочется — и начинать его нещадно эксплуатировать.

Показания датчика:

В данный момент у меня есть подключение 2 датчиков, по одному на каждую комнату моего скромного жилища.Для удобства я вывела их на основной экран приложения, но можно просматривать показания и отдельно по комнатам.

Smile

Я даже не знаю, что удобнее, видимо таки один экран, если количество приборов малое. Но мы же только начинаем-с

Даже на общем экране виден % влажности и температуры. Но если открыть меню датчика, то мы увидим вот это:

Сразу дается комментарий, какой в данный момент микроклимат вблизи датчика. В случае необходимости можно просмотреть графики показаний за последнее время. Можно вывести данные за один день, за неделю или месяц.

Кстати, неплохо прописан поворот приложения в ландшафтный формат. Мелочь, а приятно. Особенно если просматривать показания за большой промежуток времени.

Smile

Точность у датчика бытовая, верить можно 1 знаку после запятой,т.е. точность одна десятая, но вообще этого более чем достаточно. Если вам кто-то скажет, что есть датчики температуры с такой точностью это фигня — расскажите ему анекдот про «Штурман, к чему такая точность?» и успокойтесь

Сценарии использования датчика температуры и влажности:

В идеале умный дом это система, которая самостоятельно будет поддерживать идеальные характеристики климата в отдельно взятой квартире.

Датчик увидел, что температура поползла вниз? Через шлюз он подал команду на включение радиатора. Упала влажность? Так же автоматически включается увлажнитель воздуха. Остается только оплачивать счета за электроэнергию, подливать воды в увлажнитель и умиляться.

Но все не так гладко. Чтобы все было автоматизированно, надо чтобы был умным не только датчик, но и увлажнитель с нагревателем, и чтобы все оно было от Сяоми. А так как я не богатый бурасяно, и идея построить систему умного дама зародилась значительно позже, чем у меня появился мой далеко не самый умный увлажнитель (и его младший совершенно тупенький, зато в форме божьей коровки собрат) к общей системе подключаться не могут даже при всем желании. Так что автоматизация выглядит примерно следующим образом:

При падении влажности в комнате менее 40% мне на телефон приходит уведомление. Я поднимаю свое бренное тело и иду смотреть, почему так произошло (скорее всего в увлажните вода кончилась, а он, бедный, после выкусывания надоевшей пищалки оповестить об этом не может). Ругаюсь, и наполняю резервуар с водой, включаю увлажнитель.

Если влажность упала до 30% то вообще начинаю внеплановую большую стирку, ибо как только в квартире сухо и с коже беда, и с носом беда.

С температурой у нас чуть проще, умный плинтусный обогреватель от Redmond хоть и не подключен к общей сети XiaoMi, но включить его и откорректировать его работу в случае чего я могу просто зайдя в соседнее приложение на телефоне. И если я лежу дома на диване, то это не кажется таким уж важным, то если я сижу на работе, а погода стремительно портится. Возможность дистанционно включить обогреватель таки очень приятная штука.

В итоге: Да, можно повесить на стенку термометр за 40 рублей и наслаждаться жизнью. Но мы же не ищем легких путей? Сенсор от XiaMi прекрасен для своих целей, поэтому не рекомендовать его я просто не могу!

Smile

Продолжению быть, вдруг мой скромный опыт поможет кому-то достичь мечты построить умный дом

Источник: irecommend.ru

Опять про BLE, температуру и датчики Xiaomi

Не так давно, удалось мне обзавестись известными датчиками температуры и влажности от Xiaomi. Эти датчики заслуженно приобрели широкую известность, так как при своей достаточно низкой цене, достаточно удобны в использовании, а также умеют передавать свои показания по протоколу BLE в тот же Mi Home. К тому же весь Интернет завален вариантами подключения этих сенсоров к Home Assistant, MajorDoMo и другим системам.
Но мне этого показалось мало и захотелось все сделать по-своему (не спрашивайте меня зачем и почему, просто захотелось). А именно, захотелось прочитать данные с датчиков, которые развешены по всему дому и как-нибудь интересно с ними поработать. Потому я покопался в своих электронных закромах и нашел там модуль ESP32.

Быстрое гугление показало: ESP32 — это то, что мне нужно. Он умеет Bluetooth и WiFi, программируется из Arduino IDE и позволит мне получить показания с датчика и отправить их по WiFi куда нужно (хоть на домашний сервер, хоть в облако). К тому же, очень быстро нашелся простой и понятный туториал, который как раз решал мою задачу. Но как выяснилось, не все так просто.

Первые проблемы

Как часто бывает с примерами из Интернета, код не заработал. А ведь так хотелось… Очевидно, что нужно разбираться с этим дальше.

Не смотря на то, что у меня в закромах лежат всякие ESP32, по основному роду деятельности я прикладной разработчик. Ковыряюсь с железками (как и многие, я полагаю) только в качестве хобби. Потому достаточно быстро пришло понимание того, что без закапывания в детали дальше продвинуться не получится. Потому пришлось изучить код, немного спецификацию BLE и понять как это устроено. По результатам разбирательств пришло некоторое понимание того, как оно работает, ну и сразу же захотелось этим с кем-нибудь поделиться.

Как оно работает

Обычно устройства BLE умеют работать в 2-х режимах. Назовем их широковещательный (discover mode) и подключенный (connection mode). В широковещательном режиме устройство может рассылать пакеты, позволяющие другим Bluetooth устройствам обнаружить его и установить соединение при необходимости. При дальнейшем установлении соединения устройства могут обмениваться данными и командами.

Некоторые устройства упаковывают какие-то данные о себе прямо в широковещательные пакеты. Это некоторым образом упрощает взамодействие с устройством, а также в числе прочих средств позволяет экономить энергию.

Сенсор Xiaomi умеет работать в двух режимах, и в Интернетах можно найти примеры работы как с широковещательными пакетами так и в режиме соединения. В найденном ранее руководстве используется вариант подслушивания широковещательных пакетов. Достаточно просто чтобы можно было быстро разобраться. Осталось только выяснить, что же не так.

Так что все-таки сломалось?

Код примера работает достаточно просто. При старте устройства инициализируется процесс сканирования устройств и устанавливается класс, функции которого будут вызываться при получении пакетов от устройств (advertising пакеты).

void initBluetooth() < BLEDevice::init(«»); pBLEScan = BLEDevice::getScan(); //create new scan pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster pBLEScan->setInterval(0x50); pBLEScan->setWindow(0x30); >

Пакеты от устройств обрабатываются в этой функции:

void onResult(BLEAdvertisedDevice advertisedDevice) < if (advertisedDevice.haveName() advertisedDevice.haveServiceData() !advertisedDevice.getName().compare(«MJ_HT_V1»)) < std::string strServiceData = advertisedDevice.getServiceData(); uint8_t cServiceData[100]; char charServiceData[100]; strServiceData.copy((char *)cServiceData, strServiceData.length(), 0); Serial.printf(«nnAdvertised Device: %sn», advertisedDevice.toString().c_str()); for (int i=0;istd::stringstream ss; ss ; switch (cServiceData[11]) < case 0x04: sprintf(charValue, «%02X%02X», cServiceData[15], cServiceData[14]); value = strtol(charValue, 0, 16); if(METRIC) < current_temperature = (float)value/10; >else < current_temperature = CelciusToFahrenheit((float)value/10); >displayTemperature(); break; case 0x06: sprintf(charValue, «%02X%02X», cServiceData[15], cServiceData[14]); value = strtol(charValue, 0, 16); current_humidity = (float)value/10; displayHumidity(); Serial.printf(«HUMIDITY_EVENT: %s, %dn», charValue, value); break; case 0x0A: sprintf(charValue, «%02X», cServiceData[14]); value = strtol(charValue, 0, 16); Serial.printf(«BATTERY_EVENT: %s, %dn», charValue, value); break; case 0x0D: sprintf(charValue, «%02X%02X», cServiceData[15], cServiceData[14]); value = strtol(charValue, 0, 16); if(METRIC) < current_temperature = (float)value/10; >else < current_temperature = CelciusToFahrenheit((float)value/10); >displayTemperature(); Serial.printf(«TEMPERATURE_EVENT: %s, %dn», charValue, value); sprintf(charValue, «%02X%02X», cServiceData[17], cServiceData[16]); value2 = strtol(charValue, 0, 16); current_humidity = (float)value2/10; displayHumidity(); Serial.printf(«HUMIDITY_EVENT: %s, %dn», charValue, value2); break; > > >

Очевидно, проблема где-то здесь.

Основное действие в этом коде происходит в конструкции switch, где проверяется значение 11го байта в service data массиве. Проблема только в том, что в моем случае массив данных был меньше 11 байт. Осталось выяснить почему.

Каждый advertising пакет помимо информации о возможности соединения с устройством может содержать пакет данных (payload). Этот пакет содержит расширенные данные об устройстве, также данные о сервисах, которые поддерживает устройство. В одном пакете может быть информация о нескольких сервисах. Типичный payload моих устройств выглядит так (это отдельные байты в шестнадцатиричной системе счисления):

020106121695fe5020aa01ab9f0231342d580a10014309094d4a5f48545f563105030f180a180916ffffc8b33f8a48db

Информация здесь кодируется достаточно просто. Первый байт (в примере 0x02) задает размер блока в байтах. За ним следует байт, который указыает назначение блока (подробно о типах блоков здесь). Затем следуют данные в зависимости от типа блока.

Ну и дальше все повторяется (опять появляется длина блока) пока не закончится пакет данных.

Нас больше всего интересют блоки с типом 0x16, которые отвеают за service data, т.е. за данные, описывающие отдельные функции устройства. В нашем примере таких блоков 2:

121695fe5020aa01ab9f0231342d580a100143 0916ffffc8b33f8a48db

Если присмотреться поближе, то можно заметить, что 11й байт в первом блоке очень похож, на тот, что ожидает наш switch (0x0A). А второй блок как раз похож на тот, слишком короткий блок, на который мы ссылались в начале. Похоже здесь и порылась собака. Похоже, что наш код ожидает видеть первый блок, а получает второй.

Почему так вышло?

Может у нас какие-то не такие устройства, а может у автора кода другие, но факт остается фактом, у нас оно так не работает. Самое время посмотреть в исходники библиотеки ESP32 для Arduino. Не будем вдаваться в подробности, но по этому коду видно, что getServiceData должен иметь параметр с индексом блока данных, который найден в пакете.

Т.е. в библиотеке предусмотрена возможность того, что payload может содержать несколько блоков service data. Однако, не все так просто. Оказывается, что эта ветка изменений на момент написания этой заметки еще не опубликована (текущая версия релиза 1.0.4). И просто так скачав в Arduino IDE все необходимое для ESP32 через Boards Manager будет получена более старая версия библиотеки.

И как раз в этой версии функция getServiceData() всегда возвращает последний блок service data. Это не очень приятно, но всегда можно использовать последнюю версию библиотеки. Главное, что мы смогли понять в чем была проблема.

Финальный код

С новой библиотекой решить проблему можно будет очень просто. Но не очень хочется создавать зависимость от новой версии библиотеки. Мы можем добавить простой код, который сделает то, что нужно нам нужно и так. Для этого нам нужен код, который в payload найдет нужный нам блок service data (в примере ниже функция findServiceData).

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks < uint8_t* findServiceData(uint8_t* data, size_t length, uint8_t* foundBlockLength) < // пэйлоад у состоит из блоков [байт длины][тип блока][данные] // нам нужен блок с типом 0x16, следом за которым будет 0x95 0xfe // поэтому считываем длину, проверяем следующий байт и если что пропускаем // вернуть надо указатель на нужный блок и длину блока uint8_t* rightBorder = data + length; while (data < rightBorder) < uint8_t blockLength = *data; if (blockLength < 5) < // нам точно такие блоки не нужны data += (blockLength+1); continue; >uint8_t blockType = *(data+1); uint16_t serviceType = *(uint16_t*)(data + 2); if (blockType == 0x16 serviceType == 0xfe95) < // мы нашли что искали *foundBlockLength = blockLength-3; // вычитаем длину типа сервиса return data+4; // пропускаем длину и тип сервиса >data += (blockLength+1); > return nullptr; > void onResult(BLEAdvertisedDevice advertisedDevice) < if (!advertisedDevice.haveName() || advertisedDevice.getName().compare(«MJ_HT_V1»)) return; // нас интересуют только устройства, которые транслируют нужное нам имя uint8_t* payload = advertisedDevice.getPayload(); size_t payloadLength = advertisedDevice.getPayloadLength(); Serial.printf(«nnAdvertised Device: %sn», advertisedDevice.toString().c_str()); printBuffer(payload, payloadLength); uint8_t serviceDataLength=0; uint8_t* serviceData = findServiceData(payload, payloadLength, if (serviceData == nullptr) < return; // нам этот пакет больше не интересен >Serial.printf(«Found service data len: %dn», serviceDataLength); printBuffer(serviceData, serviceDataLength); // 11й байт в пакете означает тип события // 0x0D — температура и влажность // 0x0A — батарейка // 0x06 — влажность // 0x04 — температура switch (serviceData[11]) < case 0x0D: < float temp = *(uint16_t*)(serviceData + 11 + 3) / 10.0; float humidity = *(uint16_t*)(serviceData + 11 + 5) / 10.0; Serial.printf(«Temp: %f Humidity: %fn», temp, humidity); >break; case 0x04: < float temp = *(uint16_t*)(serviceData + 11 + 3) / 10.0; Serial.printf(«Temp: %fn», temp); >break; case 0x06: < float humidity = *(uint16_t*)(serviceData + 11 + 3) / 10.0; Serial.printf(«Humidity: %fn», humidity); >break; case 0x0A: < int battery = *(serviceData + 11 + 3); Serial.printf(«Battery: %dn», battery); >break; default: break; > > >;

Вывод

Вся проделанная работа в очередной раз показыает, что не всегда код из Интернета хорошо работает. Будь-то пример для ESP32 или кусок кода со StackOverflow, крайне желательно все же понимать как оно работает. Всегда могут появиться не самые стандартные случаи, которые заставят код развалиться. Хорошо, когда это происходит в хобби-проектах, но, очевидно, никому не хотелось бы наталкиваться на подобные случаи в боевом коде. Давайте будем осторожны с использованием чужого кода, ну или по крайней мере попытаемся в нем разбираться.

Как-то длинновато получилось, но надеюсь, что кому-то это будет полезно. Со своей же стороны, надеюсь, что этому эксперименту будет продолжение, и данные температуры все же будут отправлены дальше.

Полный код примера можно скачать здесь.

Источник: temofeev.ru

Рейтинг
( Пока оценок нет )
Загрузка ...
Китай Покупай