11 березня 2017 р.

16. Інфрачервоні сенсори відстані

Апаратна частина

Зазнавши невдачі з ультразвуковими сенсорами відстані, ми засіли за креслярські ватмани гугл, щоб все ж знайти для нашого робота надійний спосіб орієнтування на місцевості і розпізнавання перешкод на шляху.

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

Справжньою знахідкою виявився набір інфрачервоних сенсорів, які так і називаються "інфрачервоні сенсори для уникання перешкод роботами на основі Ардуіно". За три з половиною долари пропонують аж 5 штук - цілком достатньо для початку.
Infrared Obstacle Avoidance Sensor For Arduino Smart Car Robot


Сенсор побудований дуже просто. На перші два контакти йому треба подати плюс (VCC) та мінус (GND) живлення. На третьому контакті (OUT) - сенсор виставляє високий рівень напруги, якщо бачить перед собою перешкоду, і низький рівень напруги, якщо не бачить. Чутливість сенсора можна змінювати підкручуючи резистор.

Експерименти показали, що сенсори без проблем працюють поряд, але для достовірності показів таки варто їх один від одного мінімально ізолювати чимось непрозорим.



Ми вирішили почепити спереду чотири сенсори. Пара має дивитися вниз і пильнувати чи не наближається робот до прірви. Друга пара має дивитися вперед і помічати перешкоди. Причому, перешкоди треба помічати навіть невисокі, тому сенсори повинні стояти якомога нижче.

Шасі робота не передбачає монтажу сенсорів такого типу. Довелося зробити зі шматків кредитних карток спеціальні консолі і причепити їх спереду. Для красивішого вигляду картки обклеїли білим самоклейним оракалом.

Консоль для монтажу сенсорів


Розміри конструкції підбиралися експериментальним шляхом. Отвори під болти випалили розпеченим на газі дротом. Для надійної фіксації сенсорів ми їх приклеїли на двосторонній скотч. Крім того сенсор фіксується в додаткових невеликих отворах в які входять шпильки трьох тильних контактів.

Вцілому - вийшло досить непогано. Консолі виявилися міцними, захищають сенсори від ударів а також екранують їх один від одного.

Інфрачервоні сенсори - вид знизу
Інфрачервоні сенсори - вид спереду


Перший експеримент пройшов не зовсім вдало.



Виявилося, що зорієнтувавши сенсори прірви чітко по вертикалі, ми не дали шансів роботу помічати краї прірви завчасно. Програма робота просто не встигала зреагувати і зупинити робота.

Тому довелося випалювати нові дірки в консолях і відхилити промені сенсорів трохи вперед. На фото нижче видно що з цього вийшло.

Консоль для кріплення сенсорів - остаточна версія

Отже, вивіривши місце і налаштувавши чутливість кожного сенсора, ми приступили до переписування програми робота.

Детектування прірв і ям


Для початку - треба було навчити робота не падати у глибокі прірви.

Оскільки уникнення падіння це справа досить термінова - ми використали систему переривань. Як тільки якийсь сенсор помічає прірву перед гусеницями - виконання програми зупиняється на апаратному рівні, і модуль штучного інтелекту повідомляється про потребу негайної реакції. (Якщо не зовсім розумієте як працюють переривання - подивіться відеоурок Джеремі Блума (англійською або російською)).

Робота з перериваннями відбувається у файлі robot.ino.

Для початку - визначимося, до яких виходів Arduino підключити сенсори.
const uint8_t ABYSS_RIGHT_PIN = 20;//правий сенсор підключений до виходу #20
const uint8_t ABYSS_LEFT_PIN = 21; //лівий сенсор підключений до виходу #21
Ці номери виходів вибрані не випадково. Серед усіх виходів лише на деякі можна вішати обробники переривань. Для Arduino MEGA 2560 це виходи 2, 3, 18, 19, 20 та 21 (див. офіційну документацію про переривання).

Наступний крок - реєструємо функцію abyssDetected() як обробник переривань від сенсорів. Щоб не плутатися між номерами використаних виходів і номерами переривань, які ними генеруються - краще покластися на функцію digitalPinToInterrupt(). Бо інакше довелося би ритися в документації і шукати ті відповідності вручну.
attachInterrupt(digitalPinToInterrupt(ABYSS_LEFT_PIN), 
                abyssDetected, FALLING);
attachInterrupt(digitalPinToInterrupt(ABYSS_RIGHT_PIN), 
                abyssDetected, FALLING);

Як тільки на контакті правого або лівого сенсора прірви рівень напруги впаде з високого рівня до нуля (сенсор помітить прірву) - буде запущено функцію abyssDetected(). Звісно ж, було б гарніше весь цей код перенести в модуль штучного інтелекту, але деякі технічні особливості дуже ускладнюють реалізацію цієї ідеї (обробник переривання має бути функцією, яка не приймає жодних параметрів на вхід, а у всі методи класів неявно передається вказівник на екземпляр того класу...  одним словом не переймайтеся, наш код працює - от і чудово).

Все, що робить сам обробник - повідомляє модуль штучного інтелекту що перед роботом прірва.
void abyssDetected() {
 robotAI->abyssDetectedFlag = true;
}

Вже на наступному такті виконання - штучний інтелект зупинить робота, а далі буде розбиратися що з цим робити далі (RobotAI.cpp).

void RobotAI::processTask() {
 // перевіряємо, чи бува не натрапив робот на прірву
 if (abyssDetectedFlag) {
  // якщо ми вже почали опрацювання прірви
  // в одному з попередніх викликів processTask()
  // то цього разу можемо пропустити
  if(!abyssDetectedProcessing) {
   // на всяк випадок перепитуємо сенсори 
   // чи дійсно вони бачать прірву
   if(robotDistanceSensor->getFrontAbyssDetected()) {
    // зупиняємо двигуни
    robotMotors->fullStop();
    // пищимо наляканим голосом
    robotVoice->queueSound(sndScared);
    // запускаємо режим відповзання
    abyssDetectedProcessing = true;
   }
  }
  abyssDetectedFlag = false;
 }
...

Пізніше в коді в режимі вільного блукання, якщо було помічено перешкоду - робот має 900 мілісекунд відповзати назад, а потім поводитися так, ніби наштовхнувся на якусь перешкоду. Скоріш за все робот успішно відверне від прірви і поїде в іншому напрямку:

if(abyssDetectedProcessing) {
 // 900 мілісекунд заднього ходу
 robotMotors->driveBackward(MOTOR_DRIVE_SPEED, 900);
 // чекаємо секунду (із запасом, для надійності)
 scheduleTimedTask(1000);
 // діємо так, ніби перед нами просто була собі якась перешкода
 currentAIState = stateAI_QueryDistances;
}
...

В результаті це виглядає десь так:



Вільне блукання по кімнаті - версія 2


З новими сенсорами рухатися по кімнаті стало значно простіше. Щоби не загромаджувати блок штучного інтелекту особливостями роботи матриці сенсорів, весь код по читанню стану сенсорів винесено в модуль RobotDistanceSensor.cpp. Штучний інтелект просто питає в сенсорів "Чи є попереду перешкода?" (виклик isObstacleInFront()). Якщо перешкода є - дає наступне питання "В якому напрямку перешкода?" (виклик getObstacleDirection()). Ну а знаючи вже де перешкода, легко прийняти рішення в яку сторону повернути, щоб її об'їхати - тут код повністю зберігся без змін.

Ці дві функції рознесено окремо, бо isObstacleInFront() ніколи не використовує бічні виміри ультразвукового сенсора - їй нема потреби махати головою. Тому вона в більшості випадків повертає результат блискавично, хоча використовує лише "швидкі" сенсори.

getObstacleDirection() навпаки орієнтована на точність і виваженість. Вона вичитує значення всіх сенсорів, а також змушує робота покрутити головою навколо, вишуковуючи перешкоди всіма можливими способами.

Окремо слід зазначити те, що модуль сенсорів не завжди може чітко сказати де перешкода. Ми все ще використовуємо ультразвуковий сенсор і все ще обертаємо його в різні сторони, щоб подивитися правіше і лівіше. Тому крім варіантів відповіді на питання "де перешкода?" крім "справа", "зліва" чи "і там, і там", сенсори можуть відповісти "невідомо де". В такому випадку модуль штучного інтелекту має зачекати трохи (ми чекаємо 3 секунди) і перепитати про перешкоди знову.

Код штучного інтелекту досить громіздкий вийшов, але все ж нескладний. Тому не будемо його тут наводити.

Краще подивимося як модуль сенсорів визначає з якого боку є перешкода (метод getObstacleDirection()):

// цей метод повертає специфічні коди, які вказують де є перешкода
// odLEFT - перешкода зліва
// odRIGHT - перешкода справа
// odBOTH - перешкоду видно і зліва, і справа
// odNONE - перешкод не видно ніяких
// odUNKNOWN - невідомо - модуль ще не встиг виміряти всі відстані
ObstacleDirections RobotDistanceSensor::getObstacleDirection() {
 // перш за все - перевіряємо сенсори прірви
 // інформація з них має найвищий пріорітет
 bool leftFlag = getFrontLeftAbyssDetected();
 bool rightFlag = getFrontRightAbyssDetected();

 if(leftFlag) {
  if(rightFlag) {
   return odBOTH;
  } else {
   return odLEFT;
  }
 } else {
  if(rightFlag) {
   return odRIGHT;
  }
 }

 // те, що ми тут, означає, що сенсори прірви
 // небезпек не помітили
 // перевіряємо тепер що бачать інфрачервоні сенсори перешкод
 // якщо вони бачать перешкоди - нема потреби 
 // ганяти ультразвуковий сенсор
 leftFlag = getFrontLeftIRDetected();
 rightFlag = getFrontRightIRDetected();

 if(leftFlag) {
  if(rightFlag) {
   return odBOTH;
  } else {
   return odLEFT;
  }
 } else {
  if(rightFlag) {
   return odRIGHT;
  }
 }

 // те, що ми тут, означає, що жоден інфрачервоний сенсор
 // не побачив попереду нічого небезпечного
 // нехай вже тоді попрацює ультразвуковий сенсор
 if((lastFLDistance == -1) || (lastFRDistance ==-1)) {
  // бокові відстані не готові, треба дочекатися
  // поки вони будуть переміряні
  return odUNKNOWN;
 } else {
  if(lastFLDistance == lastFRDistance) {
   return odBOTH;
  } else {
   if(lastFLDistance < lastFRDistance) {
    return odLEFT;
   } else {
    return odRIGHT;
   }
  }
 }

 // те, що ми тут, означає, що жоден сенсор
 // жодних перешкод не побачив
 return odNONE;
}

Ну і остання зміна, яку ми внесли в код програми - довелося змінити спосіб, яким робот здійснює об'їзд перешкод. Раніше для виконання повороту ми використовували одночасне обертання обидвох гусениць - так поворот виконується набагато швидше, але робот надто сильно навалювався на перешкоду і частенько в неї впирався гусеницями:

// поворот вправо - агресивний стиль
// ліва гусениця тягне вперед, права - назад
runDrives(speed, duration, FORWARD, BACKWARD);

Тепер ми виконуємо поворот лише за рахунок заднього ходу однієї із гусениць. Таким чином робот витримує дистанцію до перешкоди, хоча і здійснює поворот повільніше.

// поворот вправо - поміркований стиль
// ліва гусениця стоїть на місці, права - тягне назад
runDrives(speed, duration, RELEASE, BACKWARD);

Робот почав поводити себе дуже навіть пристойно. Він не наштовхується на перешкоди як раніше, проявляє ознаки інтелекту коли пробує їх обійти, і в принципі справляє набагато краще враження.



Звісно ж - для повноцінної навігації в приміщеннях, такого примітивного коду явно недостатньо. Робот може зациклитися і вічно плутати в трьох соснах. Робот все ще може встрягнути в якусь халепу, якщо конфігурація перешкод підловить його на якійсь слабині алгоритму.

Тепер стало очевидним, що блукання нашого робота є хаотичними і безцільними. Коли ми знову повернемося до питань штучного інтелекту і навігації - мабуть вже варто буде попрацювати з прокладанням реальних маршрутів, будуванням карти місцевості, розпізнавання зображень і всякими іншими цікавостями, якими щедро розбавлені сучасні інтелектуальні пристрої.

Немає коментарів:

Дописати коментар