Катаемся на Xiaomi Vacuum Cleaner
Вот и пришли новогодние праздники, а с ними и куча свободного времени, да еще и умный пылесос угодил ко мне в руки. Как только я увидел в приложении MiHome ручное управление, я сразу понял, что хочу сделать: будем управлять пылесосом с помощью геймпада Dualshock v4!
Шаг 1, тащим токен, прошиваем (опционально)
Ставим пропатченное приложение MiHome, которое будет показывать нам токен, далее выбираем рутованную прошивку, скачиваем, ставим python-miio (pip install python-miio), пробуем установить прошивку с помощью mirobo --ip %ip% --token %token% update-firmware %filename%
и на этом моменте у меня все сломалось. Пылесос отчаянно отказывался обновляться, после нескольких часов гугления я попробовал посмотреть отладочный вывод mirobo и о чудо! Из-за того, что у меня на ноутбуке установлено несколько адаптеров, он пытался раздать прошивку в сети адаптера VirtualBox Host-Only. Далее я просто поднял файловый сервер и выполнил эту команду: mirobo --ip=%ip% --token=%token% raw-command miIO.ota '{"mode":"normal", "install":"1", "app_url":"http://%my_ip:port%/%filename%.pkg", "file_md5":"%md5%","proc":"dnld install"}'
. Прошивка встала где-то за 10 минут, доступ по ssh работал
Шаг 2, пытаемся покататься на роботе
import miio
ip = ''
token = ''
bot = miio.vacuum.Vacuum(ip, token)
bot.manual_start()
bot.manual_control(0, 0.3, 2000) # move forward with max speed for 2 seconds
bot.manual_control(90, 0, 1000) # rotate
bot.manual_stop()
На этом этапе пылесос должен сказать Using remote controls (или что-то подобное в зависимости от прошивки), подергаться и остановиться
Шаг 3, подключаем Dualshock
После небольшого исследования было решено использовать pygame
Смотрим, какие кнопки/стикеры за что отвечают
BUTTON_SQUARE = 0
BUTTON_X = 1
BUTTON_CIRCLE = 2
BUTTON_TRIANGLE = 3
def init_joystick():
pygame.init()
pygame.joystick.init()
controller = pygame.joystick.Joystick(0)
controller.init()
return controller
def main():
controller = init_joystick()
bot = miio.vacuum.Vacuum(ip, token)
modes = ['manual', 'home', 'spot', 'cleaning', 'unk']
mode = 'unk'
axis = [0.00 for _ in range(6)]
flag = True
button = [False for _ in range(14)]
print('Press start to start!')
while flag:
for event in pygame.event.get():
if event.type == pygame.JOYAXISMOTION:
axis[event.axis] = round(event.value,2)
elif event.type == pygame.JOYBUTTONDOWN:
button[event.button] = True
# Touchpad to exit
if event.button == 13:
flag = False
elif event.type == pygame.JOYBUTTONUP:
if mode == 'unk':
print('Ready to go! Press X to start manual mode')
if event.button == BUTTON_X:
mode = 'manual'
bot.manual_start()
elif mode == 'manual':
if event.button == BUTTON_TRIANGLE:
bot.manual_stop()
mode = 'unk'
elif event.button == BUTTON_X:
play_sound('http://192.168.1.43:8080/dejavu.mp3') # see ya later
elif event.button == BUTTON_CIRCLE:
# stop sound
play_sound(';')
if mode == 'manual':
try:
move_robot(bot, button, axis) # see ya in the next step
except:
bot.manual_start()
pass
time.sleep(0.01)
Пока в move_robot можно сделать просто print (axis) и проверить, что джойстик работает.
Далее нам нужно сделать так, чтобы робот ездил при нажатии на кнопки/стики, я выбрал левый стик по оси Y (вверх -1, вниз 1) для скорости и правый стик по оси X для угла, получилась примерно такая функция
def translate(value, leftMin, leftMax, rightMin, rightMax):
leftSpan = leftMax - leftMin
rightSpan = rightMax - rightMin
valueScaled = float(value - leftMin) / float(leftSpan)
return rightMin + (valueScaled * rightSpan)
def move_robot(bot, buttons, axis):
rot = 0
val = 0
to_min, to_max = -0.3, 0.3
# Right stick X
if axis[2] != 0:
rot = -translate(axis[2], -1, 1, -90, 90)
if abs(rot) < 8:
rot = 0
# Left stick Y, -1 up, 1 down
if axis[1] != 0:
val = -translate(axis[1], -1, 1, to_min, to_max)
if abs(val) < 0.07:
val = 0
if rot or val:
bot.manual_control(rot, val, 150)
Запускаем скрипт, жмем Х на контроллере и робот должен ездить и поворачивать
На этом этапе у меня возникла проблема: почему-то если нажать левый стик вперед до конца и попытаться повернуть, он не будет поворачивать, придется сначала сбросить скорость, если попытаться уменьшить значения маппинга, например поставить -0.29, 0.29, он начнет ездить по кругу, пока не изменится положение левого стикера, я так и не разобрался, в чем тут проблема
Шаг 4, добавим музыки
Заходим по ssh на нашего робота и смотрим, какие скриптовые языки тут есть.
Питона не было, а устанавливать его я смысла не видел, зато нашел перл, для нашей небольшой задачки подойдет
Далее устанавливаем sox:
sudo apt-get install sox, libsox-fmt-mp3
и пишем небольшой сервер на перле:
#!/usr/bin/perl
use IO::Socket::INET;
$| = 1;
my $socket = new IO::Socket::INET (
LocalHost => '0.0.0.0',
LocalPort => '7777',
Proto => 'tcp',
Listen => 2,
Reuse => 1
);
die "cannot create socket $!\n" unless $socket;
print "server waiting for client connection on port 7777\n";
while(1)
{
my $client_socket = $socket->accept();
my $client_address = $client_socket->peerhost();
my $client_port = $client_socket->peerport();
print "connection from $client_address:$client_port\n";
my $data = "";
$client_socket->recv($data, 256);
print "received data: $data\n";
my @urls = split /;/, $data;
system("killall play > /dev/null");
$data = "ok";
$client_socket->send($data);
shutdown($client_socket, 1);
if ( $urls[0] ne "") {
system("play -q -v 0.4 " . $urls[0] . " &");
}
}
$socket->close();
sudo perl sound_server.pl
у себя в консольке делаем что-то вроде
import socket
ip = ''
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, 7777))
s.sendall(b'http://%local_ip%:%local_port%/test.mp3;')
s.close()
И через пылесос должен заиграть наш test.mp3 (соответственно, нужно поднять файловый сервер на нашей локальной машине)
Наша функция play_sound () будет делать практически то же самое, только будет sendall (url+';'), url — аргумент функции
Результат