Некоторое время назад у меня были длинные выходные, чтобы убить, и я был в настроении для какого-то забавного взлома. Прогнозировали, что погода будет ужасной, и я вспомнил, что всегда хотел построить робота. Конечно, я построил простых роботов линейного следования и тому подобное несколько лет назад, но на этот раз я стремился к более высокому уровню. Моя идея состояла в том, чтобы создать робота под управлением Linux с Ruby-мозгом и возможностью предоставлять оператору живой видеопоток. Ну, по крайней мере, это моя первая веха. Модуль искусственного интеллекта и оружие судного дня должны пока оставаться в отставании.
Сначала я подумал о том, чтобы получить LEGO® Mindstorms для этой задачи, но потом я быстро вспомнил несколько вещей:
- Lego Mindstorms все еще довольно дороги, хотя они и крутая игрушка.
- NXT запрограммирован на странном языке LabView .
- У меня есть неиспользованный 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.
- Arduino оборудован Adafruit Motor Driver Shield .
- Две силовые функции LEGO® M-Motors для вождения.
- Слегка измененный аккумуляторный блок LEGO® Power Functions, подключенный к щиту драйвера двигателя, который используется в качестве внешнего источника питания (питание от одного USB-порта недостаточно для питания двигателей 9 В).
- LEGO® Power Functions M-Motor для поворота захвата.
- Миниатюрный красный микромотор LEGO® для открывания и закрывания захвата.
- Колесо заклинателя способно поворачиваться на 360 °.
- Модифицированные провода LEGO® Power Functions, подключенные к экрану привода двигателя, обеспечивающие разъемы для двигателей.
- USB-подключение к Arduino.
- Камера (640 × 480 пикселей, 30 кадров в секунду).
Пайка не требуется вообще. Хотя я сделал небольшую пайку, чтобы создать меньший, более тонкий USB-кабель, чтобы добиться беспрепятственного внешнего вида.
Програмное обеспечение
Зная, что мне не понадобится модный внешний интерфейс и что приложение в целом получится довольно маленьким, я решил использовать Sinatra в качестве базовой платформы, сопровождаемой небольшим набором Gems и библиотек:
- Sinatra
- Синатра-WebSocket
- Тонкий веб-сервер
- Рубин-SerialPort
- Haml
- JQuery
- Uvccapture (модифицированный .. читать дальше)
Я уже давно искал вескую причину поиграться с веб-сокетами и, наконец, воспользовался возможностью, чтобы уменьшить задержку управления роботом. Я могу сказать вам, что в таких случаях соединение через веб-сокет действительно дает вам дополнительную безопасность по сравнению с вызовами 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 для страницы индекса. Три функции обратного вызова onopen
onmessage
onclose
Метод 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, в этом репозитории .
Как всегда, я надеюсь, вам понравился этот маленький проект, и если это так, не стесняйтесь рассказать своим друзьям 🙂