Материал посвящён теме, популярной на роботехнических фестивалях: "Следование по линии".
Эта публикация - вклад в разработку главы 3 анонсированного курса основ программирования и робототехники, но может рассматриваться как самостоятельный текст для читателей, имеющих опыт программирования в среде makecode.
Замечание о стиле. Материал предназначен для ведущего занятия и содержит результат авторского исследования. Превратить результат обратно в процесс - задача ведущего. Вопросы в тексте обычно заготовлены для участников проекта (хотя это только авторское видение, каждый ведущий может развернуть материал в соответствии со своим видением).
Признаюсь, до поры я относился к теме следования по линии без симпатии. Но, осознав природу своего чувства, я изменил мнение. Почему мне не нравилась эта тема? Дело в том, что результат здесь не достигается как чисто логическое решение задачи, как это происходит в математике или программировании. Если программа (я программист) написана правильно, то она, вообще говоря, будет одинаково выдавать результаты на разных компьютерах, процессорах и операционных системах. Робот, следующий линии - совсем другое дело: небольшое изменение трассы, конструкции робота и даже смена батареек может изменить поведение робота и нарушить замысел. Почему так происходит? Добро пожаловать в физический мир: здесь качество резины на колесах, микрокочка на пути робота, слабый ток батарейки и прочие мелочи, от которых мы абстрагируемся в математических задачах, ставят проблемы, которые с наскока не так просто даже осознать. Программирование - инженерная дисциплина, но она рафинирована почти до математического уровня строгости рукотворной средой с формальными правилами, тогда как следование по линии - это настоящая инженерия в физическом мире. Это даёт задаче большой учебный потенциал и, одновременно, бросает вызов педагогу.
Итак, тема следования по линни - это не изучение соответствующего алгоритма, а привитие навыков решения инженерной задачи.
Инженерная история 1
Братья Райт, коим принадлежит слава первого управляемого пилотом полёта летательного аппарата с двигателем, значительное время занимались изучением управляемости планёра.
Мораль: для решения сложной инженерной задачи нужна хорошая подготовка
Описываемая дальше работа привязана к конкретному полю и конкретному роботу (Maqueen). Естественно, не все результаты будут приложимы к другим роботам и полям, наша цель - показать возможные подходы для решения этого класса задач. Подготовку к разработке алгоритма начнём со сбора начальной информации
Подготовка: сбор начальной информации
Как можно подготовиться к разработке алгоритма следования по линии, что нужно изучить? Естественно, изучить то, что для процесса существенно, это первый вопрос для обсуждения с разработчиками.
Немного подумав, можно прийти к выводу, что нужно
- понимать возможности робота
- понимать особенности трассы.
Возможности робота - это об источниках информации для робота (информация с датчиков следования линии). В нашей ситуации речь идёт о роботе Maqueen, оснащённом парой датчиков для следования линии. Датчики цифровые, т.е. возможен возврат двух значений: 0 и 1 ("цифровые" - такова терминология, происхождение и смысл которой мне не ясны, поскольку 0 и 1 ассоциируются, прежде всего, с булевой алгеброй, а не с цифрой, но ... такова терминология, укоренившаяся в микроконтроллерном мире).
Поскольку документации в коробке с роботом нет,
Задача 1: определить кодировку датчика
Это - несложная задача для самостоятельной работы, не будем останавливаться на деталях.
На роботе Maqueen белый цвет кодируется единичкой. Мы получили первые константы, пора начинать сбор информации в проект "Следование по линии":
Задача 2: определить скорость робота
Задача допускает разные решения (например, можно запустить робота на 10 секунд).
У меня получилась скорость 19.5 см/c на солевых батарейках Flarx. Стоп, батарейки-то уже были какое-то время в работе. Меняем батарейки на новые: 23.5 см/c. Добавлю, что можно попробовать и с другими типами батареек, и результат наверняка будет другой.
Делаем вывод: нельзя жёстко опираться на скорость:
Линейная скорость - не единственная скорость, которая нас интересует. В ситуации, когда робот сбился с курса и должен поправить свой маршрут, он, естественно, не может продолжать прямолинейное движение.
Дополнительное задание: определить угловую скорость робота, когда одно колесо остановлено, а другое двигается.
Задача 3: определить расстояние между датчиками следования линии
У робота Maqueen расстояние между датчиками фиксировано и равно 15мм.
Задача 4. Определить параметры поля для следования по линии
Фирма dfrobot предлагает 2 поля (они напечатаны с разных сторон бумажного листа).
Первое поле воспроизводит по форме беговую дорожку вокруг футбольного поля: 2 параллельных отрезка, соединенных с обоих концов полуокружностями.
Размеры?
- длина по продольной оси (включая ширину линии) - 86 см
- ширина (включая ширину линии) 46 см
- радиус закругления (включая ширину линии) - 23 см
- ширина линии - 15 мм
Определение стратегии следования по линии
Прежде всего, нужно определиться, будем ли мы разрабатывать алгоритм для конкретной трассы, учитывая её параметры, или пытаться разработать универсальный алгоритм для любой замкнутой непересекающейся трассы.
Попытка сразу пытаться написать эффективный алгоритм под конкретную трассу может привести к эффекту "За деревьями леса не видим". Лучше, пожалуй, поработать над общим алгоритмом, а уж после решать, допускает ли он оптимизацию под конкретную трассу.
Отправная точка разработки - определение состояния робота, которое нас устраивает, то есть, не требует коррекции. Конечно, нам хочется, чтобы робот двигался вперёд.
А показания датчиков? Чтобы определиться с этим вопросом, нужно сначала сравнить ширину линии трассы и расстояние между датчиками:
- если расстояние между датчиками больше ширины линии, то можно попытаться держать датчики слева и справа от линии, то есть, ориентироваться на показания "белый И белый"
- если расстояние между датчиками меньше ширины линии, то можно попытаться держать датчики над линией, то есть, ориентироваться на показания "чёрный И чёрный"
- если расстояния равны?
В последнем случае (а это как раз наш случай), очевидно, придётся обеспечивать несимметричное положение робота относительно линии трассы: один датчик должен находиться вне линии ("белый"), а второй - на линии ("черный).
Вариант показания датчиков "белый И чёрный" допускает два расположения робота относительно линии:
- расположение "внутри", когда "белый" датчик находится внутри трассы
- расположение "снаружи", когда "белый" датчик находится снаружи трассы
Что выбрать: прохождение трассы снаружи или внутри? Самоё простое соображение - внутри путь короче. Остановимся для начала на этом решении (но будем помнить, что выбору мы посвятили совсем немного времени, значит, надо оставить за собой право пересмотреть подход позже. Вообще, развилки проекта следует помнить и документировать, чтобы позже иметь возможность проверить альтернативные подходы).
Для определённости будем считать, что "белым" будет левый датчик. Это означает, что испытания мы будем проводить с ездой против часовой стрелки.
Настал момент сформулировать желаемое состояние робота во время прохождения трассы:
- робот движется вперед
- левый датчик показывает "белый", правый - "чёрный"
Если робот находится в этом состоянии, то мы не предпринимаем никаких корректирующих действий и позволяем роботу двигаться дальше.
Как только робот выходит из целевого состояния, необходимо выполнить корректирующие действия.
Мы подошли к важному моменту практически любой программистской задачи: определение, наукоообразно выражаясь, пространства событий, а проще говоря - всех возможных состояний робота. Заметим, что определение пространства событий - важный элемент культуры мышления: прежде чем бросаться на решение сложной задачи, не только учебной или научной, но и житейской, нужно прежде всего определить набор вариантов, который следует рассмотреть.
Состояния робота и структура программы
Состояния можно определить просто: 2 датчика, по два показания у каждого, итого 2 во второй степени, то есть 4. На каждое из 4 состояний мы напишем функцию, и будем вызывать функцию в зависимости от текущей комбинации показаний датчиков.Опрос датчиков и вызов подпрограмм поместим внутрь цикла.
До начала цикла запустим моторы - начнём движение. После завершения цикла нужно остановить моторы. Заодно покажем код завершения. В результате получим следующий шаблон программы:
Вопрос: почему используется бесконечный цикл, а не блок "постоянно"? Дело в том, что блок "постоянно" будет исполняться всегда, не оставляя возможности завершить программу в случае обнаружения неразрешимых проблем (а с такой ситуацией в инженерной задаче нужно считаться). Блок же "пока" работает, пока условие в нем истинно. Поэтому мы задаём переменную код завершения, которую можно изменить позже - внутри вызываемых функций.
Впереди -
Разработка функций
Функция "БелыйЧёрный" - делать ничего не надо
Как мы договорились, это наше главное состояние - здесь делать ничего не надо, функция останется пустой (до поры: если мы сочтём позже нужным что-то делать в желаемом состоянии, - да хоть радостно посигналить, нам не нужно будет переделывать структуру программы).
Из состояния "БелыйЧёрный" робот может перейти в состояние "БелыйБелый", то есть, оказаться внутри трассы или начать движение наружу и перейти в состояние "ЧёрныйЧерный". Состояние "ЧёрныйБелый" будем пока считать запрещённым: из соседнего состояния "ЧёрныйЧёрный" мы должны вернуться в базовое состояние "БелыйЧёрный".
Функция "ЧёрныйБелый" - запретное состояние
Итак, мы запретили это состояние, о чём надо сделать пометку в дневнике проекта, не забывая о возможности пересмотреть это решение в процессе развития программы.
В начальном варианте мы просто выставим флажок окончания работы, то есть, зададим переменной код завершения значение 1. (Единица выбрана не случайно: комбинация чёрный-белый, в числовом виде 01 в двоичной системе и есть 1.)
Получаем следующее:
Это программу можно проверить на корректность завершения работы, поставив робота так, чтобы датчики сразу показали "ЧёрныйБелый".
Теперь приступаем к содержательной части задачи: коррекции курса робота.
Функция "БелыйБелый" - выход из положения внутри трассы
Что делать, если мы оказались внутри трассы? Для начала надо вернуться к линии.
Как? Например, выполняя поворот направо, то есть, остановив правое колесо, продолжить движение до тех пор, пока линия не будет "поймана" датчиками (не забудем занести наше решение в дневник).
Что дальше? Не будем спешить, для начало посмотрим результат возвращения на линию:
После испытания программы мы можем видеть, что курс робота идёт под существенным углом к трассе. Если в этот момент возобновить работу двух моторов, то робот через долю секунды выскочит за пределы трассы. Значит? Нужно скорректировать курс робота. Как? Например, останавливаем левый мотор, запускаем правый.
Как долго нужно корректировать курс. Примерно столько же, сколько робот "ловил" линию.
А как отмерить "примерно столько же"? Заведём счётчик числа обращений к датчикам, чтобы использовать его как меру времени:
Зачем мы опрашиваем датчики во втором цикле? Просто чтобы шаг цикла потреблял примерно такое же время, как шаг первого цикла.
Функции ЧёрныйЧерный и ЧёрныйБелый
После испытаний получившейся версии, пора приступить к коррекции в состоянии ЧёрныйЧёрный. В этой ситуации робот настроен выскочить наружу (вправо). Попробуем подбором найти время коррекции. После нескольких опытов получилось, что вариант с 200 мс (переменная время после поворота) работает более или менее.
Поскольку робот выскакивает наружу, стоит пересмотреть запрет состояния ЧёрныйБелый. Как нетрудно видеть, ситуация в состоянии ЧёрныйЧёрный схожа с ситуацией ЧёрныйБелый: надо немного подрулить влево. Для начала просто используем функцию ЧёрныйЧёрный в состоянии ЧёрныйБелый.
Получившийся вариант заработал: робот достаточно стабильно ходит по трассе как по часовой стрелке (снаружи линии), так и против (внутри линии), и это приятный бонус.
Заключение
Программу можно посмотреть по адресу https://makecode.microbit.org/_5piHMFL4gDo0
Это, конечно, "Аэроплан братьев Райт". Впереди, при желании можно открыть поле совершенствования алгоритма, более широких исследований приложения алгоритма к разным типам трасс, и т.д.
Сложилось впечатление, что подобного размера программа, пожалуй, является пределом работы с блоками: при большом числе блоков достоинства визуального программирования превращаются в недостатки: становится сложно искать изменяемый участок программы, переносы блоков несут опасность их падения "не там", причём отсутствие синтаксиса маскирует поломку. Если мышка слегка барахлит, дело совсем неважно.
Словом, наступает время перехода на традиционные текстовые языки.
Инженерная история 2
Заслуженный и известный в своё время учёный и изобретатель Лэнгли имел щедрый правительственный грант на разработку пилотируемого летательного аппарата. Но история помнит братьев Райт, начинавших в своём гараже.
Мораль: то, что в области поработало немало авторитетов, не значит, что там не осталось места для прорыва. Нужно дерзать!
Приложение 1. Текст программы на JavaScript
function БелыйЧёрный () {
}
function БелыйБелый () {
maqueen.motorStop(maqueen.Motors.M2)
счётчик = 0
правый_датчик_ББ = правый_датчик
левый_датчик_ББ = левый_датчик
while (левый_датчик_ББ == белый && правый_датчик_ББ == белый) {
счётчик += 1
левый_датчик_ББ = maqueen.readPatrol(maqueen.Patrol.PatrolLeft)
правый_датчик_ББ = maqueen.readPatrol(maqueen.Patrol.PatrolRight)
}
maqueen.motorStop(maqueen.Motors.M1)
maqueen.motorRun(maqueen.Motors.M2, maqueen.Dir.CW, 255)
while (счётчик > 0) {
счётчик += -1
левый_датчик_ББ = maqueen.readPatrol(maqueen.Patrol.PatrolLeft)
правый_датчик_ББ = maqueen.readPatrol(maqueen.Patrol.PatrolRight)
}
maqueen.motorRun(maqueen.Motors.M1, maqueen.Dir.CW, 255)
}
function ЧёрныйЧерный () {
maqueen.motorStop(maqueen.Motors.M1)
basic.pause(время_после_поворота)
maqueen.motorRun(maqueen.Motors.M1, maqueen.Dir.CW, 255)
}
function ЧерныйБелый () {
ЧёрныйЧерный()
}
let левый_датчик_ББ = 0
let правый_датчик_ББ = 0
let счётчик = 0
let правый_датчик = 0
let левый_датчик = 0
let время_после_поворота = 0
let белый = 0
белый = 1
let чёрный = 0
let код_завершения = 0
время_после_поворота = 200
maqueen.motorRun(maqueen.Motors.All, maqueen.Dir.CW, 255)
while (код_завершения == 0) {
левый_датчик = maqueen.readPatrol(maqueen.Patrol.PatrolLeft)
правый_датчик = maqueen.readPatrol(maqueen.Patrol.PatrolRight)
if (левый_датчик == белый && правый_датчик == белый) {
БелыйБелый()
}
if (левый_датчик == чёрный && правый_датчик == чёрный) {
ЧёрныйЧерный()
}
if (левый_датчик == чёрный && правый_датчик == белый) {
ЧерныйБелый()
}
if (левый_датчик == белый && правый_датчик == чёрный) {
БелыйЧёрный()
}
}
maqueen.motorStop(maqueen.Motors.All)
basic.showNumber(код_завершения)