суббота, 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, и программу для его работы можно скачать по ссылке.



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

EV3 Music Station

В нашем новом проекте мы вновь коснемся музыкальной темы. Ранее мы уже строили несколько музыкальных "роботов": NXT-гитару, EV3-гитару, EV3-скрипку, NXT робота для тренировки чувства ритма, Arduino-барабанную установку и EV3-секвенсор. Каждый из них выполнял одну, специфическую функцию, ради которой собственно и создавался.


В проекте EV3 Music Station мы создадим робота-универсала, который сможет стать полноценным участником на ваших музыкальных репетициях. Хотя наш робот и построен на базе LEGO Mindstorms EV3, но использует в своем составе компоненты других наборов - пару моторов от NXT, пару датчиков-кнопок от него же, кроме этого применяются детали LEGO Technic. Традиционно мы подготовили для вас инструкцию по сборке в формате LEGO Digital Designer, которую можно скачать по ссылке.


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

Ритм


Как гласит музыкальная теория, музыка состоит из мелодии, гармонии и ритма, которые в свою очередь состоят из звуков и звукосочетаний различной высоты и длительности и образуют внешнюю форму музыкального произведения.
Начнем, пожалуй, с ритма. Наш робот должен уметь колотить по барабанам, в качестве которых мы будем использовать жестяные банки, так как от них много шума :) Количество моторов, которое можно подключить к одному блоку EV3 ограничено 4-мя, а робот должен не только барабанить, но и выполнять другие прикольные вещи, поэтому для ритма мы будем использовать только два мотора и две барабанные палочки, в качестве которых возьмем пару простых карандашей. Жестяные банки-барабаны возьмем специально разные, большая банка будет рабочим барабаном, мелкая банка - чем-то вроде тарелки.




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


Посмотрите, для примера, как настучал бы человек ритм для двух барабанов (синим цветом - моменты нажатия первой кнопки, красным - второй). Так как нотная запись - точная наука, мы будем привязывать эти нажатия к ближайшим долям такта, выравнивая ритм. Для того, чтобы зациклить ритмический рисунок, последний удар барабана должен повторять первый, это важно. Его мы отбросим, а вот длительность паузы перед ним нам нужна.

Давайте посмотрим на программу ритм-лупера:

' инициализируем датчики-кнопки
Sensor.SetMode(1,0)
Sensor.SetMode(2,0)
' плавно опускаем барабанные палочки в течении 5 секунд
Motor.Start("BC",-3)
Program.Delay(5000)
' останавливаем моторы и сбрасываем энкодеры
Motor.Stop("BС","False")
Motor.ResetCount("BC")
' поднимаем палочки в исходное положение
drum_start = 45
Motor.Move("BC",50,drum_start,"True")

' drum1 и drum2 - логические управляющие сигналы - поднять/опустить палочку
drum1 = "False"
drum2 = "False"
' reg1 и reg2 - верхние положения палочек
reg1 = drum_start 
reg2 = drum_start
' пропорциональный коэф-т регуляторов
k = 0.8

' процедура Drums будет выполняться в параллельном процессе, содержит в себе регуляторы, управляющие положением палочек
Sub Drums
  While "True"
    ' если поступил сигнал поднять/опустить 1-ю палочку - изменяем reg1
    If drum1 = "True" Then
      reg1 = 0
    Else  
      reg1 = drum_start                  
    EndIf
    ' если поступил сигнал поднять/опустить 2-ю палочку - изменяем reg2
    If drum2 = "True" Then
      reg2 = 0
    Else  
      reg2 = drum_start
    EndIf
    
    ' рассчитываем ошибку 1-го регулятора
    e1 = reg1 - Motor.GetCount("B")
    ' рассчитываем управляющее воздействие 1-го регулятора
    v1 = e1 * k
    ' Если палочка поднялась, останавливаем ее
    If Math.Abs(Motor.GetCount("B")) > 40 And drum1 = "False" Then
      Motor.Stop("B","False")
    Else  
    ' иначе подаем управляющее воздействие на мотор
      Motor.Start("B",v1)
    EndIf
    
    ' все тоже самое со вторым регулятором и вторым мотором
    e2 = reg2 - Motor.GetCount("C")
    v2 = e2 * k
    If Math.Abs(Motor.GetCount("C")) > 40 And drum2 = "False" Then
      Motor.Stop("C","False")
    Else     
      Motor.Start("C",v2)  
    EndIf
    
    ' Если палочка опустилась ниже порога, устанавливаем drum1 в "False", тем самым подавая сигнал поднять палочку
    If Motor.GetCount("B") < 20 Then
      drum1 = "False"
    EndIf

    ' тоже самое со вторым мотором, поговое значение отличается, так как банки у нас разные и стучать нужно с разной силой
    If Motor.GetCount("C") < 10 Then
      drum2 = "False"
    EndIf
  EndWhile
EndSub

' запускаем вышеописанную процедуру как параллельный процесс
Thread.Run = Drums  

' создаём переменные
n1 = 0
n2 = 0
' время начала такта
tn = 0
' время конца такта
tc = 0
' время начала "прослушивания" кнопок
d = EV3.Time
n = 0
trigger1 = "False"
trigger2 = "False"

' "прослушиваем" кнопки в течении 8 секунд
While EV3.Time < d + 8000
' если кнопка нажата и не была нажата
  If Sensor.ReadPercent(1) = 100 And trigger1 = "False" Then
  ' если это первое нажатие
    If n = 0 Then
      n = EV3.Time
    EndIf  
    ' запоминаем что кнопка уже нажата
    trigger1 = "True"
    ' в массив n1 заносим время нажатия
    b1[n1] = EV3.Time - n   
    ' переходим к следующему элементу массива
    n1 = n1 + 1
  Else
    ' если кнопка отжата - сбрасываем триггер
    If Sensor.ReadPercent(1) = 0 Then   
      trigger1 = "False"
    EndIf
  EndIf  
  ' повторяем для второй кнопки и массива n2
  If Sensor.ReadPercent(2) = 100 And trigger2 = "False" Then
    If n = 0 Then
      n = EV3.Time
    EndIf    
    trigger2 = "True"
    b2[n2] = EV3.Time - n
    n2 = n2 + 1
  Else
    If Sensor.ReadPercent(2) = 0  Then
      trigger2 = "False"
    EndIf
  EndIf    
EndWhile

' определяем начало и конец такта
If b1[0] < b2[0] Then
  tn = b1[0]  
Else  
  tn = b2[0]
EndIf  
If b1[n1-1] > b2[n2-1] Then
  tc = b1[n1-1]  
Else  
  tc = b2[n2-1]
EndIf  
' определяем длительность такта
dt = tc - tn  

' создаем пустые массивы с 16-ми долями такта для каждого барабана
For r = 0 To 16
  d1[r] = 0
  d2[r] = 0
EndFor  

' привязываем время нажатия кнопок к ближайшим долям такта
For e = 0 To n1 - 1
  d1[Math.Round(b1[e]/dt*16)] = 1
EndFor
For e2 = 0 To n2 - 1
  d2[Math.Round(b2[e2]/dt*16)] = 1
EndFor

' выводим на экран содержимое массивов d1 и d2 для контроля

LCD.Clear()

x = 0
y = 0
For u = 0 To 16
  LCD.Text(1,x,y,1,d1[u])
  x = x + 10
EndFor  
x2 = 0
y2 = 10
For u2 = 0 To 16
  LCD.Text(1,x2,y2,1,d2[u2])
  x2 = x2 + 10
EndFor
' любуемся результатом на экране в течении 10 секунд
Program.Delay(10000)  

' начинаем проигрывать ритм в цикле
While "True"
  For i = 0 To 15
    ' если в ячейке массива d1 единица - отсылаем процессу Drums сигнал drum1 = "True"
    If d1[i] = 1  Then 
      drum1 = "True"
    EndIf
    ' если в ячейке массива d2 единица - отсылаем процессу Drums сигнал drum2 = "True"
    If d2[i] = 1  Then 
      drum2 = "True"    
    EndIf
  ' ждем 1/16 такта
  Program.Delay(dt/16)
  EndFor
EndWhile



Мелодия


Контроллер EV3 обладает довольно развитыми звуковыми возможностями, на нем вполне можно слушать музыку, синтезировать речь. Например в проекте "EV3 Internet Radio Receiver" мы слушали с его помощью онлайн-радио. Однако базовая LEGO-прошивка таких крутых возможностей нам не дает и в данном проекте мы ограничимся использованием одноголосного спикера, который может воспроизводить ноты в пределах нескольких октав.


Записывать последовательность вызовов Speaker.Note в программу мы конечно же не будем, а попробуем использовать ранее не испытанные возможности EV3 Basic по работе с файлами и текстовыми строками.
Для начала определимся с форматом файла. Мы придумали вот такой вот простейший формат хранения (см. ниже).

90
4 4
1
0000000000001000
0000000000000000
Ps  4  Ps  4  Ps  4  A5  4 
Ps  1 
2
1010101010101010
1000100010101000
E5  4  A5  4  E5  4  A5  4 
Ps  1 

Первая строка в файле - bpm - удары в минуту - показатель, определяющий скорость исполнения или воспроизведения композиции. BPM — это количество четвертных нот в минуту, например, 120 BPM означает, что в минуту играется 120 четвертных нот (следовательно, 2 четверти в секунду), или 120 четвертных ударов метронома в минуту.
Вторая строка - размер (4/4, 3/4 и т.п.).
Далее идут такты, первая строка такта содержит его номер.
Вторая строка - удары первого барабана (тарелки) в 16-х долях такта.
Третья строка - удары второго барабана (рабочего) в 16-х долях такта
Четвертая строка содержит 7-символьные текстовые последовательности воспроизводимых нот мелодии - первые три символа - нота с указанием октавы, разделитель, длительность ноты в долях такта. Ps - пауза.
Пятая строка зарезервирована под аккорды гармонии.
Файл завершается номером такта 999.

Чтобы открыть файл для чтения будем использовать следующую конструкцию:

MyFile = EV3File.OpenRead("/home/root/lms2012/prjs/EV3MusicStation/file.txt")

Считать текстовую строку из него можно так

size = EV3File.ReadLine(MyFile)

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

Гармония


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


Пятая, зарезервированная строка в каждом такте может содержать аккорды гармонии

3
1010101010101010
1000100010001000
C5  8  C5  8  C5  8  B4  4  C5  4  Ps 8
C   1

Формат такой же, 7-символьный, тоника аккорда указывается в первом символе, причем "С" - чистое "До", а "с" - До-диез. Следующий два символа - возможная надстройка аккорда, например минор - "m", септаккорды - "7", "m7", пауэр-аккорд - "5". В конце длительность, в течении которой он действует. Ps - все также пауза.

Пример строки гармонии: "Ps  8  Am  4  Dm  4  C   8  Ps  4  "

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



В заключение хотелось бы отметить, что EV3 Basic, в отличии от официального ПО LEGO отлично работает с массивами строк. Это очень важная особенность, существенно расширяющая применимость данного инструментария.

В ходе работы над проектом мы обнаружили интересную особенность - штатная прошивка LEGO с легкостью выполняет функции, которых нет в официальном ПО от LEGO, например функции работы со строками

Text.GetSubText() - вырезать кусок подстроки
Text.GetLength() - вычислить дkину строки

Кроме этого прекрасно работает перевод строки в число с помощью функции EV3File.ConvertToNumber(). Такая возможность была в LEGO NXT-G, а вот в ПО LEGO EV3 ее очень недоставало. Из новых функций, которые мы использовали в проекте также стоит отметить Math.Remainder() - остаток от деления и Math.Round() - округление до ближайшего целого. Примеры применения всех этих функций вы можете посмотреть в исходном коле программы.

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

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