Статьи

Ruby + Arduino + LEGO® = RuBot

Некоторое время назад у меня были длинные выходные, чтобы убить, и я был в настроении для какого-то забавного взлома. Прогнозировали, что погода будет ужасной, и я вспомнил, что всегда хотел построить робота. Конечно, я построил простых роботов линейного следования и тому подобное несколько лет назад, но на этот раз я стремился к более высокому уровню. Моя идея состояла в том, чтобы создать робота под управлением Linux с Ruby-мозгом и возможностью предоставлять оператору живой видеопоток. Ну, по крайней мере, это моя первая веха. Модуль искусственного интеллекта и оружие судного дня должны пока оставаться в отставании.

Сначала я подумал о том, чтобы получить LEGO® Mindstorms для этой задачи, но потом я быстро вспомнил несколько вещей:

  1. Lego Mindstorms все еще довольно дороги, хотя они и крутая игрушка.
  2. NXT запрограммирован на странном языке LabView .
  3. У меня есть неиспользованный Arduino и пара моторов LEGO® Power Functions вместе с некой общей техникой LEGO®. Это должно делать свое дело.

аппаратные средства

Я сделал то, что сделал бы каждый здравомыслящий тинкер, и заказал Щит водителя мотора у Адафрута . Характеристики этого милого маленького щитка убедили меня просто получить его в качестве готового и протестированного продукта и не начинать паять чипы L293D самостоятельно. В конце концов, я в основном разработчик программного обеспечения, а не инженер-электронщик.

Пока я ждал доставки, я начал играть с некоторыми блоками LEGO®, чтобы создать шасси, достаточно прочное, чтобы носить с собой маленький ноутбук и Arduino. Я использовал свой надежный старый нетбук Asus EeePC 701, который я держу в основном в целях взлома и в качестве резервного компьютера. Этот причудливый маленький парень сидел бы на вершине и выполнял все тяжелые задачи, связанные с использованием процессора: запуск Ruby, предоставление видеоданных в режиме реального времени (640 × 480 пикселей), подключение WIFI. Даже при том, что он весит всего 922 г (~ 2 фунта) и имеет размеры 225×165 мм (~ 8,86 × 6,5 дюймов), нетрудно собрать что-то из LEGO® достаточно сильно, чтобы удержать его, не выглядя как гигантский блок. Забавно, насколько элегантное программирование похоже на элегантное машиностроение. Можно применять базовые принципы «СУХОЙ» или «ЯГНИ» и тому подобное, наряду с сущностью высокого уровня «меньше значит больше».

Для того, чтобы стать живым и здоровым, мозг роботов должен был общаться через USB с Arduino, который был бы снабжен Adafruits Motor Driver Shield. К щиту привода двигателя подключаются двигатели LEGO® Power Functions.

The finished robot

большая версия

  1. Arduino оборудован Adafruit Motor Driver Shield .
  2. Две силовые функции LEGO® M-Motors для вождения.
  3. Слегка измененный аккумуляторный блок LEGO® Power Functions, подключенный к щиту драйвера двигателя, который используется в качестве внешнего источника питания (питание от одного USB-порта недостаточно для питания двигателей 9 В).
  4. LEGO® Power Functions M-Motor для поворота захвата.
  5. Миниатюрный красный микромотор LEGO® для открывания и закрывания захвата.
  6. Колесо заклинателя способно поворачиваться на 360 °.
  7. Модифицированные провода LEGO® Power Functions, подключенные к экрану привода двигателя, обеспечивающие разъемы для двигателей.
  8. USB-подключение к Arduino.
  9. Камера (640 × 480 пикселей, 30 кадров в секунду).

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

Програмное обеспечение

Зная, что мне не понадобится модный внешний интерфейс и что приложение в целом получится довольно маленьким, я решил использовать Sinatra в качестве базовой платформы, сопровождаемой небольшим набором Gems и библиотек:

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

get '/' do
  if request.websocket?
    request.websocket do |ws|
      ws.onopen do
        ws.send("CONNECTED #{Time.now}")
        settings.websockets << ws
      end
      ws.onmessage do |msg|
        handle_commands(msg)
        EM.next_tick { settings.websockets.each{|s| s.send(msg) } }
      end
      ws.onclose do
        settings.websockets.delete(ws)
      end
    end
  else
    haml :index
  end
end

Здесь мы монтируем websocket прямо под «/», обрабатывая как запрос websocket, так и обычный GET для страницы индекса. Три функции обратного вызова onopenonmessageonclose

Метод handle_commands принимает сообщение websocket, которое в данном случае является строкой JSON, и передает действительные команды в Arduino через Ruby-Serialport Gem. Я написал тонкую оболочку, которая немного облегчает регистрацию нескольких двигателей и связывает их с именем и портом (на экране драйвера двигателя) соответственно. Он также позволяет определить, какое направление «вперед» или «назад» для каждого двигателя в вашей модели.

 handle_commands

Заметили линию, которая использует eSpeak ? Это примитивно реализованный, но невероятно забавный трюк, который добавляет голосовой вывод роботу! Другие параметры предназначены для фактических элементов управления и обрабатываются $arduino = Arduino.new
$motordriver = MotorDriver.new(
$arduino,
{ :left => Motor.new(2, $arduino, { :forward => Motor::BACKWARD, :backward => Motor::FORWARD }),
:right => Motor.new(1, $arduino, { :forward => Motor::BACKWARD, :backward => Motor::FORWARD }),
:gripper => Motor.new(3, $arduino, { :forward => Motor::FORWARD, :backward => Motor::BACKWARD }),
:rotator => Motor.new(4, $arduino, { :forward => Motor::FORWARD, :backward => Motor::BACKWARD }),
}
)

def handle_commands(params={})
params = (JSON.parse(params) unless params.class == Hash rescue {})
Thread.new{`espeak '#{params['speak'].tr(''','')}' 2< /dev/null`} if params['speak']
$motordriver.left(*params['left']) if params['left']
$motordriver.right(*params['right']) if params['right']
$motordriver.gripper(*params['gripper']) if params['gripper']
$motordriver.rotator(*params['rotator']) if params['rotator']
rescue
p $!
end
Он просто переводит все команды двигателя в сообщения длиной 3 байта в Arduino в соответствии с очень простым двоичным протоколом, который я создал. Первый байт определяет двигатель (1-4), второй байт определяет направление (вперед = 1, назад = 2, тормоз = 3, отпускание = 4), а третий байт определяет скорость (0-255). Полный эскиз Arduino ( эскиз — это программа на языке Arduino) выглядит следующим образом:

 $motordriver

Для видео соединения я хотел использовать реальный аудио / видео поток с каким-то необычным кодеком, таким как H.263, но отказался после нескольких попыток с VLC , FFMPEG и MPlayer в качестве стримера. Я просто не мог получить задержку ниже 3-4 секунд. Я знаю, что это звучит не так уж и много, но когда вы управляете удаленным роботом, который выполняет ваши команды почти мгновенно, но доставляет изображения с такой задержкой, у вас будет плохое время и вы будете часто сталкиваться с объектами. Я решил попробовать простую потоковую передачу Motion JPEG, которая в основном просто отправляет изображения JPEG в браузер, не закрывая соединение. Почему-то, несмотря на гораздо большее использование полосы пропускания, задержка сократилась до половины секунды или около того. Моя теория заключается в том, что Asus EeePC 701 слишком медленный для более привлекательных кодеков. Но, тем не менее, если кто-то из вас имел успех с «настоящей» потоковой передачей с низкой задержкой, кроме Red5 (поскольку я не ищу Flash-решения), пожалуйста, не стесняйтесь оставлять комментарии и указывать мне правильное направление.

Мой код для обработки потоковой передачи M-JPEG немного более загроможден, но, в конце концов, это все равно хак;)

 #include <AFMotor.h> // Adafruit Motor shield library

AF_DCMotor motors[] = {AF_DCMotor(1), AF_DCMotor(2), AF_DCMotor(3), AF_DCMotor(4)};
int i = 0;

void setup() {
  Serial.begin(9600);
  Serial.println("Motor test!n");
  for(i=0;i<4;i++){ motors[i].run(RELEASE); }
}

void loop() {
  uint8_t motor;
  uint8_t direction;
  uint8_t speed;
  while (Serial.available() > 2) {
    motor     = Serial.read();
    direction = Serial.read();
    speed     = Serial.read();
    if(motor > 0 && motor < 5) {
      if(direction < 1 || direction > 4){ direction = 4; }
      motors[motor-1].setSpeed(speed);
      motors[motor-1].run(direction);
    }
  }
}

В основном я изменил / расширил программу Uvccapture до…

  • … Разрешить «STDOUT» для опции -o
  • … Использовать миллисекунды вместо секунд для опции -t
  • … Иметь опцию -D для указания разделителя или использовать get '/mjpgstream' do
    fps = (params['fps'] || 10).to_i
    ms = (1000 / fps.to_f).to_i
    headers('Cache-Control' => 'no-cache, private', 'Pragma' => 'no-cache',
    'Content-type' => 'multipart/x-mixed-replace; boundary={{{NEXT}}}')
    stream(:keep_open) do |out|
    if !$mjpg_stream || $mjpg_stream.closed?
    puts "starting mjpg stream with #{fps} fps. #{ms} ms between frames."
    $mjpg_stream = IO.popen "./uvccapture/uvccapture -oSTDOUT -m -t#{ms} -DMJPG -x640 -y480 2> /dev/null"
    end
    settings.video_connections << out
    out.callback {
    settings.video_connections.delete(out)
    if settings.video_connections.empty?
    puts 'closing mjpg stream'
    $mjpg_stream.close
    end
    }
    buffer = ''
    buffer << $mjpg_stream.read(1) while !buffer.end_with?("{{{NEXT}}}n")
    while !$mjpg_stream.closed?
    begin
    out << $mjpg_stream.read(512)
    rescue
    end
    end
    end
    end

и использовал его с вызовом "nn--{{{NEXT}}}nnContent-type: image/jpegnn" Поток Uvccapture буферизуется и используется повторно в случае, если более одного клиента запрашивает ресурс, а также открывается и закрывается по требованию. Я осознаю, что это, вероятно, не самое элегантное решение, но в данном случае оно работало достаточно хорошо для меня.

Веб-интерфейс для робота довольно прост, как и было обещано, и содержится в самом основном файле в виде двух крошечных шаблонов Haml:

[Haml]
@@ layout
!!! 5
% html {: lang => ‘en’}
%голова
% meta {: charset => ‘utf-8 ′}
% link {: rel => ‘значок ярлыка’,: type => ‘image / gif’,: href => ‘favicon.gif’}
% title RuBot
% Тела {: стиль => ‘цвет фона: черный; выравнивания текста: центр;}
= yield (: макет)
% script {: src => ‘jquery.js’}
% script {: src => ‘app.js’}

@@ показатель
% ДИВ
% {IMG: SRC => ‘/ mjpgstream’}
% Р {: стиль => ‘цвет: # 555’}
Помогите:
Используйте клавиши со стрелками для езды,
Q / W, чтобы открыть / закрыть захват
A / S, чтобы повернуть захват влево / вправо
удерживать смену для медленного режима,
пространство для разговора
[/ Haml]

Вся магия внешнего интерфейса происходит в «app.js», где элементы управления робота просто отображаются на события клавиатуры. Оглядываясь назад, немного не повезло, потому что управлять роботом через браузер мобильного телефона намного сложнее, но я надеюсь, что вы меня простите. Другая вещь, которая заметна в этих нескольких строках кода Haml, это строка IO.popen Это все, что нужно для отображения потока M-JPEG. Тип содержимого %img{:src=>'/mjpgstream'} multipart/x-mixed-replace; boundary={{{NEXT}}} Довольно круто, если вы спросите меня.

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

Как всегда, я надеюсь, вам понравился этот маленький проект, и если это так, не стесняйтесь рассказать своим друзьям 🙂