Бутылочный Bluetooth-катер

  • Платформы: Strela
  • Языки программирования: Arduino (C++)
  • Тэги: лодка, катер, бутылка, утки, корм.

Что это?

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

Что нам понадобится?

  1. 4× винт М1.6 для крепления моторов
  2. NiMH-аккумулятор, подойдёт например от радиоуправляемой игрушки
  3. Телефон на базе ОС Android
  4. Многожильный монтажный провод с сечением не менее 1 мм² (2 шт. разного цвета)
  5. Одножильный монтажный провод c сечением не менее 4 мм²
  6. Крепёжные элементы: двусторонняя клейкая лента, болты, гайками, шайбы.
  7. Набор пластиковых бутылок разной формы и ёмкости
  8. Корм для уток, в нашем случае сушки

Как собрать?

  1. Вставьте модуль Bluetooth Bee в специальный разъём форм-фактора Xbee на платформе Strela.
  2. Возьмите 2 коллекторных мотора, припаяйте к их контактным площадкам провода и подключите к платформе Strela через специальные клеммники для подключение моторов. Полярность тут не важна, если вдруг мотор будет вращаться в другую сторону, это можно будет исправить программно.
  3. Перейдём к изготовлению корпуса катера:
    • Возьмите 1 большую бутылку, вырежьте в ней сверху что то наподобие люка и через него вставьте в носовую часть бутылки аккумулятор.
    • В задней центральной части с помощью маленьких винтов установите коллекторные моторы по бокам бутылки, так чтобы они находились внутри бутылки, а вал выходил наружу.
    • Возьмите ещё 2 средних по размеру бутылки отрежьте у них донышко и через специальные втулки закрепите их к валу каждого из моторов. Также вырежьте в них лопасти, чтоб нашему катеру было легче передвигаться по воде.
    • Подключите платформу Strela к компьютеру, прошейте скетч, приведённый ниже, и подключите к ней аккумулятор через клеммник PWR, соблюдая полярность.
    • В таком состоянии удобно откалибровать скорость вращения моторов. Про калибровку читайте ниже.
  4. Теперь подключите сервопривод постоянного вращения через 3-проводной шлейф в разъём P1. В итоге должна получиться схема, как на рисунке ниже.
  5. Установите платформу Strela в носовой части бутылки сверху на аккумулятор. Закрепите сервопривод через соединительную скобу в верхней центральной части бутылки. Возьмите толстый одножильный провод, скрутите его в спираль и, используя втулку, прикрутите к сервоприводу.
  6. Так как носовая часть катера самая тяжелая, нам понадобятся дополнительные поплавки, чтобы удерживать её над поверхностью воды. Для этого возьмите две маленьких бутылки и примотайте их по бокам скотчем в передней части большой бутылки. Для защиты устройства кормления от брызг возьмите среднюю бутылку, отрежьте донышко и оденьте её на манер чехла, чтобы сервопривод оказался внутри бутылки. Убедитесь, что ничего не мешает спирали свободно вращаться.
  7. Теперь можно смело ставить катер на воду, загружать его вкусняшками и отправлять в плаванье. Если катер при движении клонит в сторону, повторите процесс калибровки.

Калибровка: настройка скорости вращения моторов

  • Сразу при подаче питания удерживайте кнопку S1, должен пикнуть 3 раза зуммер, зажечься 2 и 3 светодиод, моторы начнут вращаться.
    • Для выхода из режима калибровки без сохранения снова нажмите на кнопку S1.
  • Кнопками S2 и S3 отрегулируйте скорость вращения обоих моторов, чтобы они вращались с одинаковой частотой.
  • Нажмите кнопку S4 для сохранения результатов в энергонезависимую память. После чего зуммер снова пикнет 3 раза, светодиоды 2 и 3 погаснут, а светодиоды 1 и 4 в свою очередь зажгутся, моторы остановятся.

Настройка управления

  • Для Android существует огромное количество приложений, при помощи которых можно управлять Arduino при помощи Bluetooth. В данном случае для управления Strela мы использовали Arduino Bluetooth RC Car.
  • Интерфейс приложения напоминает джойстик от игровой консоли. Зелёный индикатор в верхнем левом углу сигнализирует о том, что мы соединены с Bluetooth устройством. Если же горит красный индикатор, зайдите в меню «Настройки», выполните поиск Bluetooth устройств и подключитесь к Bluetooth Bee. При первом запуске возможен запрос пароля, по умолчанию «1234».
  • В управлении моторов всё интуитивно понятно (вперёд, назад, влево, вправо). В правом верхнем углу есть ползунок настройки скорости моторов. По нажатию на кнопку Δ сервопривод начнёт вращаться и утиные угощения посыпятся в воду. Повторное нажатие остановит процесс. Также можно посигналить уткам с помощью зуммера, нажав на кнопку с изображением пищалки.

Алгоритм

  • Сразу после подачи питания программа проверяет было ли что-либо записано в энергонезависимую память.
    • если да, то считываем калибровочные значения скорости вращения моторов из энергонезависимой памяти.
  • Моторы вращаются не синхронно?
    • если да, то производим калибровку скорости вращения моторов относительно друг друга.
  • Находим поправочный коэффициент, как отношение значений скоростей одного мотора к другому, при которых оба мотора вращаются с одинаковой скоростью.
  • Если есть данные, которые приходят через Bluetooth, выполняем команду, пришедшую со смартфона.

Исходный код

bottleboat.ino
// библиотека для работы с платформой Strela
#include <Strela.h>
 
// библиотека для работы с I2C-расширителем портов
#include <Wire.h>
 
// EEPROM — энергонезависимая память
// библиотека для записи и считывания информации с EEPROM
#include <EEPROM.h>
 
// библиотека для работы с сервоприводами
#include <Servo.h>
 
// создадим объект для управления сервоприводом
Servo myservo;
 
// это число мы будем использовать в логике поворотов
int defaultSpeed = 100;
 
// w1 и w2 - это скорость вращения первого и второго мотора
// скорость регулируется в пределах от -255 до 255
// если это число положительное - мотор будет вращаться вперёд
// если отрицательное - назад
// если баланс скорости вращения моторов не бы совершен
// по умолчанию равны 100
int w1 = 100;
int w2 = 100;
 
// значение поправочного коефициента
// скорости одного мотора к другому
float k = 1;
 
// переменная хранит контрольную сумму
// проверка на то, было ли что нибудь записано в EEPROM
int sum;
 
// переменные состояния каждой из 4 кнопок
// была ли кнопка отпущена?
boolean button1WasUp = true;
boolean button2WasUp = true;
boolean button3WasUp = true;
boolean button4WasUp = true;
 
void setup()
{
  // я неправильно прикрутил один мотор
  // поэтому, чтобы их не перекручивать
  // можно воспользоваться этой функцией.
  // направление вращения мотора 2 будет изменено.
  motorConnection(0, 1);
 
  // открываем последовательный порт со скоростью 9600 бод
  Serial.begin(9600);
  // Bluetooth Bee по умолчанию использует скорость 9600 бод
  Serial1.begin(9600);
 
  // пикнем зуммером с частотой 1000 Гц, 100 мс
  tone(BUZZER, 1000, 100);
  delay(500);
 
  // считываем значение из 4 ячейки памяти EEPROM
  sum = EEPROMReadInt(4);
 
  // записывали ли мы в EEPROM значение баланса скоростей
  if (sum == 777) {
    // чтение из памяти значение баланса скоростей
    w1 = EEPROMReadInt(0);
    w2 = EEPROMReadInt(2);
  }
 
  delay(100);
  // нажата ли кнопка S1
  // вход в меню настройки баланса скорости моторов
  if (uDigitalRead(S1)) {
    // пищим 3 раза зуммером
    tone(BUZZER, 500, 50);
    delay(300);
    tone(BUZZER, 500, 50);
    delay(300);
    tone(BUZZER, 500, 50);
    delay(300);
 
    // вызываем функцию баланса скорости моторов
    balanceMotors();
  }
 
  // зажгём первый и четвёртый светодиод
  uDigitalWrite(L1, HIGH);
  uDigitalWrite(L4, HIGH);
 
  // вызываем функцию нахождение поправочного коефициента
  // скорости одного мотора к другому
  correction();
}
 
void loop()
{
  // если появились новые команды
  // вызываем функцию управления
  if (Serial1.available() > 0) {
    control();
  }
  // вывод скоростей
  serialPrint();
}
 
// функция настройки баланса скорости моторов
void balanceMotors()
{
  while (1) {
    // зажгём второй и третий светодиод
    uDigitalWrite(L2, HIGH);
    uDigitalWrite(L3, HIGH);
 
    // если левое колесо (мотор 1) медленнее правого (мотор 2)
 
    // нам нужно определить клик кнопки
    // определить момент «клика» несколько сложнее, чем факт того,
    // что кнопка сейчас просто нажата. Для определения клика мы
    // сначала понимаем, отпущена ли кнопка прямо сейчас
    boolean button2IsUp = uDigitalRead(S2);
 
    // если кнопка была отпущена и не отпущена сейчас
    // и значение первого мотора менее 255
    if (!button2WasUp && button2IsUp && w1 < 255) {
      // может это «клик», а может и ложный сигнал (дребезг),
      // возникающий в момент замыкания/размыкания пластин кнопки,
      // поэтому даём кнопке полностью «успокоиться»
      delay(10);
      // и считываем сигнал снова
      button2IsUp = uDigitalRead(S2);
        // если она всё ещё нажата, значит это клик!
      if (button2IsUp) {
        // Скорость первого мотора увеличиваем, а второго уменьшаем
        w1++;
        w2--;
      }
    }
 
    // запоминаем последнее состояние кнопки для новой итерации
    button2WasUp = button2IsUp;
 
    // если правое колесо (мотор 2) медленнее левого (мотор 1)
 
    // нам нужно определить клик кнопки
    // определить момент «клика» несколько сложнее, чем факт того,
    // что кнопка сейчас просто нажата. Для определения клика мы
    // сначала понимаем, отпущена ли кнопка прямо сейчас
    boolean button4IsUp = uDigitalRead(S4);
 
    // если кнопка была отпущена и не отпущена сейчас
    // и значение второго мотора менее 255
    if (!button4WasUp && button4IsUp && w2 < 255) {
      // может это «клик», а может и ложный сигнал (дребезг),
      // возникающий в момент замыкания/размыкания пластин кнопки,
      // поэтому даём кнопке полностью «успокоиться»
      delay(10);
      // и считываем сигнал снова
      button4IsUp = uDigitalRead(S4);
      // если она всё ещё нажата, значит это клик!
      if (button4IsUp) {
        // Скорость второго мотора увеличиваем, а первого уменьшаем
        w1--;
        w2++;
      }
    }
 
    // запоминаем последнее состояние кнопки для новой итерации
    button4WasUp = button4IsUp;
 
    // Индикация увеличение скорости первого мотора
    if (uDigitalRead(S2)) {
      uDigitalWrite(L4, HIGH);
    } else {
      uDigitalWrite(L4, LOW);
    }
 
    // Индикация увеличение скорости второго мотора
    if (uDigitalRead(S4)) {
      uDigitalWrite(L1, HIGH);
    } else {
      uDigitalWrite(L1, LOW);
    }
 
    // вывод скоростей
    serialPrint();
 
    // ход по значениям скоростей w1 и w2
    drive(w1, w2);
 
    // если нажата кнопка S1
    // пишем CANCEL в Serial
    // и выходим из бесконечного цикла while(1) без сохранения
    if (!button1WasUp && uDigitalRead(S1)) {
      Serial.println("CANCEL");
      break;
    }
    button1WasUp = uDigitalRead(S1);
 
    // если нажата кнопка S3
    if (!button3WasUp && uDigitalRead(S3)) {
      // сохраняем значение первого мотора
      EEPROMWriteInt(0, w1);
      // сохраняем значение второго мотора
      EEPROMWriteInt(2, w2);
      // сохраняем значение контрольной суммы
      EEPROMWriteInt(4, 777);
      // Пишем SAVE в Serial и выходим из бесконечно цикла
      Serial.println("SAVE");
      break;
    }
    button3WasUp = uDigitalRead(S3);
  }  /// while (1)
 
  // останавливаем моторы
  drive(0, 0);
 
  // погасим второй и третий светодиод
  uDigitalWrite(L2, LOW);
  uDigitalWrite(L3, LOW);
 
  // пикнем 3 раза зуммером
  tone(BUZZER, 1000, 50);
  delay(100);
  tone(BUZZER, 1000, 50);
  delay(100);
  tone(BUZZER, 1000, 50);
  delay(100);
}  /// balanceMotors
 
//запись двухбайтового числа в память
void EEPROMWriteInt(int address, int value)
{
  EEPROM.write(address, lowByte(value));
  EEPROM.write(address + 1, highByte(value));
}
 
//чтение двухбайтового из числа из памяти
unsigned int EEPROMReadInt(int address)
{
  byte lowByte = EEPROM.read(address);
  byte highByte = EEPROM.read(address + 1);
  return (highByte << 8) | lowByte;
}
 
void serialPrint()
{
  Serial.print("speed  w1 = ");
  Serial.print(w1);
  Serial.print("    ");
  Serial.print("speed  w2 = ");
  Serial.println(w2);
}
 
 
// пуск сервопривода постоянного вращения
void servoStart(void)
{
  myservo.attach(11);
  myservo.write(0);
}
 
// остановка сервопривода
void servoStop(void)
{
  // Самый простой способ остановить серву постоянного вращения
  // отсоединиться от неё
  myservo.detach();
}
 
void control()  // функция управления
{
  // считаем значение пришедшей команды
  char dataIn = Serial1.read();
 
  if (dataIn == 'F') {
    // пришла команда "F", едем вперёд
    drive(w1, w2);
  } else if (dataIn == 'B') {
    // пришла команда "B", едем назад
    drive(-w1, -w2);
  } else if (dataIn == 'L') {
    // пришла команда "L", поворачиваем налево на месте
    drive(-w1, w2);
  } else if (dataIn == 'R') {
  // пришла команда "R", поворачиваем направо на месте
    drive(w1, -w2);
  } else if (dataIn == 'I') {
    // пришла команда "I", едем вперёд и направо
    drive(defaultSpeed + w1, defaultSpeed - w2);
  } else if (dataIn == 'J') {
    // пришла команда "J", едем назад и направо
    drive(-defaultSpeed - w1, -defaultSpeed + w2);
  } else if (dataIn == 'G') {
    // пришла команда "G", едем вперёд и налево
    drive(defaultSpeed - w1, defaultSpeed + w2);
  } else if (dataIn == 'H') {
    // пришла команда "H", едем назад и налево
    drive(-defaultSpeed + w1, -defaultSpeed - w2);
  } else if (dataIn == 'S') {
    // если пришла команда "S", стоим на месте
    drive(0, 0);
  } else if (dataIn == 'X') {
    // пришла команда "X", крутим серву
    servoStart();
  } else if (dataIn == 'x') {
    // пришла команда "x", останавливаем серву
    servoStop();
  } else if (dataIn == 'V') {
    // пришла команда "V", пищим
    tone(BUZZER, 1000);
  } else if (dataIn == 'v') {
    // пришла команда "v", не пищим
    noTone(BUZZER);
  } else if (((dataIn - '0') >= 0) && ((dataIn - '0') <= 9)) {
    // настройка скорости вращения обоих моторов от 0 до 9
    // если первый мотор быстрее
    if (w1 > w2) {
      // сохраняем новое значение скорости обоих моторов
      // второй с поправкой на баланс
      w1 = (dataIn - '0') * 25;
      w2 = (dataIn - '0') * 25*k;
    } else {
      // сохраняем новое значение скорости обоих моторов
      // первый с поправкой на баланс
      w1 = (dataIn - '0') * 25*k;
      w2 = (dataIn - '0') * 25;
    }
  } else if (dataIn == 'q') {
    // если "q" - полный газ
    if (w1 > w2) {
      // первый мотор максимум, второй с поправкой на баланс
      w1 = 255;
      w2 = 255*k;
    } else {
      // второй мотор максимум, первый с поправкой на баланс
      w1 = 255*k;
      w2 = 255;
    }
  }
}  /// end control
 
// функция нахождения поправочного коефициента
// скорости одного мотора к другому
void correction()
{
  float m1 = w1;
  float m2 = w2;
  if (m1 > m2) {
    k = m2 / m1;
  } else {
    k = m1 / m2;
  }
}

Демонстрация работы устройства

Что дальше?

Если у вас вдруг не оказалось платформы Strela, вы можете использовать, к примеру, Arduino Uno, добавить к ней Motor Shield и, немного изменив скетч, управлять моторами. А плата расширения Wireless Shield позволит вам подключить Bluetooth Bee.

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