- PVSM.RU - https://www.pvsm.ru -

Тёмная сторона MH-Z19

Что, опять? [1] Сколько ж можно? [2]

Да, сегодня мы снова поговорим о всеми нами любимом датчике углекислого газа Winsen MH-Z19. Нет, повторов не будет*.

Тёмная сторона MH-Z19 - 1

* почти

Как вы могли заметить, на страницах настоящего ресурса регулярно появляются страшилки статьи [3] об опасностях высокой концентрации СО2 в помещении. И хотя утверждения, что концентрации выше 1000 ppm несут ад и погибель, слегка преувеличены (википедия [4] говорит, что хоть какие-то эффекты начинаются с 1%, то бишь 10 000 ppm, тогда как у датчика весь диапазон — 5000 ppm) — СО2 может служить индикатором присутствия других нехороших вещей в силу недостаточности вентиляции. Потому я тоже решил приобщиться к увлекательному миру СО2-метрии и разжился упомянутым датчиком.

Первым делом, конечно, я его подключил к ардуине. Скопипастил (с необходимыми изменениями) код из статьи [5], на экранчике проступили заветные цифры.

Но пока я копипастил, в душу закрался червь сомнения — а отчего это датчик выдаёт девять байт, а на СО2 из них приходится всего два байта? Может быть, он хочет сказать мне что-то ещё?

Поиск в интернете навёл меня на такую интересную страничку [6]. Автор экспериментирует с MH-Z19 и выявляет его реакцию на недокументированные (да и документированные) команды. Команды он перепробовал ещё не все, так что и на нашу долю останется, где порезвиться. Этим мы и займёмся.

Первым делом разберёмся с «классической» командой 0x86 (или по-простому 134), с помощью которой мы и получаем концентрацию СО2. Revspace сообщает:

A response to command 0x86 typically looks like this:
0xFF CM HH LL TT SS Uh Ul CS
where
CM is the command repeated back
HH/LL is the CO2 ppm value, high/low part
TT is the temperature in degrees Celcius, plus 40. For example, when temperature is 25 deg C, then TT = 0x41
SS is some kind of status byte, this byte always has only one bit set!
Uh/Ul is some unknown value, perhaps related to pressure? After booting the sensor, it starts out at 15000 exactly, then typically settles to about 10500.
Cs is the checksum

То есть в ответе датчика на эту команду содержится также температура T (сдвинутая на 40 градусов) и две величины неизвестного предназначения — однобайтная S и двухбайтная U. S принимает значения степеней двойки, а U при запуске датчика падает с 15 000 до чуть более чем 10 000.

Как понять, что значат числа S и U? Конечно, надо нарисовать график! В любой непонятной ситуации рисуй график.

Унылые технические подробности

А чтобы нарисовать график, хорошо бы загнать показания датчика в компьютер. Что я и сделал посредством Serial.println(). Каждые пять секунд ардуина опрашивает датчик и пишет его показания в USB-UART, остаётся их только прочитать на компьютере и сохранить в файл. Делаю я это (в линуксе) так:

rlwrap cat | cu -l /dev/ttyACM0 > sensor_$(date '+%Y%m%d_%H%M%S').log
Ногами не пинать

Да, я знаю, что можно сделать просто cat /dev/ttyACM0 > .., но отчего-то у меня это работает далеко не всегда, порою сия команда сразу молча завершается. Приведённая команда (rlwrap cat | cu -l /dev/ttyACM0) также нравится мне тем, что позволяет удобно общаться с микроконтроллером в интерактивном режиме (хоть в данном случае это и не нужно). Наверняка есть более лучшие средства для этой цели, но я, к сожалению, их не знаю.

В другом окне терминала можно смотреть этот файл в реальном времени:

tail -f sensor_датавремя.log

Получается вот такая простыня из чисел:

...
1188 62 64 42 38 10790 
1188 62 64 42 38 10790 
1190 62 64 42 38 10790 
1191 62 64 42 38 10790 
1192 62 64 42 38 10790 
1193 62 64 42 38 10790 
1195 62 64 42 38 10790 
...

В каждой строчке — CO2, T, S, U (U повторено дважды — как два байта и двухбайтное число, не спрашивайте зачем).

Теперь можно и графики построить. Делать я это буду при помощи ipython --pylab.

y = map(lambda a: map(int, a.split()), open("sensor1.log", "r").readlines()) # читаем файл как список строк, разбиваем строки по разделяющим пробелам, преобразуем в числа
yt = transpose(y)
x = arange(len(yt[0])) / 720. # массив для оси абсцисс, чтобы по ней было время в часах
figure()
plot(x, yt[0]) # CO2
figure(); plot(x, yt[1], "g"); # T
figure(); plot(x, yt[2], "r"); # S
figure(); plot(x, yt[-1], "m"); # U

Итак, график СО2:

Тёмная сторона MH-Z19 - 2

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

Температура:

Тёмная сторона MH-Z19 - 3

Хорошо заметен момент, когда я открыл окно. Подвирает на 1 — 2 градуса, что для недокументированной возможности неплохо. Впрочем, если вспомнить принцип NDIR [7]-датчика, можно понять, что, большой точности от встроенного в него градусника ждать не следует. Эти приборы измеряют поглощение света в дальнем инфракрасном диапазоне, а для генерации этого света используется старая добрая лампочка Ильича, и можно даже через окошки разглядеть, как она каждые пять секунд загорается (на КДПВ пойман именно такой момент). И эта лампочка расходует немало энергии и, как следствие, нагревает весь датчик, а насколько она его нагреет — зависит от сиюминутной конъюнктуры воздушных потоков.

Подходим к самому интересному. Величина S:

Тёмная сторона MH-Z19 - 4

Ничего не говорит? Мне тоже. С горя нарисуем СО2 и S на одном графике и немного увеличим:

Тёмная сторона MH-Z19 - 5

Ага! Вот теперь всё ясно! Когда всё хорошо, S равна 64, а когда показания датчика СО2 начинает колбасить, она опускается до 4. Таким образом, можно использовать S для того, чтобы узнавать, насколько хорошо чувствует себя датчик и насколько точны его показания.

Осталась, как говорят парашютисты, крайняя величина — U:

Тёмная сторона MH-Z19 - 6

К сожалению, фокус с наложением здесь мало что даёт. Видно только, что, как и обещал revspace, в начале она равна 15 000, а затем падает до чуть более 10 000 (но может немного падать и по прошествии длительного времени). И раз в сутки цикл повторяется.

Зато когда я подключил питание датчика не к 5, а к 3,3 вольтам ардуины, ситуация изменилась:

Тёмная сторона MH-Z19 - 7

3,3 вольта на ардуине берутся от микросхемы LP2985, каковая представляет собой линейный стабилизатор на 160 миллиампер. А лампочка, судя по статьям в интернете, приблизительно столько и кушает. При взгляде на датчик заметно, что лампочка при таком питании разгорается за большее время, чем при пяти вольтах. И величина U в полтора раза выше.

Вырисовывается следующая гипотеза — датчик автоматически определяет, сколько времени надо жечь лампочку, чтобы получить достаточно ИК-излучения, и если тока не хватает, жжёт лампочку дольше. И величина U как раз отражает время горения лампочки (или ещё какую-то связанную с этим величину, например, затраченную энергию).

Чтобы проверить это предположение, запитаем датчик от литий-ионного аккумулятора, который заведомо может выдать намного больший ток:

Тёмная сторона MH-Z19 - 8

И действительно, U порою опускается ниже 10 000! Впрочем, это очевидно не абсолютное доказательство, да и после 24 часов всё становится едва ли не наоборот. Так что гипотеза остаётся только гипотезой, но ничего лучшего я не придумал.

Ну что же, с одной командой разобрались. Пора бы идти дальше, а то статья близится к концу, а ещё 255 команд не тестировано!

В статье на revspace [6] перечень протестированных команд оканчивается так:

command 0x89-0x8F
No response is returned, however command 0x8d seems to reset the sensor.

command 0x99 (range)
...

Кроме того, команды с меньшими номерами проверены не все. Таким образом, неизведанных команд остаётся ещё чуть более половины из общего числа.

Не мудрствуя лукаво, я решил подавать команды случайно (точнее, псевдослучайно). И вот что я получил:

...
Command: 255 1 47 0 0 0 0 0 208 Response: 0 0 0 0 0 0 0 0 0 CRC: 00 CO2/t/s/u: 0 0 0 0
Command: 255 1 17 0 0 0 0 0 238 Response: 0 0 0 0 0 0 0 0 0 CRC: 00 CO2/t/s/u: 0 0 0 0
Command: 255 1 45 0 0 0 0 0 210 Response: 0 0 0 0 0 0 0 0 0 CRC: 00 CO2/t/s/u: 0 0 0 0
Command: 255 1 5 0 0 0 0 0 250 Response: 0 0 0 0 0 0 0 0 0 CRC: 00 CO2/t/s/u: 0 0 0 0
Command: 255 1 88 0 0 0 0 0 167 Response: 0 0 0 0 0 0 0 0 0 CRC: 00 CO2/t/s/u: 0 0 0 0
Command: 255 1 245 0 0 0 0 0 10 Response: 0 0 0 0 0 0 0 0 0 CRC: 00 CO2/t/s/u: 0 0 0 0
Command: 255 1 107 0 0 0 0 0 148 Response: 0 0 0 0 0 0 0 0 0 CRC: 00 CO2/t/s/u: 0 0 0 0
Command: 255 1 214 0 0 0 0 0 41 Response: 0 0 0 0 0 0 0 0 0 CRC: 00 CO2/t/s/u: 0 0 0 0
Command: 255 1 136 0 0 0 0 0 119 Response: 0 0 0 0 0 0 0 0 0 CRC: 00 CO2/t/s/u: 0 0 0 0
Command: 255 1 7 0 0 0 0 0 248 Response: 0 0 0 0 0 0 0 0 0 CRC: 00 CO2/t/s/u: 0 0 0 0
Command: 255 1 153 0 0 0 0 0 102 Response: 255 153 1 0 0 0 0 0 102 CRC: 102102 CO2/t/s/u: 256 0 0 0
Command: 255 1 146 0 0 0 0 0 109 Response: 0 0 0 0 0 0 0 0 0 CRC: 00 CO2/t/s/u: 0 0 0 0
Command: 255 1 72 0 0 0 0 0 183 Response: 96 249 2 211 215 212 17 215 204 CRC: 159204 CO2/t/s/u: 723 215 212 4567
Command: 255 1 51 0 0 0 0 0 204 Response: 93 151 80 143 212 255 255 255 217 CRC: 185217 CO2/t/s/u: 20623 212 255 -1
Command: 255 1 98 0 0 0 0 0 157 Response: 16 136 252 75 66 50 48 48 13 CRC: 9313 CO2/t/s/u: -949 66 50 12336
Command: 255 1 65 0 0 0 0 0 190 Response: 10 0 0 0 0 0 0 0 0 CRC: 0 0 CO2/t/s/u: 0 0 0 0
Command: 255 1 243 0 0 0 0 0 12 Response: 0 0 0 0 0 0 0 0 0 CRC: 0 0 CO2/t/s/u: 0 0 0 0
Command: 255 1 13 0 0 0 0 0 242 Response: 0 0 0 0 0 0 0 0 0 CRC: 0 0 CO2/t/s/u: 0 0 0 0
Command: 255 1 35 0 0 0 0 0 220 Response: 0 0 0 0 0 0 0 0 0 CRC: 0 0 CO2/t/s/u: 0 0 0 0
Command: 255 1 229 0 0 0 0 0 26 Response: 0 0 0 0 0 0 0 0 0 CRC: 0 0 CO2/t/s/u: 0 0 0 0
Command: 255 1 95 0 0 0 0 0 160 Response: 0 0 0 0 0 0 0 0 0 CRC: 0 0 CO2/t/s/u: 0 0 0 0
Command: 255 1 48 0 0 0 0 0 207 Response: 0 0 0 0 0 0 0 0 0 CRC: 0 0 CO2/t/s/u: 0 0 0 0
Command: 255 1 209 0 0 0 0 0 46 Response: 0 0 0 0 0 0 0 0 0 CRC: 0 0 CO2/t/s/u: 0 0 0 0
Command: 255 1 200 0 0 0 0 0 55 Response: 0 0 0 0 0 0 0 0 0 CRC: 0 0 CO2/t/s/u: 0 0 0 0
...

Здесь Command — то, что было послано датчику (сам номер команды — третье число с начала), Response — что датчик ответил, на остальное можно не смотреть (CRC — контрольная сумма вычисленная/фактическая, CO2/t/s/u — результат разбивки ответа датчика на четыре числа, как если бы он отвечал на «дефолтную» команду).

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

Command: 255 1 134 0 0 0 0 0 121 Response: 0 0 0 0 0 0 0 0 0 CRC: 0 0 CO2/t/s/u: 0 0 0 0
Command: 255 1 130 0 0 0 0 0 125 Response: 242 98 200 201 207 216 178 130 33 CRC: 50 33 CO2/t/s/u: -14135 207 216 -19838
Command: 255 1 134 0 0 0 0 0 121 Response: 204 91 151 80 143 212 255 255 236 CRC: 93 236 CO2/t/s/u: -26800 143 212 -1
Command: 255 1 200 0 0 0 0 0 55 Response: 181 156 252 77 79 68 66 85 83 CRC: 241 83 CO2/t/s/u: -947 79 68 16981
Command: 255 1 134 0 0 0 0 0 121 Response: 13 10 0 0 0 0 0 0 0 CRC: 246 0 CO2/t/s/u: 0 0 0 0
Command: 255 1 216 0 0 0 0 0 39 Response: 0 0 0 0 0 0 0 0 0 CRC: 0 0 CO2/t/s/u: 0 0 0 0

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

Первым делом я попробовал старый добрый рецепт любого эникейщика — выключить и снова включить. То бишь вынул и воткнул у датчика плюс питания при работающем всём остальном. Датчик выдал приблизительно то же, что и в последнем процитированном отрывке (но с немного различающимися цифрами). Ага, значит, датчик не совсем помер и при каждой загрузке что-то говорит, догадался штирлиц.

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

Получается, что датчик вовсе прекратил воспринимать какие-либо команды. Видимо, 5 вольт на последовательный интерфейс не прошли даром. Да, не задалась как-то статья. Всем спасибо за внимание. Расходимся, здесь не на что смотреть.

Ах, постойте…

Видите эти цифры в конце?

13 10

Ничего не напоминает?

Конечно, это старый добрый перевод строки [8]! Другими словами, 0x0D 0x0A — так принято переводить строку, например, в Windows (а в Unix поступают проще — 0x0A, потому-то некоторые файлы при открытии в виндовом блокноте лишаются переводов строк).

Так может быть, датчик хочет что-то нам сказать человеческим языком? Впрочем, если этот язык — китайский, мне это мало поможет. Так что будем надеяться, что это более понятный язык, и расшифруем сообщение по таблице ASCII [9]:

print reduce(lambda a, b: a + b, map(lambda a: chr(int(a)), "255 255 255 250 24 220 207 254 77 79 68 66 85 83 13 10".split()))
�������MODBUS

Получили целых шесть букв. А вот что этот же датчик говорил при загрузке немного ранее:

print reduce(lambda a, b: a + b, map(lambda a: chr(int(a)), "96 249 2 211 215 212 17 215 204  93 151 80 143 212 255 255 255 217  16 136 252 75 66 50 48 48 13  10 ".split()))
`������]�P��������KB200

Разница налицо — строка KB200 сменилась на строку MODBUS. Остальное — похоже, что вовсе не текст, даже не китайские иероглифы — хотя бы потому, что большая их часть раз от раза меняется.

Так что же такое говорит нам датчик? По запросу KB200 поиск в интернете выдаёт клавиатуру, керноотборник, утюг, пеленальный столик, контроллер управления поворотными PTZ камерами, смеситель, разве что чёрта лысого не выдаёт. Хорошо, но не вполне ясно, как эти знания применить к нашему случаю. Ну что же, поищем MODBUS. На этот раз к нашим услугам сама википедия [10]:

Modbus — открытый коммуникационный протокол, основанный на архитектуре ведущий-ведомый (master-slave). Широко применяется в промышленности для организации связи между электронными устройствами. Может использоваться для передачи данных через последовательные линии связи RS-485, RS-422, RS-232, а также сети TCP/IP (Modbus TCP).

Сама собой напрашивается гипотеза — после одной из недокументированных команд датчик переключился на протокол Modbus и теперь доступными ему способами оповещает нас об этом. Поскольку других вариантов не предвидится — займёмся прямой проверкой этой гипотезы, то бишь попробуем обратиться к датчику по Modbus.

На просторах интернета была найдена реализация [11] Modbus для ардуины. Только вот незадача — Arduino Leonardo не поддерживается, а у меня по чудовищной случайности как раз Leonardo.

Но, как я вскорости понял, это даже к лучшему. Протокол Modbus прост, как три копейки. И к тому же до боли похож на «родной» протокол MH-Z19. Так зачем тащить всякую гадость из интернета и мучительно разбираться в ней, когда можно за две минуты (ну хорошо, за два часа) реализовать всё необходимое самостоятельно?

Итак, раньше мы, желая узнать показания, просили датчик их выдать следующим образом:

0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79

Или в десятичном выражении:

255, 1, 134, 0, 0, 0, 0, 0, 121

Где 255 — магическое число, 1 — адрес датчика, 134 — команда, 121 — байт контрольной суммы.

А как это будет выглядеть в Modbus? Точного соответствия не будет, но можно сделать, например, так:

1, 4, 0, 0, 0, 4, 213, 197

1 — адрес датчика.

4 — номер команды. Команды в Modbus можно пересчитать по пальцам, и нас пока будут интересовать лишь две из них — 3 и 4. По причинам, сокрытым в тьме веков, они называются Read Multiple Holding Registers и Read Input Registers, а по сути — дают команду на чтение заданного количества двухбайтовых чисел по заданному адресу. По команде 3 читаются числа, доступные на чтение/запись, а по команде 4 — только на чтение.

0, 0 — адрес, по которому мы собираемся читать (в данном случае 0). Если один и тот же адрес задать с командой 3 или 4 — мы получим в общем случае разные результаты.

0, 4 — количество чисел, которые мы хотим прочитать (в данном случае 4). Здесь есть забавный момент. Хотя можно задать это число вплоть до 65535, фактически протокол позволяет читать не более 125 чисел за раз. А всё потому, что в ответе указывается количество байт посланной информации, и оно занимает всего один байт (а числа-то двухбайтовые). К тому же, насколько я понял, длина самого ответа ограничена 255 байтами.

213, 197 — два байта контрольной суммы (CRC16 [12]). В общем-то это самый сложный момент во всей нашей реализации. По правде говоря, я даже не вникал, как оно считается, а просто скопипастил код отсюда [13]. Поскольку различных CRC16 существует море, надо ответственно подходить к выбору функции. Проверить, подходит ли конкретная функция для Modbus, можно, например, здесь [14].

Вот, собственно, и всё, что нам нужно знать о Modbus. По крайней мере, для начала.

Хотя вы уже давно догадались, что моё воззвание к MH-Z19 по Modbus завершилось успехом, но давайте сделаем вид, что интрига сохраняется. Пока что мы ещё даже не знаем, какой у нашего датчика адрес (хотя «в оригинале» он равен 1, далеко не факт, что по Modbus он такой же). Значит, надо повторить команду с разными адресами (а их всего 255) и посмотреть, на какой наш датчик откликнется:

...
Command: 254 4 0 3 0 1 213 197 CRC: 213 197 Response:
Command: 255 4 0 3 0 1 212 20 CRC: 212 20 Response:
Command: 0 4 0 3 0 1 192 27 CRC: 192 27 Response:
Command: 1 4 0 3 0 1 193 202 CRC: 193 202 Response:
Command: 2 4 0 3 0 1 193 249 CRC: 193 249 Response: 1 132 2 194 193
Command: 3 4 0 3 0 1 192 40 CRC: 192 40 Response:
Command: 4 4 0 3 0 1 193 159 CRC: 193 159 Response:
...

Похоже, что адрес — 2. Повторяю несколько команд с адресом 2 — нет ответа. Меняю адрес на 1 — есть ответ! Таким образом, адрес датчика такой же, какой и был — 1.

А почему в приведённом отрывке получился адрес 2? Здесь снова вылезает индусскость моего кода. После отдачи команды я проверяю, есть ли байты на приём. Но поскольку я делаю это сразу же, датчик не успевает ничего послать, так что ответ датчика принимается программой лишь в следующем цикле. Что и можно наблюдать в приведённом логе — первое число в ответе — 1, а оно как раз обозначает адрес датчика. Эту неприятность я обошёл, попросту добавив задержку в 50 миллисекунд перед приёмом ответа.

Рассмотрим ответ датчика на нашу команду:

1 132 2 194 193

1 — как мы уже выяснили — адрес датчика.

132 — код команды и код ошибки. Если бы ошибки не было, это число было бы таким же, как и посланная команда — то бишь 4. Но ошибка произошла, о чём говорит выставленный в 1 старший бит, так что число стало равно 4 + 128 = 132. Поэтому, кстати, команда в Modbus не может иметь номер более 127 — для такой команды код успешного завершения был бы таким же, как код ошибки.

2 — код ошибки. Говорит, какая именно ошибка произошла. Как говорит википедия, 02 — Адрес данных, указанный в запросе, недоступен. Значит, по этому адресу рыбы нет и ловить нечего. Будем пробовать другие адреса.

194 193 — CRC16.

Теперь, наконец, пора бы прошерстить адресное пространство датчика, чтобы понять, где там рыба. Это я сделал просто — посылал команду на прочтение одного числа с новым адресом каждые 0,1 секунды. Поскольку адресов 65536, этот процесс завершается приблизительно за два часа. Результаты вкратце таковы:

Команда 1 (Read Coils) — с любым адресом ошибка 2.
Команда 2 (Read Discrete Inputs) — с любым адресом ошибка 2.
Команда 4 (Read Input Registers) — с любым адресом ошибка 2.
Команда 3 (Read Multiple Holding Registers) — выдаёт успех при адресах от 0 до 289.
Команды на запись значений (например, 6) — вроде работают, но, похоже, что записанное значение вскорости заменяется тем, что было. Но вплотную этот вопрос не исследовал.

Итак, круг поиска сужается — нам нужны числа по команде 3 и адресам от 0 до 289. Представление о богатом внутреннем мире датчика можно получить по этим строкам:

Command: 1 3 0 0 0 64 68 58 CRC: 68 58 Response: 1 3 128 0 0 0 255 0 1 0 1 0 255 0 255 0 255 0 255 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 255 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 158 124
Command: 1 3 0 64 0 64 69 238 CRC: 69 238 Response: 1 3 128 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 27 165
Command: 1 3 0 128 0 64 69 210 CRC: 69 210 Response: 1 3 128 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 73 82 0 153 0 8 0 17 0 177 0 19 0 19 0 196 0 18 0 20 0 214 0 18 0 21 0 232 0 18 0 22 0 250 0 5 0 24 0 255 0 5 0 18 1 4 0 3 0 23 1 7 0 25 0 0 0 0 0 0 0 0 0 0 0 255 0 255 0 1 0 0 0 5 0 0 0 0 0 0 0 0 0 0 137 122
Command: 1 3 0 192 0 64 68 6 CRC: 68 6 Response: 1 3 128 165 165 0 165 0 0 255 255 255 255 255 255 255 255 0 15 3 232 0 100 0 90 0 0 0 63 128 0 0 255 0 15 0 5 0 10 0 5 0 5 0 30 0 15 0 0 0 20 0 40 0 60 0 80 0 100 0 0 0 5 0 5 3 232 255 255 255 255 165 165 0 165 0 0 16 3 0 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 163 198

Правда, в архиве сохранились числа только до адреса 255. Здесь, опять же, 1 3 — адрес и команда, 128 — число переданных байт, два байта в конце — CRC, а всё остальное — содержимое памяти.

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

Время снова строить графики!

Величина по адресу 261:

Тёмная сторона MH-Z19 - 9

Похоже на концентрацию СО2. Видно, когда я пробовал дышать на датчик. Правда, минимальное значение лежит заметно ниже, чем «эталонные» 400 ppm, так что калибровка оставляет желать лучшего. Так или иначе, датчик объявляется воскресшим.

Величина по адресу 264:

Тёмная сторона MH-Z19 - 10

Приблизительно то же, только в другом масштабе и перевёрнутое.

И обе величины на одном графике:

Тёмная сторона MH-Z19 - 11
Возникает естественный вопрос — быть может, одна из этих величин — это и есть то самое «сырое» значение, которое приходит с АЦП и которое датчик одному ему ведомым способом преобразует в оценку СО2? Если так, то это открывает большие возможности по перекалибровке и повышению точности датчика, а быть может, и по использованию его не по назначению. Ответ на этот вопрос (и на некоторые другие) мы попытаемся получить в следующей серии.

В каментах принимаются пожелания и предложения — как ещё поиздеваться над MH-Z19 в следующей статье. Правда, обещать ничего не буду.

Автор: wormball

Источник [15]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/umny-j-dom/245296

Ссылки в тексте:

[1] Что, опять?: https://geektimes.ru/search/?q=MH-Z19

[2] Сколько ж можно?: https://habrahabr.ru/search/?q=MH-Z19

[3] статьи: https://geektimes.ru/search/?target_type=posts&order_by=relevance&q=co2

[4] википедия: https://en.wikipedia.org/wiki/Carbon_dioxide#Toxicity

[5] статьи: https://geektimes.ru/post/278178/

[6] интересную страничку: https://revspace.nl/MHZ19

[7] NDIR: https://en.wikipedia.org/wiki/Nondispersive_infrared_sensor

[8] перевод строки: https://ru.wikipedia.org/wiki/Перевод_строки

[9] ASCII: https://ru.wikipedia.org/wiki/ASCII

[10] википедия: https://ru.wikipedia.org/wiki/Modbus

[11] реализация: https://habrahabr.ru/post/249043/

[12] CRC16: https://en.wikipedia.org/wiki/Cyclic_redundancy_check

[13] отсюда: http://stackoverflow.com/questions/19347685/calculating-modbus-rtu-crc-16

[14] здесь: http://crccalc.com/

[15] Источник: https://geektimes.ru/post/285572/