День #9: Морозное программирование!

День #9 адвент-календаря Let it Glow — Морозное программирование

Наступил день #9 адвент-календаря Let it Glow!

Сегодня снова день управляющего компонента, и было бы неправильно не включить датчик температуры — учитывая типичные для этого времени года холода (и насколько интересно с ним работать!).

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

Датчик сегодняшнего дня немного умнее некоторых других: он умеет измерять и температуру, и влажность, а общается с нашим Pico по-другому — с помощью протокола I2C. Не беспокойтесь, мы всё объясним!

Давайте приступим к программированию…

Содержимое коробки #9

В этой коробке вы найдёте:

  • 1x датчик температуры и влажности DHT20/AHT20

  • 4x провода-перемычки «папа–папа»

Содержимое коробки #9 адвент-календаря Let it Glow

Задания сегодняшнего дня

Сегодня мы научимся пользоваться датчиком температуры — использовать его показания в коде для вывода информации и управления RGB-светодиодами.

Сегодня предстоит многому научиться, включая несколько более сложных тем: I2C, установка библиотек и словари. Начнём!

Что такое I2C?

I2C, иногда называемый IIC, расшифровывается как Inter-Integrated Circuit («межинтегральная схема»).

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

I2C требует всего двух проводов для связи (плюс провода 3.3V и GND для питания датчика) и имеет преимущества перед некоторыми другими вариантами связи — но мы не будем вас этим утомлять, поскольку это не понадобится до гораздо более позднего этапа вашего пути мейкера.

Нельзя использовать любой вывод GPIO для I2C — нужно использовать синие выводы SDA или SCL, которые вы увидите на карте выводов Pico.

Для работы с I2C его также нужно импортировать в коде, что мы покажем через минуту!

Сборка схемы

Как всегда, убедитесь, что Pico отключён от USB-кабеля перед работой со схемой.

Подготовка макетной платы

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

Вот ваша отправная точка:

Отправная точка дня #9

Подключение датчика температуры

Сегодня простой компонент всего с четырьмя контактами для подключения. Будьте аккуратны — маленькие ножки не любят усилий!

Сначала установите датчик в правую часть макетной платы. Передняя сторона (с отверстиями, похожая на вафлю) должна быть обращена к вам.

Затем подключите его, двигаясь слева направо с ножки 1 по 4:

  • Ножка 1 (левая) подключается к 3.3V (физический вывод 36)

  • Ножка 2 подключается к SDA на GPIO 14 (физический вывод 19)

  • Ножка 3 подключается к GND (используем физический вывод 18)

  • Ножка 4 подключается к SCL на GPIO15 (физический вывод 20)

Ваша макетная плата должна выглядеть так (ваш датчик будет чёрного цвета):

Готовая схема дня #9

Совет: мы используем выводы SDA и SCL на GPIO 14 и 15, но если посмотреть на карту выводов Pico, видно, что можно было бы использовать и многие другие, например GPIO10 и GPIO11. У Pico много I2C-выводов на выбор!


Установка библиотеки

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

До сих пор мы импортировали только библиотеки, уже встроенные в MicroPython. Иногда вам понадобятся внешние библиотеки, которые нужно сначала сохранить на Pico, а затем импортировать как обычно в начале программы.

Установка библиотеки DHT20

Мы будем использовать отличную библиотеку pico-dht20, созданную пользователем GitHub flrrth.

Мы не будем просить вас разбираться в GitHub — вместо этого мы скопировали нужный код библиотеки и разместили его ниже.

Скопируйте длинный код ниже в Thonny обычным способом, но вместо запуска выберите File в главном меню, затем Save As, и когда появится окно с вопросом, куда сохранить, выберите „Raspberry Pi Pico“, назовите файл dht20.py, затем нажмите OK.

# https://github.com/flrrth/pico-dht20

from machine import I2C
from utime import sleep_ms

class DHT20:
    """Class for the DHT20 Temperature and Humidity Sensor.

    The datasheet can be found at http://www.aosong.com/userfiles/files/media/Data%20Sheet%20DHT20%20%20A1.pdf
    """

    def __init__(self, address: int, i2c: I2C):
        self._address = address
        self._i2c = i2c
        sleep_ms(100)

        if not self.is_ready:
            self._initialize()
            sleep_ms(100)

            if not self.is_ready:
                raise RuntimeError("Could not initialize the DHT20.")

    @property
    def is_ready(self) -> bool:
        """Check if the DHT20 is ready."""
        self._i2c.writeto(self._address, bytearray(b'\x71'))
        return self._i2c.readfrom(self._address, 1)[0] == 0x18

    def _initialize(self):
        buffer = bytearray(b'\x00\x00')
        self._i2c.writeto_mem(self._address, 0x1B, buffer)
        self._i2c.writeto_mem(self._address, 0x1C, buffer)
        self._i2c.writeto_mem(self._address, 0x1E, buffer)

    def _trigger_measurements(self):
        self._i2c.writeto_mem(self._address, 0xAC, bytearray(b'\x33\x00'))

    def _read_measurements(self):
        buffer = self._i2c.readfrom(self._address, 7)
        return buffer, buffer[0] & 0x80 == 0

    def _crc_check(self, input_bitstring: str, check_value: str) -> bool:
        """Calculate the CRC check of a string of bits using a fixed polynomial.

        See https://en.wikipedia.org/wiki/Cyclic_redundancy_check
            https://xcore.github.io/doc_tips_and_tricks/crc.html#the-initial-value

        Keyword arguments:
        input_bitstring -- the data to verify
        check_value -- the CRC received with the data
        """

        polynomial_bitstring = "100110001"
        len_input = len(input_bitstring)
        initial_padding = check_value
        input_padded_array = list(input_bitstring + initial_padding)

        while '1' in input_padded_array[:len_input]:
            cur_shift = input_padded_array.index('1')

            for i in range(len(polynomial_bitstring)):
                input_padded_array[cur_shift + i] = \
                    str(int(polynomial_bitstring[i] != input_padded_array[cur_shift + i]))

        return '1' not in ''.join(input_padded_array)[len_input:]

    @property
    def measurements(self) -> dict:
        """Get the temperature (°C) and relative humidity (%RH).

        Returns a dictionary with the most recent measurements.

        't': temperature (°C),
        't_adc': the 'raw' temperature as produced by the ADC,
        'rh': relative humidity (%RH),
        'rh_adc': the 'raw' relative humidity as produced by the ADC,
        'crc_ok': indicates if the data was received correctly
        """
        self._trigger_measurements()
        sleep_ms(50)

        data = self._read_measurements()
        retry = 3

        while not data[1]:
            if not retry:
                raise RuntimeError("Could not read measurements from the DHT20.")

            sleep_ms(10)
            data = self._read_measurements()
            retry -= 1

        buffer = data[0]
        s_rh = buffer[1] << 12 | buffer[2] << 4 | buffer[3] >> 4
        s_t = (buffer[3] << 16 | buffer[4] << 8 | buffer[5]) & 0xfffff
        rh = (s_rh / 2 ** 20) * 100
        t = ((s_t / 2 ** 20) * 200) - 50
        crc_ok = self._crc_check(
            f"{buffer[0] ^ 0xFF:08b}{buffer[1]:08b}{buffer[2]:08b}{buffer[3]:08b}{buffer[4]:08b}{buffer[5]:08b}",
            f"{buffer[6]:08b}")

        return {
            't': t,
            't_adc': s_t,
            'rh': rh,
            'rh_adc': s_rh,
            'crc_ok': crc_ok
        }

Закрытие файла библиотеки

Важно! После сохранения библиотеки на Pico закройте вкладку скрипта, нажав «X» на вкладке, как показано здесь:

Закрытие вкладки в Thonny

Сохранённую библиотеку вы также увидите в боковой панели файлов, на что указано на изображении выше.

Чтобы открыть новый файл для сегодняшних примеров, выберите значок «New» (или File > New):

Открытие нового файла в Thonny

Работа с библиотекой DHT20 и словарём

Эта библиотека создаёт словарь сохранённых значений с датчика для использования в коде, но что такое словарь в MicroPython?

Ранее мы рассматривали списки — удобные для хранения данных или переменных и позволяющие быстро обращаться к ним по индексу списка, когда это необходимо.

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

Запутались? Понятно! Отвлечёмся и прояснимна примере.

Словарь vs Список

Представим, что мы ведём список каждой понравившейся рождественской песни. Записи о каждом треке могут включать:

  • Название

  • Исполнитель

  • Приглашённый артист

  • Альбом

  • Год

  • Лейбл

  • Рейтинг

Можно сделать список для этих данных, формат выглядел бы так:

song1 = [title, artist, supportingartist, album, year, label, rating]

…и рабочий пример мог бы быть таким:

song1 = ["I Wish It Could Be Christmas Everyday", "Wizzard", "none", "none", "1973", "Warner Bro", 5]

Это работает… но будет сложно вспомнить, какой индекс в этом списке соответствует какому полю, а более длинные списки стали бы ещё более неудобными!

Словарь может быть лучшим вариантом и выглядел бы так:

song1 = {
  "title": "I Wish It Could Be Christmas Everyday",
  "artist": "Wizzard",
  "supportingartist": "none",
  "album": "none",
  "year": "1973",
  "label": "Warner Bro",
  "rating": 5,
}

Ключ словаря — удобно!

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

Используя приведённые выше примеры, напечатаем год выхода нашей песни.

Сначала с помощью списка:

print(song1[4])

Теперь с помощью словаря:

print(song1["year"])

Видите! Словарь значительно упрощает получение нужных данных, поскольку ключ имеет понятное, запоминающееся имя, а не индекс.

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

Итак, на чём мы остановились…

Задание 1: Простая проверка чтения датчика

Теперь, когда мы понимаем словари в MicroPython, начнём с простого: запустим код для получения показаний из словаря датчика каждые пять секунд и вывода их в Thonny.


Проверьте ещё раз — вы закрыли вкладку файла библиотеки? Если нет, именно здесь всё может пойти не так: вы можете случайно перезаписать библиотеку, если попытаетесь запустить скрипт в той же вкладке. Для кода нужна новая вкладка.


Код

Чтобы воспользоваться библиотекой, нам нужно её импортировать с помощью строки from dht20 import DHT20.

Затем код настраивает используемые нами I2C-выводы — это не сильно отличается от того, как мы ранее настраивали GPIO-выводы. Далее есть пара строк для настройки I2C и нашего I2C-устройства (датчика).

Каждое I2C-устройство имеет адрес — наш датчик использует 0x38. Пока вам не нужно об этом беспокоиться, но постарайтесь запомнить: если в будущем вы захотите подключить несколько I2C-устройств, возможно, придётся проверить, чтобы у них не совпадали адреса.

Цикл while получает данные с датчика, обращаясь к словарю через measurements = dht20.measurements. Теперь можно работать с этими данными!

Затем мы выводим каждое значение из словаря, используя имена (ключи) каждого доступного элемента. Вот список данных, доступных в словаре, взятый со страницы библиотеки на GitHub:

Ключ

Описание

t

Температура (°C)

t_adc

«Сырое» значение температуры, полученное от датчика

rh

Относительная влажность (%RH)

rh_adc

«Сырое» значение влажности, полученное от датчика

crc_ok

Результат проверки CRC (True или False)

Запустите приведённый ниже код и получите первые показания:

from machine import Pin, I2C
import time
from dht20 import DHT20

# Настройка I2C-выводов
i2c1_sda = Pin(14)
i2c1_scl = Pin(15)

# Настройка I2C
i2c1 = I2C(1, sda=i2c1_sda, scl=i2c1_scl)

# Настройка устройства DHT20 с I2C-адресом
dht20 = DHT20(0x38, i2c1)

while True:

    # Получить данные из словаря датчика
    measurements = dht20.measurements

    # Вывести данные
    print(measurements['t'])
    print(measurements['t_adc'])
    print(measurements['rh'])
    print(measurements['rh_adc'])
    print(measurements['crc_ok'])

    # Подождать 5 секунд
    time.sleep(5)

Задание 2: Читаемые показания!

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

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

Строку crc_ok (Cyclic Redundancy Check — проверка циклическим избыточным кодом) мы не включим, так как не хотим проверять ошибки в простом примере (это простое значение True/False, показывающее, не «сбились» ли данные).

Мы также познакомимся с форматированными строковыми литералами, чтобы сделать вывод ещё более удобным. Давайте быстро разберём это…

Что такое форматированные строковые литералы?

Форматированные строковые литералы, говоря простым языком, позволяют смешивать строки с переменными Python (и любыми операциями над ними) — и всё это объединяется в одну очень полезную строку (отлично подходит для строк вывода!).

«Но вы уже показывали нам, как добавлять текст рядом с переменной» — слышим ваш вопрос! Да, показывали, однако форматированные строковые литералы мощнее, как мы сейчас покажем.

Пример

Нам нужна строка вывода примерно такого вида: Температура: 56.1°C.

Это не кажется сложным, пока не осознаешь, что коду нужно сделать для этого. Ему нужно:

  • Начать со строки «Температура: »

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

  • Округлить показание до 1 знака после запятой

  • Вставить переменную с показанием в строку

  • Добавить «°C» в конце

Вот строка кода, которая это делает с помощью форматированных строковых литералов:

print(f"Temperature: {round(measurements['t'],1)}°C")

Разберём по частям:

  • print(f — строковые литералы требуют f после первой скобки

  • «Temperature: — начинаем строку с простого текста, внутри первой кавычки

  • {round(measurements[„t“],1)} — любые данные, которые нужно вставить в строку, помещаются в фигурные скобки {}. Внутри используем функцию round для значения температуры [„t“], задавая 1 знак после запятой.

  • °C») — заканчиваем строку символом °C и закрываем скобки и кавычки

Если это кажется слишком загруженным, можно предварительно округлить данные в отдельную переменную. Это добавляет лишнюю строку кода, но делает всё более читаемым:

temperature = round(measurements['t'],1)
print(f"Temperature: {temperature}°C")

Код

Теперь, изучив форматированные строковые литералы, вы должны понять, что происходит в примере кода ниже.

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

Попробуйте!

from machine import Pin, I2C
import time
from dht20 import DHT20

# Настройка I2C-выводов
i2c1_sda = Pin(14)
i2c1_scl = Pin(15)

# Настройка I2C
i2c1 = I2C(1, sda=i2c1_sda, scl=i2c1_scl)

# Настройка устройства DHT20 с I2C-адресом
dht20 = DHT20(0x38, i2c1)

while True:

    # Получить данные из словаря датчика
    measurements = dht20.measurements

    # Вывести данные
    print("-- Environment ---------") # Заголовок
    print(f"Temperature:      {round(measurements['t'],1)}°C")
    print(f"Humidity:         {round(measurements['rh'],1)}%")
    print("------------------------") # Разделитель
    print("                        ") # Пустая строка

    # Подождать 5 секунд
    time.sleep(5)

Задание 3: Кольцо индикации температуры

Вернём светодиодное кольцо в проект и используем его для отображения температуры на шкале.

Определите температуру окружающей среды

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

Запустите предыдущий пример, понаблюдайте за температурой несколько минут и запишите, где она обычно находится. У нас она держалась около 20°C. Мы вернёмся к этому чуть позже.

Индекс светодиодов по температуре

Создадим простую переменную для показания температуры: temp = round(measurements[„t“]). Обратите внимание, что в конце нет числа для указания знаков после запятой? Это потому, что здесь нам нужны только целые числа (integers).

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

Вот пример с нашей температурой окружающей среды 20°C:

Температура

Индекс LED

14

0

15

1

16

2

17

3

18

4

19

5

20 (наша средняя температура)

6 (средний)

21

7

22

8

23

9

24

10

25

11

Исходя из этого примера и таблицы выше: при температуре 14°C загорается 1-й светодиод (индекс 0). При температуре 23°C загорается 10-й светодиод (индекс 9).

Возьмите ручку и составьте свою шкалу на основе вашей средней температуры. Начните с занесения температуры окружающей среды рядом с индексом 6, затем заполните остальную часть таблицы.

Разбор кода

Теперь соберём всё воедино! Разберём по частям, так как это немного сложнее.

Пример ниже снова подключает библиотеку neopixel и связанные строки настройки.

Затем создаём температурный словарь, который будем использовать для преобразования температур в значения индексов светодиодов (есть и другие способы сделать это, но словарная практика сегодня как нельзя кстати):

# Создаём словарь температура/LED для шкалы
# Температура — ключ (левый), индекс LED — значение (правый)
LEDdict = {
  14: 0,
  15: 1,
  16: 2,
  17: 3,
  18: 4,
  19: 5,
  20: 6, # Верхний-средний LED (индекс 6 / LED #7) для 20°C
  21: 7,
  22: 8,
  23: 9,
  24: 10,
  25: 11,
}

Наш цикл while получает данные температуры из словаря dht20, и мы округляем их до целого числа в новой переменной temperature:

# Получить данные из словаря датчика
measurements = dht20.measurements

# Создать округлённую переменную для температуры
temperature = round(measurements['t'])

Затем начинаем оператор if, который проверяет, нет ли показания температуры в нашем словаре (от 14 до 25). Если этого не сделать, когда температура выйдет настолько за пределы диапазона, что индекс светодиода выйдет за возможный диапазон кольца, программа «упадёт»!

if temperature not in LEDdict:

    pass
    print("*** Out of temperature range ***")

Если температура находится в диапазоне нашего словаря, вместо этого сработает оператор else.

Затем говорим: «возьми это значение температуры как ключ и используй его со словарём, чтобы найти, какой индекс светодиода должен использоваться для этой температуры». Это создаёт новую переменную LEDindex со значением индекса из словаря.

# Используем переменную температуры со словарём
# для преобразования температуры в индекс LED
LEDindex = (LEDdict[temperature])

Выводим несколько строк для проверки данных и переменных:

# Вывести температуру и индекс
print("Temperature:",temperature)
print("LED index:  ",LEDindex)
print("----------------")

Быстро очищаем светодиоды, затем зажигаем тот, который соответствует индексу:

# Очистить кольцо
ring.fill((0,0,0))
ring.write()

ring[LEDindex] = (10,0,0) # Зажечь LED с нужным индексом
ring.write() # Записать данные в LED

Полная программа

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

Попробуйте открыть окно или осторожно направить фен на датчик (с расстояния не менее ~45 см) и наблюдайте, как меняются значения и светодиоды.

from machine import Pin, I2C
import time
from dht20 import DHT20
from neopixel import NeoPixel

# Настройка I2C-выводов
i2c1_sda = Pin(14)
i2c1_scl = Pin(15)

# Настройка I2C
i2c1 = I2C(1, sda=i2c1_sda, scl=i2c1_scl)

# Настройка устройства DHT20 с I2C-адресом
dht20 = DHT20(0x38, i2c1)

# Определяем номер вывода кольца (2) и количество светодиодов (12)
ring = NeoPixel(Pin(2), 12)

# Создаём словарь температура/LED для шкалы
# Температура — ключ (левый), индекс LED — значение (правый)
LEDdict = {
  14: 0,
  15: 1,
  16: 2,
  17: 3,
  18: 4,
  19: 5,
  20: 6, # Верхний-средний LED (индекс 6 / LED #7) для 20°C
  21: 7,
  22: 8,
  23: 9,
  24: 10,
  25: 11,
}

while True:

    # Получить данные из словаря датчика
    measurements = dht20.measurements

    # Создать округлённую переменную для температуры
    temperature = round(measurements['t'])

    if temperature not in LEDdict:

        pass
        print("*** Out of temperature range ***")

    else:

        # Используем переменную температуры со словарём
        # для преобразования температуры в индекс LED
        LEDindex = (LEDdict[temperature])

        # Вывести температуру и индекс
        print("Temperature:",temperature)
        print("LED index:  ",LEDindex)
        print("----------------")

        # Очистить кольцо
        ring.fill((0,0,0))
        ring.write()

        ring[LEDindex] = (10,0,0) # Зажечь LED с нужным индексом
        ring.write() # Записать данные в LED

    # Подождать 2 секунды перед следующим циклом
    time.sleep(2)

День #9 завершён!

На этом на сегодня всё — думаем, мы достаточно загрузили ваш мозг этими последними заданиями!

Вот и готово: теперь вы можете следить за внутренней обстановкой, когда начнётся снегопад (скрестим пальцы), и смотреть, насколько влажными становятся разные комнаты, когда вы закрываете окна или развешиваете бельё в доме!

Мы запускали примеры со светодиодным кольцом, но есть много способов использовать ваш растущий набор компонентов с этими датчиками:

  • Использовать с дисплеем столбчатого графика для 5-уровневого индикатора влажности

  • Использовать с блочным светодиодом как общий предупредительный сигнал, если температура или влажность превышает или падает ниже определённого значения

  • Менять цвета RGB-светодиода в зависимости от диапазонов температуры

  • Использовать с… кое-чем из будущих коробок (тссс!)

Подведём итоги — что мы изучили сегодня?

  • Подключение датчика температуры

  • Что такое I2C и как он работает

  • Выводы SDA и SCL I2C на Pico

  • Установка библиотек

  • Использование и создание словаря

  • Словари vs Списки

  • Форматированные строковые литералы

Оставьте всё как есть до утра, когда мы распакуем следующий мигающий компонент. До встречи!


Для создания схем подключения на макетной плате использовался Fritzing.