воскресенье, 20 ноября 2016 г.

Проект выходного дня "Поющее ведро"

Вот уже много лет в самом укромном и скромном по своей площади месте в нашей квартире стоит старенький аналоговый радиоприемник. Стоит неприметно, на шкафу. Но стоит только войти в это маленькое, но часто посещаемое помещение, как он оживает и радует посетителя музыкой, обычно это "Радио на 7 холмах". Устроено все очень просто: свет включается автоматически, с использованием светильника со встроенным датчиком движения.


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

И вот тут-то и попался на глаза второй динамик, оставшийся от нашего большого железного робота. Решено было сделать незатейливую замену отжившему свое устройству, способную продолжить радовать нас музыкой в автоматическом режиме.

Итак динамик, Supra SSB-5, автомобильный, пять дюймов, 40-140 Вт.
В качестве воспроизводящего устройства мы будем использовать DFPlayer Mini, он имеет на борту 3Вт усилитель и поддерживает microSD карты объемом до 32 Гб. 
Казалось бы всего 3 Вт усилитель и такой мощный динамик - это бред, но практика показало обратное. Звучит эта связка вполне приятно, громкости для задуманного сценария использования хватает с избытком, а динамик выдает довольно сочный бас, в отличии от полуваттного спикера в старом радио.
Чтобы добавить дополнительную функциональность нашему устройству, воспользуемся Arduino Pro Mini (можно использовать любой аналог). 


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


Питаться устройство будет от USB, в качестве источника питания мы использовали зарядку мобильного телефона, 5В 1А. Общая схема соединения всех компонентов такова:


Для работы с DFPlayer Mini потребуются библиотеки DFPlayer library V2.0 и SoftwareSerial. Для работы с часами DS1302 также потребуется соответствующая библиотека.

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



В качестве корпуса мы использовали ведерко из под шпатлевки, вышло довольно мило и герметично.



Код в скетча для Arduino у нас такой:

#include <SoftwareSerial.h>
#include <DFPlayer_Mini_Mp3.h>
#include <stdio.h>
#include <DS1302.h>

namespace {

const int kCePin   = 7;  // Chip Enable
const int kIoPin   = 6;  // Input/Output
const int kSclkPin = 5;  // Serial Clock

DS1302 rtc(kCePin, kIoPin, kSclkPin);

}  // namespace


SoftwareSerial mySerial(10, 11); // RX, TX

void setup () {
Serial.begin (9600);
mySerial.begin (9600);
mp3_set_serial (mySerial);
        randomSeed(analogRead(4));
delay(300);  //wait 1ms for mp3 module to set volume
        
        Time t = rtc.time();

        // ночь, громкость на минимум, бас убираем
        if (t.hr < 5) {
          mp3_set_EQ (0);
          delay(100);          
 mp3_set_volume (10);
          delay(100);
 mp3_play (1);
        }

        // поздний вечер
        if (t.hr >= 22) {
          mp3_set_EQ (2);
          delay(100);          
 mp3_set_volume (15);
          delay(100);
 mp3_play (4);
        }

        // раннее утро
        if (t.hr >= 5 && t.hr < 8) {
          mp3_set_EQ (2);
          delay(100);
          mp3_set_volume (15);
          delay(100);
 mp3_play (2);
        }

        // позднее утро
        if (t.hr >= 8 && t.hr < 11) {
          mp3_set_EQ (2);
          delay(100);          
 mp3_set_volume (18);
          delay(100);
 mp3_play (2);
        }

        // вечер
        if (t.hr >= 20 && t.hr < 22) {
          mp3_set_EQ (2);
          delay(100);          
 mp3_set_volume (20);
          delay(100);
 mp3_play (4);
        }

        // день
        if (t.hr >= 11 && t.hr < 17) {
          mp3_set_EQ (2);
          delay(100);          
 mp3_set_volume (22);
          delay(100);
 mp3_play (3);
        }

        // ранний вечер
        if (t.hr >= 17 && t.hr < 20) {
          mp3_set_EQ (2);
          delay(100);          
 mp3_set_volume (20);
          delay(100);
 mp3_play (4);          
        }

        delay(500);
        while (analogRead(A0) < 430) {
          delay(100);    
        }          
}

void loop () {         
mp3_play (int(random(5,1024)));
        delay(100);
        while (analogRead(A0) < 430) {
          delay(100);    
        }  
}

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

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


Если вы захотите повторить наш проект, то есть вариант не завязываться на внешний датчик движения, в корпус можно встроить модуль для Arduino, способный определять движение в ИК-диапазоне:

Из дополнительных возможностей, которые в скором времени уже хочется добавить:
  • Плавное повышение громкости с 0 до разрешенного в данный временной промежуток значения
  • Разная музыка в разное время суток, ночью - только спокойная классика, никакого металла!
  • Ваш вариант?


суббота, 12 ноября 2016 г.

Решение задач № 17 и 20


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


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

Программа для решения задачи на языке NXC:

// Создаем переменные
float x,d;
int speed,es,D1,D2,e,u,PD,Pk;

task main()
{
  // Инициализируем датчики
  SetSensorLight(IN_2); // Датчик освещенности слева 2й порт
  SetSensorLight(IN_3); // Датчик освещенности справа 3й порт
  SetSensorLowspeed(IN_4); // Ультразвуковой датчик - 4й порт

  x = 180; // Ширина колеи
  d = 43.2; // Диаметр колес робота
  speed = 70; // Средняя скорость 
  es = 0; // "старая" ошибка ПД - регулятора
  Pk = 2; // Пропорциональный коэф-т ПД-регулятора
  PD = 4; // Дифференциальный коэф-т ПД-регулятора
  
  OnFwdSync(OUT_BC, 20, -100); // разворачиваемся в поисках банки
  while(SensorUS(IN_4) >= 50); // ожидаем банку
  while (SensorUS(IN_4) <= 50); // ожидаем потерю банки
  Off(OUT_BC); // останавливаем моторы
  // разворачиваемся для выезда рядом с банкой
  RotateMotorEx(OUT_BC, speed, x/d*85, -  100, true, true);
  Wait(50);
  OnFwdSyncEx(OUT_BC, 50, 0,RESET_NONE);
  // едем вперед пока не обнаружена линия
  while(Sensor(IN_3) >= 44 && Sensor(IN_2) >= 40.5);
  Off(OUT_BC); // останавливаемся
  Wait(50);
  // немного вперед, чтобы развернуться над линией
  RotateMotorEx( OUT_BC , speed , 200 , 0 , true , true );
  // поворачиваем вправо, УЗ-датчик смотрит в круг
  RotateMotorEx(OUT_BC , speed , x / d * 75, -100, true , true );
  // цикл для 4 банок
  for(int i=0; i<4; i++)
  {
    // пока нет банки - едем по линии на ПД-регуляторе
    while(SensorUS(IN_4) >=10)
    {
      // показания с датчиков
      D1 = Sensor(IN_3);
      D2 = Sensor(IN_2);
      // ошибка регулятора - разница показаний
      e = D2-D1;
      // управляющее воздействие по формуле ПД-регулятора
      u = e*Pk+PD*(e-es);
      // рассчитанную мощность подаем на моторы
      OnFwd(OUT_B,u+speed);
      OnFwd(OUT_C,-u+speed);
      // сохраняем ошибку для следующей итерации цикла
      es = e;
    }
    // останавливаемся при обнаружении банки
    Off(OUT_BC);
    // поворачиваемся к банке
    RotateMotorEx(OUT_BC , speed , x / d * 95, -100, true , true );
    Wait(50);
    // заталкиваем банку в центр круга
    RotateMotorEx( OUT_BC , speed , 1100 , 0 , true , true );
    Wait(50);
    // возвращаемся на линию
    RotateMotorEx( OUT_BC , -1*speed , 1100 , 0 , true , true );
    // разворачиваемся влево, чтобы продолжить движение
    RotateMotorEx(OUT_BC , speed , x / d * 87, 100, true , true );
  }
}

// создаем много переменных
int B_enkoder,C_enkoder,D1,D2,trigger;
bool out;
int e,u,Pk,PD,speed,es;
float d,x;

task main()
{
  // инициализируем датчики освещенности
  SetSensorLight(IN_2); 
  SetSensorLight(IN_3);

  x = 180; // ширина колеи
  d = 43.2; // Диаметр колес робота
  Pk = 2; // Пропорциональный коэф-т ПД-регулятора
  PD = 6; // Дифференциальный коэф-т ПД-регулятора
  speed = 75; // Средняя скорость 

  while(true)
  {
    // сбрасываем показания энкодеров
    ResetRotationCount(OUT_B);
    ResetRotationCount(OUT_C);
    // сохраняем показания энкодеров
    B_enkoder = 0;
    C_enkoder = 0;    
    es = 0; // "старая" ошибка ПД - регулятора
    out = true; // по изменению этой переменной будем вылетать из цикла
    Wait(10);
    while(out)
    {
      // показания с датчиков
      D1 = Sensor(IN_3);
      D2 = Sensor(IN_2);
      // ошибка регулятора - разница показаний
      e = D2-D1;
      // управляющее воздействие по формуле ПД-регулятора
      u = e*Pk+PD*(e-es);
      // рассчитанную мощность подаем на моторы
      OnFwd(OUT_B,u+speed);
      OnFwd(OUT_C,-u+speed);
      // сохраняем ошибку для следующей итерации
      es = e;
      // если проехали половину круга - вылетаем из цикла
      if ((MotorRotationCount(OUT_B)+MotorRotationCount(OUT_C))/2*(d*PI/360) >= (1070*PI)/2)
      {
        out = false;
      }
    }
    // стоп моторы
    Off(OUT_BC);
    // это пауза 50 мс
    Wait(50);
    
    // смотрим на каком энкодере набралось больше показаний
    // в зависимости от этого разворачиваемся к соответствующую
    // сторону и сохраняем в trigger
    if (MotorRotationCount(OUT_B)-B_enkoder>MotorRotationCount(OUT_C)-C_enkoder)
    {
      RotateMotorEx(OUT_BC, speed, x/d*90, -100, true, true);
      trigger = 1;
    }
    else
    {
      RotateMotorEx(OUT_BC, speed, x/d*90, 100, true, true);
      trigger = 2;
    }
    // выравниваемся о линию     OnRev(OUT_B,15);
    OnRev(OUT_C,15);
    while(Sensor(IN_3) >= 46 && Sensor(IN_2) >= 44)
    {
      if (Sensor(IN_3)<=44)
      {
        Off(OUT_C);
      }
      if(Sensor(IN_2)<=40.5)
      {
        Off(OUT_B);
      }
    }
    Off(OUT_BC);
    Wait(50);
    // едем через круг
    OnFwdSyncEx(OUT_BC, speed, 0,RESET_NONE);
    while(Sensor(IN_3) >= 44 && Sensor(IN_2) >= 40.5)
    {
    }
    // при обнаружении линии - останавливаемся
    Off(OUT_BC);
    Wait(50);
    // немного вперед, чтобы развернуться над линией
    RotateMotorEx(OUT_BC, speed, 200, 0, true, true);
    // в trigger - направление разворота
    if (trigger == 1)
    {
      RotateMotorEx(OUT_BC, speed, x/d*90, 100, true, true);
    }
    else
    {
      RotateMotorEx(OUT_BC, speed, x/d*90, -100, true, true);
    }
    // сохраняем показания энкодеров
    B_enkoder = MotorRotationCount(OUT_B);
    C_enkoder = MotorRotationCount(OUT_C);
  }
}










воскресенье, 16 октября 2016 г.

LEGO Laser Pong

Судя по нашим последним работам может показаться, что нам так понравилась связка EV3 и EV3 Basic, что все проекты мы готовы делать только с их применением. EV3 Basic - действительно классная штука, однако мы отдаем себе отчет в том, что у него есть ряд ограничений и используем его по большей части как промежуточный, переходный инструмент, на пути от графического программирования к полноценному текстовому. В нашем сегодняшнем проекте LEGO Laser Pong мы вернемся к истокам и будем использовать старый добрый Mindstorms NXT. В качестве языка программирования в данном проекте мы выбрали NXС - замечательный инструмент разработки для платформ NXT и RСX. Подробнее о нем вы можете почитать в статье Антона Ботова "Программирование LEGO NXT роботов на языке NXC", являющейся переводом статьи известного LEGO-гуру Daniele Benedettelli.
NXC на наш взгляд - одна из тех крутых вещей, которая до сих пор дает платформе NXT огромное преимущество над EV3. Конечно, можно возразить, что RobotC расставляет все точки над i, но давайте вспомним его стоимость и сравним с свободно распространяемым NXC. Когда покупаешь ПО за свои деньги, а не за бюджетные или деньги клуба, а компьютеров, на которых программируют роботов несколько - это иногда имеет значение. К тому же NXC умеет работать с базовой прошивкой NXT и перепрошивать блок не приходится.


Pong является одной из самых ранних аркадных видеоигр, это теннисная спортивная игра с использованием простой двухмерной графики. Цель игры состоит в том, чтобы победить противника в настольный теннис, зарабатывая очки. Игра была создана фирмой Atari, которая выпустила её в 1972 году. Pong быстро стал популярным и стал первой коммерчески успешной видеоигрой. Вскоре после того, как Pong стал открытым, некоторые компании начали производить игры, которые копировали геймплей Pong'а.


Небольшой квадратик, заменяющий пинг-понговый мячик, двигается по экрану по линейной траектории. Если он ударяется о периметр игрового поля или об одну из нарисованных ракеток, то его траектория изменяется в соответствии с углом столкновения.
Геймплей состоит в том, что игроки передвигают свои ракетки вертикально, чтобы защищать свои ворота. Игрок получает одно очко, если ему удаётся отправить мячик за ракетку оппонента.

Мы неоднократно порывались сделать понг на экране блока сначала NXT, а затем и EV3, но каждый раз приходили идеи более интересных проектов, да и мы никак не могли придумать как можно вынести понг из этой "маленькой коробочки с экраном" в реальный мир.


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

Давайте посмотрим как работает наш робот, а затем подробно поговорим о том, как он устроен:



У привода лазерной указки две степени свободы, это позволяет роботу "светить" в любую точку на поверхности игрового поля. Один мотор отклоняет лазерную указку относительно условной оси Х, другой - относительно условной оси Y. Хотя робот и построен в основном с использование компонентов платформы NXT, мы использовали малый мотор EV3 в качестве подвижного мотора, в связи с его небольшим весом.



Одной "ракеткой" управляет человек, используя в качестве нее свою руку, сжатую в кулак. Расположение руки считывается ультразвуковым датчиком.



Второй ракеткой управляет робот. привод этой ракетки устроен следующим образом:


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



В начале игры "мяч" располагается по центру поля, для этого задаются его стартовые координаты x и y в мм относительно начала координат - точки, в которую смотрит лазер в момент запуска программы. После этого вычисляется случайное направление движения naprav в пределах некоторого сектора -60..60 градусов, в который "полетит мяч" в начале игры.


naprav = Random(120)-60;

Направление переводится в диапазон 0..360 градусов формулой

naprav = (naprav + 3600)%360;

Мяч начинает движение в памяти со скоростью speed (мм/сек), каждые 10 мс рассчитываются координаты его нового местоположения


y = y + speed/100 * sin(naprav*PI/180);
x = x + speed/100 * cos(naprav*PI/180);
Wait(MS_10);

naprav*PI/180 - переводит из градусов в радианы.
speed/100 - в связи с расчетом нового положения каждые 1/100 сек

В параллельном процессе рассчитываем углы отклонения лазера в зависимости от текущих x и y. В нем работают пара ПД-регуляторов, которые поворачивают указку под рассчитанными углами: В конструкции используется редуктор 56:1, учитываем это при расчетах углов поворота моторов.

ask MotorsControl()
{
while(true)
  {
  x_ugol = atan(x/z)*180.0/PI;
  
  gip = sqrt(x*x+z*z);
  y_ugol = atan(y/gip)*180.0/PI;

  E_x = x_ugol - MotorRotationCount(OUT_B)/56.0;
  u_x = Pk_x*E_x + Dk_x*(E_x - E_old_x);
  E_old_x = E_x;

  E_y = y_ugol - (-1*MotorRotationCount(OUT_C))/56.0;
  u_y = Pk_y*E_y + Dk_y*(E_y - E_old_y);
  E_old_y = E_y;

  Motor(u_x,-1*u_y);
    
  Wait(5);
  }


Функция Motor(int speed_B,int speed_C) реализует ограничение мощности на моторах в пределах -100..100

void Motor(int speed_B,int speed_C)
  {
  if(speed_B > 100) speed_B = 100;
  if(speed_B < -100) speed_B = -100;

  OnFwd(OUT_B, speed_B);

  if(speed_C > 100) speed_C = 100;
  if(speed_C < -100) speed_C = -100;

  OnFwd(OUT_C, speed_C);
  }

В случаях столкновения мяча с верхней или нижней границей поля мяч отскакивает, угол отражения равен углу падения

if (y<=0 || y>=Y_Max)
  {
  pic = 1;
  naprav = -1*naprav;
  naprav = (naprav + 3600)%360;
  }

Если достигнуты открытые границы поля, анализируется столкновение с ракетками:

    if (x<=-X_Max/2)
      {
      motor[0] = abs(MotorRotationCount(OUT_A)/2.44);
      
      if(y>motor[0]-10 && y<motor[0]+80)
        {
        pic = 2;

        if (abs(((motor[4] - motor[0]) / 4 * 10 + speed * sin(naprav*PI/180)) / speed) < 1) 
          {
          naprav = (asin (((motor[4] - motor[0]) / 4 * 10 + speed * sin(naprav*PI/180)) / speed)) *180 / PI;
          naprav = (naprav + 3600)%360;
          }
        else
          {
          naprav = -1*naprav+180;
          naprav = (naprav + 3600)%360;
          }
        }
      else
        {
        pic = 3;
        gol_i++;

        TextOut(0,LCD_LINE1,"PLAYER1 = ");
        TextOut(0,LCD_LINE3,"PLAYER2 = ");
        NumOut(80,LCD_LINE1,gol_i);
        NumOut(80,LCD_LINE3,gol_v);
        Wait(SEC_3);

        for (int i = -1*X_Max/2;i < 0;i++)
          {
          x = i;
          Wait(MS_10);
          }
        y = Y_Start;
        Wait(SEC_2);

        naprav = Random(120)-60;
        naprav = (naprav + 3600)%360;
        }
      }

    if (x>=X_Max/2)
      {
      US[0] = SensorUS(S4)*10-70;
      
      if(y>US[0]-50 && y<US[0]+70)
        {
        pic = 2;
      
        if (abs(((US[0] - US[4]) / 4 * 10 + speed * sin(naprav*PI/180)) / speed) < 1) 
          {
          naprav = (asin (((US[0] - US[4]) / 4 * 10 + speed * sin(naprav*PI/180)) / speed)) *180 / PI;
          }
        naprav = (naprav + 3600)%360;

        naprav = -1*naprav+180;
        naprav = (naprav + 3600)%360;
        }
      else
        {
        pic = 3;
        gol_v++;
      
        TextOut(0,LCD_LINE1,"PLAYER1 = ");
        TextOut(0,LCD_LINE3,"PLAYER2 = ");
        NumOut(80,LCD_LINE1,gol_i);
        NumOut(80,LCD_LINE3,gol_v);
      
        Wait(SEC_3);

        for (int i = abs(X_Max/2);i > 0;i--)
          {
          x = i;
          Wait(MS_10);
          }
        y = Y_Start;
        Wait(SEC_2);

        naprav = Random(120)+120;
        naprav = (naprav + 3600)%360;
        }
      }

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


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

новое_направление = arcsin ((скорость_ракетки + вертикальная скорость_мяча) / скорость_мяча)

Формула применима в случае, если модуль суммы скоростей ракетки и мяча не превышают скорости мяча.


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

task Protivnik()
{
  long Time_p = CurrentTick();
  int granica = Random(200)-200;
  while(true)
  {
    E_A =y*2.3 - (-1*MotorRotationCount(OUT_A));
    u_A = Pk_R*E_A + Dk_R*(E_A - E_old_A);
    E_old_A = E_A;

    if(u_A > 100) u_A = 100;
    if(u_A < -100) u_A = -100;

    if (Time_p - CurrentTick() > 2000)
      {
      granica = Random(200)-200;
       
      float tmp = Random(50)/100+0.5;
       
      Pk_R = Pk_A*tmp;
      Dk_R = Dk_A*tmp;

      Time_p = CurrentTick();
      }

    if(x<=granica)
      {
      OnRev(OUT_A, u_A);
      }
    else
      {
      Off(OUT_A);
      }
    
    Wait(10);
  }
}

Инструкцию по сборке робота, которую мы традиционно подготовили в формате LEGO Digital Designer, и программу для его работы можно скачать по ссылке.



Самое популярное