Катаемся на Xiaomi Vacuum Cleaner

в 16:25, , рубрики: dualshock, IoT, linux, perl, python, Интернет вещей

Вот и пришли новогодние праздники, а с ними и куча свободного времени, да еще и умный пылесос угодил ко мне в руки. Как только я увидел в приложении 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 7777n";

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_portn";
    my $data = "";
    $client_socket->recv($data, 256);
    print "received data: $datan";
    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 — аргумент функции

Результат

Автор: saveliy_zhuravlev

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js