День #10 адвент-календаря мейкера: Прерывание луча!
Добро пожаловать в десятый день вашего адвент-календаря «12 проектов Кодмаса». Сегодня у нас ещё один новый датчик для изучения — датчик прерывания луча!
Эти датчики позволяют проекту обнаруживать объект, проходящий между ними, поэтому они отлично подходят для систем сигнализации или безопасности, а также для подсчёта прерываний луча при отслеживании движения или оборотов.
Сегодня мы также сделаем несколько забавных игр, так что давайте сразу приступим!
Содержимое коробки #10
В этой коробке вы найдёте:
1x Кастомный датчик прерывания луча (излучатель и приёмник) с вилочными концами джамперных проводов
1x Резистор 10 кОм
4x Джамперные провода «папа-папа»
Сегодняшний проект
Сегодня мы настроим датчик прерывания луча с простым примером, а затем сразу перейдём к весёлой игре — таймеру нажатий пальцем!
Датчик прерывания луча в вашей коробке состоит из двух частей — излучателя (2 провода) и приёмника (3 провода). Излучатель посылает инфракрасный свет, который обнаруживается приёмником, поддерживая сигнальный белый провод в состоянии HIGH. Если что-то прерывает этот луч (встаёт на пути), приёмник не обнаруживает сигнал и переводит провод в состояние LOW.
Мы говорим коду отслеживать сигнал LOW, который сообщает программе, что луч прерван, и тогда мы можем использовать это для запуска сигнала тревоги, счётчика или другого действия. Это обратная версия того, что мы использовали ранее, поскольку обычно мы ищем сигналы HIGH.
Они хорошо работают на расстоянии до 20–25 см друг от друга, но за пределами этого диапазона могут быть немного ненадёжны. Более продвинутые/промышленные версии используются для больших площадей, например дверных проёмов.
Существует так много крутых способов использования этих датчиков:
Как ручной датчик на столе для запуска кода
Вместе с поездом или набором Scalextric для обнаружения прохождения поезда/машины и подсчёта кругов
Сигнализация на банке с печеньем, обнаруживающая, когда кто-то тайком тянется за вкусняшкой
Вместе с роботом, используя диск с прорезанным отверстием для обнаружения и подсчёта оборотов колеса
…и многое другое!
Сборка схемы
Как всегда, отключите USB-кабель, чтобы Pico не был подключён к питанию.
Снимите детали датчика наклона со вчерашнего дня, но оставьте светодиоды и зуммер на месте. После сегодняшней коробки мы больше не будем использовать светодиоды!
Начальная схема должна выглядеть вот так:
Теперь возьмите мини-макетную плату и вставьте красный и чёрный джамперные провода в те же отверстия, что и на нашем рисунке. Излучатель и приёмник будут использовать одни и те же выводы 3,3 В (красный) и GND (чёрный), поэтому мы вставляем их в одни и те же шины.
Белый провод — это провод данных/сигнала только для приёмника, поэтому он располагается отдельно:
Нам нужно добавить входящий в комплект резистор 10 кОм между выводами 3,3 В (красный) и сигнальным (белый), поэтому вставьте его так же, как показано ниже:
Теперь нужно подключить всё к Pico/макетной плате.
Подключите чёрный провод GND к синему каналу GND, красный провод 3,3 В к нашему выводу 3,3 В (физический вывод 36) и белый сигнальный провод к GPIO 26 (*физический вывод 31*):
Задание 1: Базовая программа для датчика прерывания луча
Начнём сегодня с минимальной программы для датчика прерывания луча, чтобы показать, как они работают.
Вам нужно закрепить излучатель и приёмник на ровной поверхности, направив маленькие круглые головки друг на друга — расстояние около 15 см будет оптимальным. Можно использовать Blu-tack, липкую ленту или просто книги или другие тяжёлые предметы, положенные сверху, чтобы удержать их на месте — просто убедитесь, что ничто не мешает лучам.
Также следите за джамперными проводами, поскольку они любят выскакивать из макетных плат!
Код
Простой и понятный код, с которым вы уже работали раньше — мы импортируем необходимые модули, устанавливаем номер вывода датчика прерывания луча (с подтяжкой к земле), затем запускаем цикл while с оператором if, ожидающим перехода вывода в состояние LOW (прерывания луча).
Скопируйте это в Thonny, запустите код, а затем перекройте луч рукой:
# Импорты
from machine import Pin
import time
# Настраиваем вывод датчика
beam = Pin(26, Pin.IN, Pin.PULL_DOWN)
while True: # Запускать бесконечно
time.sleep(0.1) # Короткая задержка
if beam.value() == 0: # Если луч прерван
print("Луч прерван!")
Задание 2: Игра на рекорд с датчиком прерывания луча
Давайте сделаем небольшую игру с этими датчиками, которая:
Считает количество раз, которое вы можете прервать луч за 30 секунд
Выводит ваш счёт в конце
Затем завершает программу
Нам нужно убедиться, что игроки не смогут жульничать, просто держа палец на месте, поэтому мы используем тот же метод, что и в коде подсчёта со вчерашнего дня, где мы требуем изменения состояния вывода, прежде чем код примет следующий счёт. Мы также добавим зуммер в код, чтобы добавить звуковые сигналы для начала и конца игры.
В примере кода есть несколько новых элементов, которые нам нужно ввести, поэтому давайте сначала разберём их…
Время и Эпоха
Мы много использовали time.sleep в последние десять дней, но сегодня мы воспользуемся другой частью этого модуля.
В нашем примере кода мы используем time.time(). Это возвращает количество секунд, прошедших с момента эпохи. Эпоха — это временная метка, широко используемая в вычислениях, и представляет собой количество секунд, прошедших с 1 января 1970 года!
Теперь это может показаться очень странным использованием, но поскольку время эпохи продолжает тикать каждую секунду, это простой в использовании, надёжный и всегда доступный таймер. Существуют и другие способы запуска таймеров, но это был хороший повод рассказать об эпохе!
Мы делаем снимок эпохи как время начала игры, сохраняя его в переменной, затем считываем показания каждый цикл, используя простую математику для сравнения времени начала (в секундах) с текущим временем (в секундах), проверяя, прошло ли 30 секунд с момента начала.
Мы не можем просто считать секунды с начала игры с помощью обычного time.sleep, поскольку это не учитывает другие задержки в нашей программе (например, короткую задержку, которую мы добавляем в оператор while, чтобы программа не работала с безумной скоростью).
Если вам интересно, попробуйте короткий код ниже, чтобы увидеть, как сейчас выглядит эпоха:
import time
print(time.time())
Sys
Мы также импортируем sys в этом примере. Единственная причина добавления — использование sys.exit() для завершения программы после окончания игры.
Это ещё одна удобная команда, которую вы можете добавить в свои программы, чтобы убедиться, что они завершаются после выполнения условия. Как и во всём, что касается кода/Python/MicroPython, существует много способов достичь одного и того же результата — это лишь один из них, который мы хотели представить.
Код
Хотя пример кода ниже длиннее некоторых других, которые мы вам показывали, здесь нет ничего страшного или нового (за исключением заметок выше). Мы импортируем, настраиваем выводы и ШИМ, создаём переменные, затем используем цикл while с операторами if — всё это вы уже умеете!
Вы заметите, что наш оператор if всегда сначала проверяет таймер, потому что это самое важное. Если время вышло, мы не хотим, чтобы игрок мог продолжать. Если время ещё не вышло, операторы elif проверяют состояние и прервался ли луч.
Начинаем игру!
Скопируйте код ниже в Thonny и попробуйте, а затем посмотрите, сможете ли вы побить рекорды нашего персонала, приведённые ниже (единственное правило — можно использовать только один палец). Это отличная маленькая игра, которую можно устроить в праздничный период, чтобы бросить вызов семье и друзьям, и при этом показать свой новый набор и навыки!
Billie: 223
Marie: 221
Rich: 220
Chris C: 190
Jamie: 183
Colin: 172
Nigel: 165
# Импорты
from machine import Pin, PWM
import time, sys
# Настраиваем вывод датчика прерывания луча
beam = Pin(26, Pin.IN, Pin.PULL_DOWN)
# Настраиваем вывод зуммера как ШИМ
buzzer = PWM(Pin(13))
# Устанавливаем частоту ШИМ зуммера на 1000
buzzer.freq(1000)
# Начинаем с нулевой громкостью зуммера (duty 0)
buzzer.duty_u16(0)
# Создаём игровые переменные
starttime = 0
timecheck = 0
scorecounter = 0
state = 0
print("Игра начнётся после сигнала!")
# Длинный сигнал для обозначения начала игры
buzzer.duty_u16(10000)
time.sleep(2)
buzzer.duty_u16(0)
print("ВПЕРЁД!")
# Сохраняем время начала (в секундах)
starttime = time.time()
while True: # Запускать этот блок до остановки кода
time.sleep(0.0001) # Очень короткая задержка
# Берём текущее время эпохи и вычитаем время начала
# Это даёт нам количество секунд с начала игры
timecheck = time.time() - starttime
if timecheck >= 30: # Если прошло 30 или более секунд
print("ИГРА ОКОНЧЕНА!")
# Сигнал об окончании игры
buzzer.duty_u16(10000)
time.sleep(0.2)
buzzer.duty_u16(0)
# Вывести счёт игрока
print("ВАШ СЧЁТ:", scorecounter)
# Завершить программу
sys.exit()
elif state == 0 and beam.value() == 0: # Если state равен 0 И вывод LOW
scorecounter = scorecounter + 1 # Добавить +1 к счётчику
state = 1 # Изменить state на 1
print("СЧЁТ =", scorecounter) # Вывести новое значение счётчика
print("Осталось времени:", (30 - timecheck)) # Вычитаем timecheck из 30 — получаем оставшееся время
elif state == 1 and beam.value() == 1: # Если state равен 1 И вывод HIGH
state = 0 # Изменить state на 0
Задание 3: Игра с целевым счётом и датчиком прерывания луча
В последнем задании мы создали игру, где нужно набрать максимальный счёт за 30 секунд. На этот раз мы создаём игру с целевым показателем, который нужно достичь за 30 секунд, используя светодиоды для индикации того, насколько близок игрок к победе в игре.
Используется похожий код, как в примере выше, но мы делаем его немного насыщеннее, добавляя светодиоды с новой переменной для установки цели (targetscore), дополнительные операторы if для включения светодиодов в зависимости от счёта, и некоторые изменения в строках print для отображения цели рядом с текущим счётом.
Установка целевого счёта
Чтобы можно было легко установить другой целевой счёт (который управляет игрой и строками вывода), мы задаём цель как переменную и используем её по всей программе. Мы даже используем это для управления светодиодами, превращая значения в проценты в наших операторах if.
Например, один из операторов if для светодиодов выглядит так:
if scorecounter < (targetscore / 100 * 33):
Это говорит: *«Если счёт игрока меньше 33% от целевого счёта»*.
Всё, что мы делаем здесь — берём целевой счёт, делим на 100, чтобы получить 1%, затем умножаем на тот процент выполнения счёта, при котором мы хотим зажигать каждый светодиод — и сравниваем это со счётом игрока.
Мы используем от 0 до 33% для красного светодиода, от 33% до 66% для янтарного светодиода, затем любой счёт выше 66% зажигает зелёный светодиод. Это даёт игроку визуальную индикацию того, насколько близко он к победе в игре.
Операторы if внутри if?!
Вы заметите, что операторы if для индикатора светодиодов находятся внутри оператора elif. Это нормально и называется вложенным оператором if. Хотя это может немного усложнить чтение и проверку кода, это ещё один инструмент, который вы можете использовать, если хотите, чтобы сработавший оператор if затем проверял что-то ещё.
В нашем примере нас устраивает то, что игрок сработал оператор elif для увеличения счёта, но мы также хотим проверить счёт и обновить светодиоды в рамках этого.
Код
Попробуйте, скопировав и запустив код ниже, а затем, почему бы не попробовать:
Увеличить целевой счёт, чтобы сделать игру сложнее
Изменить проценты/диапазоны для светодиодов
Создать функции для выключения светодиодов или сигнала зуммера, чтобы сэкономить несколько строк кода
# Импорты
from machine import Pin, PWM
import time, sys
# Настраиваем выводы светодиодов
red = Pin(18, Pin.OUT)
amber = Pin(19, Pin.OUT)
green = Pin(20, Pin.OUT)
# Настраиваем вывод датчика прерывания луча
beam = Pin(26, Pin.IN, Pin.PULL_DOWN)
# Настраиваем вывод зуммера как ШИМ
buzzer = PWM(Pin(13))
# Устанавливаем частоту ШИМ зуммера на 1000
buzzer.freq(1000)
# Начинаем с нулевой громкостью зуммера (duty 0)
buzzer.duty_u16(0)
# Создаём игровые переменные
starttime = 0
timecheck = 0
scorecounter = 0
state = 0
targetscore = 100
print("Игра начнётся после сигнала!")
# Длинный сигнал для обозначения начала игры
buzzer.duty_u16(10000)
time.sleep(2)
buzzer.duty_u16(0)
print("ВПЕРЁД!")
print("-------------------------------")
# Сохраняем время начала (в секундах)
starttime = time.time()
while True: # Запускать этот блок до остановки кода
time.sleep(0.0001) # Очень короткая задержка
# Берём текущее время и вычитаем исходное время начала
# Это даёт нам количество секунд с начала игры
timecheck = time.time() - starttime
if timecheck >= 30: # Если прошло 30 или более секунд
# Светодиоды выключены
red.value(0)
amber.value(0)
green.value(0)
# Сигнал об окончании игры
buzzer.duty_u16(10000)
time.sleep(0.2)
buzzer.duty_u16(0)
# Вывести цель и счёт игрока
print("-------------------------------")
print("ИГРА ОКОНЧЕНА! ВЫ ПРОИГРАЛИ :(")
print("Цель была", targetscore, ", вы набрали", scorecounter)
print("-------------------------------")
# Завершить программу
sys.exit()
elif scorecounter >= targetscore: # Если счёт игрока достиг цели
# Светодиоды выключены
red.value(0)
amber.value(0)
green.value(0)
# Сигнал об окончании игры
buzzer.duty_u16(10000)
time.sleep(0.2)
buzzer.duty_u16(0)
# Вывести время, за которое удалось победить
print("-------------------------------")
print("ВЫ ПОБЕДИЛИ!")
print("Вам потребовалось", timecheck, "секунд!")
print("-------------------------------")
# Завершить программу
sys.exit()
elif state == 0 and beam.value() == 0: # Если state равен 0 И вывод LOW
scorecounter = scorecounter + 1 # Добавить +1 к счётчику
state = 1 # Изменить state на 1
print("СЧЁТ =", scorecounter, "/", targetscore) # Вывести счёт и цель
print("Осталось времени:", (30 - timecheck)) # Вычитаем timecheck из 30 — получаем оставшееся время
if scorecounter < (targetscore / 100 * 33): # Если счёт меньше 33% от цели
red.value(1) # Красный светодиод включён
amber.value(0)
green.value(0)
elif (targetscore/ 100 * 33) < scorecounter < (targetscore / 100 * 66): # Если счёт между 33% и 66% от цели
red.value(1) # Красный светодиод включён
amber.value(1) # Янтарный светодиод включён
green.value(0)
elif scorecounter > (targetscore / 100 * 66): # Если счёт более 66% от цели
red.value(1) # Красный светодиод включён
amber.value(1) # Янтарный светодиод включён
green.value(1) # Зелёный светодиод включён
elif state == 1 and beam.value() == 1: # Если state равен 1 И вывод HIGH
state = 0 # Изменить state на 0
День #10 завершён!
Надеемся, вам понравились игры в сегодняшних заданиях с датчиком прерывания луча — почему бы не привлечь всю семью, чтобы похвастаться своим проектом и узнать, кто наберёт наибольший счёт, завоевав право на хвастовство?
Теперь у вас есть ещё один датчик в постоянно растущей коробке с запчастями для создания проектов — возможно, вы объедините его с PIR-датчиком из дня #7, чтобы создать отличную сигнализацию с несколькими датчиками?
Это последний раз, когда мы используем светодиоды в календаре, поэтому смело убирайте их из схемы (лучше хранить их в той же пакете, что и резисторы, чтобы не перепутать). Завтра мы также не будем использовать зуммер, так что его тоже можно убрать.
Итак, что мы рассмотрели в день #10? Сегодня вы:
Собрали схему с датчиками прерывания луча
Научились программировать датчики прерывания луча (с их способом работы «всегда HIGH»)
Создали забавную игру с датчиком прерывания луча
Познакомились с дополнительными возможностями модуля time, включая время эпохи
Научились использовать модуль sys для завершения программы
Создали вложенные операторы if
Использовали простую математику для перевода значений в проценты с помощью MicroPython
Продолжили повторно использовать всё то, что вы узнали за последние 10 дней
Завтра нет датчика! Мы решили, что к этому моменту вы, возможно, немного устали от датчиков. Это последний датчик, который вы получите в календаре, …и теперь вы задаётесь вопросом, что же в следующих двух коробках… увидимся завтра!