4.2.2021

foto Petr Bravenec

Petr Bravenec
Twitter: @BravenecPetr
+420 777 566 384
petr.bravenec@hobrasoft.cz

Úvod

V minulém díle jsem popsal základní hardware pásovce. Dnes je na řadě ruční ovládání základního pohybu. Ruční ovládání není dozajista to, co nás ve výsledku zajímá, ale je extrémně důležité pro sběr dat. Abychom mohli úspěšně natrénovat neuronovou síť, potřebujeme spoustu dat. V případě Pásovce budeme potřebovat spoustu obrázků z obou kamer, abychom na nich neuronové síti ukázali, jak vypadá volná cesta a jak vypadá překážka.

Omlouvám se předem, že moje popisy budou silně CLI orientované a velmi nedotažené. Navíc komplet vše dělám na Linuxu — ve Windows mohou postupy vypadat úplně jinak. Na Windows se mě neptejte, Windows mám pouze na notebooku a používám v nich jedinou pracovní aplikaci: linuxový terminál z balíku WSL.

Instalace systému

Nvidia Jetson Nano spouští systém z µSD karty. Celá instalace systému tak spočívá v tom, že z internetu stáhnete obraz karty, překopírujete jej na µSD kartu, nastavíte síť a je to. Podrobnosti nebudu popisovat, odkážu vás přímo na hotový návod:

K návodu bych měl jen několik poznámek:

Krok 3: V tuto chvíli bych nastavil i ssh server, aby se dalo dostat na povelovou řádku vzdáleně.

Krok 4: Na displeji se vypisuje pouze IPv4 adresa. Pro většinu z vás to nic neznamená, ale já bych tam raději viděl linkovou IPv6 adresu. Poslední bod kroku 4 vás pošle na Jupyter notebook. To je dobrý odrazový můstek. Najdete tam spoustu příkladů, bobužel sem mi ty nejzajímavější z nich nikdy nepodařilo rozchodit.

Krok 5: Zkuste to alespoň ze začátku raději bez upgrade systému. Můžete samozřejmě zkusit nainstalovat novou verzi balíku jetbot, ale během experimentů jsem získal nepříjemný pocit, že upgrade celého systému zbytečně rozbil tu část, která mě především zajímá — neuronové sítě. Velkou spustu balíků jsem pak musel instalovat přes pip. A právě vytvoření funkčního prostředí nástrojem pip mi trvalo neuvěřitelnou dobu — strávil jsem s tím určitě měsíc.

Jakmile je systém nainstalovaný a nastavený, ověřte, že můžete ovládat svého robota:

root@pasovec:~/pasovec# python3
Python 3.6.9 (default, Oct  8 2020, 12:12:24) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from jetbot import Robot
>>> 

Import na povelové řádce pythonu 3 nesmí skončit chybou. Tohle je špatně:

Python 2.7.17 (default, Sep 30 2020, 13:38:04) 
[GCC 7.5.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from jetbot import Robot
Traceback (most recent call last):
  File "", line 1, in 
ImportError: No module named jetbot
>>>

První kroky Pásovce

Pro ovládání pohybu Pásovce jsem zkoušel kde co. Jako první jsem vyzkoušel Jupyter notebook přibalený s balíkem jetbot. Ovládání klikáním myší na tlačítka ve webové aplikaci je opravdu chuťovka. Na tyto první krůčky připadá nejvyšší počet pokusů o proražení zdi Pásovcem.

Od dětí jsem si půjčil gamepad. Ten je pro ovládání celkem příjemný, ale gamepad velmi často posílal i v neutrální poloze nějaké malé hodnoty výchylek. Pásovec v důsledku toho místo úplného zastavení pomaličku popojížděl některým směrem, případně stál, ale tiše si bzučel v rytmu PWM řízení motorů. Navíc bezdrátový gamepad funguje jen na poměrně krátkou vzdálenost.

Nejlépe se mi pak osvědčila zavedená kombinace kláves WASD. Přidal jsem si ještě klávesu X pro úplné zastavení. Nezjistil jsem totiž (ani jsem moc nepátral), jak získat v Pythonu událost "KeyPressEvent" a "KeyReleaseEvent" odděleně, navíc v ssh relaci.

Při ovládání z klávesnice velmi oceníte vzdálený přístup přes ssh. Klávesnice připojená přímo k Pásovci se dá taky použít, ale chodit za Pásovcem v podřepu a snažit se řídit směr je nepohodlné cvičení.

Nyní už hotový kód:

import sys, termios, tty, os
from jetbot import Robot

def getch():
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    try:
        tty.setraw(sys.stdin.fileno())
        ch = sys.stdin.read(1)

    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    return ch

maxspeed = 0.4
fwspeed = 0;
robot = Robot()
while True:
    char = getch()
    if (char == "w"):
        fwspeed = maxspeed
        robot.forward(fwspeed)
        continue
    if (char == "a"):
        if (fwspeed > 0):
            robot.set_motors(-fwspeed, fwspeed);
        else:
            robot.left(maxspeed)
        continue
    if (char == "s"):
        fwspeed = -maxspeed
        robot.forward(fwspeed)
        continue
    if (char == "d"):
        if (fwspeed > 0):
            robot.set_motors(fwspeed, -fwspeed);
        else:
            robot.right(maxspeed)
        continue
    if (char == "x"):
        fwspeed = 0
        robot.stop()
        continue
    if (char == "p"):   # ukončení programu
        break

A takhle se s tím pak jezdí:

MQTT

Při experimentech se ukázalo, že někdy může být dobrý nápad zpracovat některé části (tj. video) na jiném počítači. Zajímavé to pro mě bylo především v situaci, kdy jsem ještě nedokázal zpracovat video v Nvidia Jetson Nano, ale bez problémů jsem to dokázal ve svém stolním počítači. Tehdy jsem posílal video přes gstreamer do PC a z PC se posílala jednoduchá informace překážka/volno MQTT protokolem.

MQTT je jednoduchý odlehčený protokol pro posílání zpráv mezi IOT zařízeními. Odkazy:

Abychom dokázali komunikovat protokolem MQTT, potřebujeme dvě základní komponenty: MQTT server a MQTT klienta pro Python. Server je přímo součástí distribuce, nainstalujeme jej prostým apt-get install mosquitto. Klienta pro Python jsem nainstaloval pomocí pip: pip3 install paho-mqtt.

Server není nutné nijak nastavovat. Po startu server automaticky poslouchá a distribuuje zprávy klientům. Samozřejmě může být vhodné nějak omezit přístupy, ale na malé lokální síti mě to příliš netrápí. Zkontrolovat server můžete programem ss:

root@pasovec:~# ss -ntpl | grep mosquitto
LISTEN   0         100                 0.0.0.0:1883             0.0.0.0:*        users:(("mosquitto",pid=4254,fd=4))
LISTEN   0         100                    [::]:1883                [::]:*        users:(("mosquitto",pid=4254,fd=5))

Program se potom rozdělí na dvě části. Jedna část obsluhuje klávesnici (může běžet i na jiném počítači), druhá část obsluhuje motory. Navzájem se domlouvají protokolem MQTT. Následující kód už je verze, kterou používám i pro komunikaci s neuronovou sítí, obsahuje proto i režim "automat". Informace z neuronové sítě ale zatím pochopitelně chybí.

Klávesnice (keyboard.py):

import sys, termios, tty, os
import paho.mqtt.client as mqtt

class Keyboard():
    def getch(self):
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch

    def run(self):
        while True:
            char = self.getch()
            if char == "h": self.client.publish(self.topic, "speed up")     # zrychli
            if char == "n": self.client.publish(self.topic, "speed down")   # zpomal
            if char == "w": self.client.publish(self.topic, "forward")      # dopředu
            if char == "a": self.client.publish(self.topic, "left")         # doleva
            if char == "s": self.client.publish(self.topic, "backward")     # zpátky
            if char == "d": self.client.publish(self.topic, "right")        # doprava
            if char == "x": self.client.publish(self.topic, "stop")         # zastav 
            if char == "l": self.client.publish(self.topic, "light")        # rozsviť světlo
            if char == "q": self.client.publish(self.topic, "auto")         # přepni na automat
            if char == "k": self.client.publish(self.topic, "XX")           # překážka před námi
            if char == "j": self.client.publish(self.topic, "XL")           # překážka, objeď ji vlevo
            if char == "l": self.client.publish(self.topic, "XR")           # překážka, objeď ji vpravo
            if char == "i": self.client.publish(self.topic, "OK")           # volno
            if char == "p": break

    def __init__(self):
        self.topic = "pasovec/kbd"
        self.client = mqtt.Client("kbd")
        self.client.connect("localhost")
        self.client.loop_start()
        self.run()

if __name__ == "__main__":
    x = Keyboard()

Ovládání motorů (motor.py):

from jetbot import Robot
import paho.mqtt.client as mqtt
import paho.mqtt.publish as publish

import random
import inspect
robot = Robot()

class Motor:
    def startAutomat(self):
        print("publish pasovec/motor/automat True");
        publish.single("pasovec/motor/automat", "True", hostname="localhost")
        self.automat = True;

    def stopAutomat(self):
        print("publish pasovec/motor/automat False");
        publish.single("pasovec/motor/automat", "False", hostname="localhost")
        self.automat = False;

    def on_message(self, client, userdata, message):
        payload = str(message.payload.decode('utf8'))
        topic = message.topic
        print(topic, " ", payload)

        if topic == "pasovec/camera":
            self.status = payload

        if topic == "pasovec/kbd":
            if payload == "forward":
                self.stopAutomat()
                self.fwspeed = abs(self.fwspeed)
                self.robot.forward(self.fwspeed)
                return
            if payload == "left":
                self.stopAutomat()
                self.robot.set_motors(-abs(self.fwspeed), abs(self.fwspeed));
                return
            if payload == "right":
                self.stopAutomat()
                self.robot.set_motors(abs(self.fwspeed), -abs(self.fwspeed));
                return
            if payload == "backward":
                self.stopAutomat()
                self.fwspeed = -abs(self.fwspeed)
                self.robot.forward(self.fwspeed)
                return
            if payload == "stop":
                self.stopAutomat()
                # self.fwspeed = 0.2
                self.robot.stop()
                return
            if payload == "speed up":
                self.fwspeed = self.fwspeed*1.1 if self.fwspeed < self.maxspeed/1.1 else self.maxspeed
                self.adjustSpeed()
                return
            if payload == "speed down":
                self.fwspeed = self.fwspeed/1.1 if self.fwspeed > 0.1 else 0.1
                self.adjustSpeed()
                return
            if payload == "auto":
                self.startAutomat()

        if self.automat:
            print ("automat", self.status, self.fwspeed)
            if self.status == "XX":
                direction = 1 if random.random() > 0.5 else -1
                self.robot.set_motors(direction*self.fwspeed, -direction*self.fwspeed)
                return
            if self.status == "XL":
                self.robot.set_motors(-self.fwspeed, self.fwspeed)
                return
            if self.status == "XR":
                self.robot.set_motors(self.fwspeed, -self.fwspeed)
                return
            if self.status == "OK":
                self.robot.set_motors(self.fwspeed, self.fwspeed)
                return

    def adjustSpeed(self):
        left = self.robot.left_motor.value
        right = self.robot.right_motor.value
        print ("adjustSpeed ", left, right, self.fwspeed)
        left = 0 if left == 0 else self.fwspeed if left > 0 else -self.fwspeed
        right = 0 if right == 0 else self.fwspeed if right > 0 else -self.fwspeed
        print ("adjustSpeed ", left, right)
        self.robot.left_motor.value = left
        self.robot.right_motor.value = right

    def run(self):
        self.client.loop_forever()

    def __init__(self):
        self.OK = False
        self.status = "OK"
        self.automat = True
        self.stopAutomat()
        self.maxspeed = 0.4
        self.fwspeed = 0.3;
        self.client = mqtt.Client("motor")
        self.client.connect("localhost")
        self.client.subscribe("pasovec/+")
        self.client.on_message = self.on_message
        self.robot = Robot()
        self.run()

if __name__ == "__main__":
    x = Motor()

V jednom terminálu si spusťe skript motor.py, v druhém terminálu skript keyboard.py a můžete jezdit!

Závěr

Pásovec by už nyní měl poslouchat naše povely. Zároveň máme vytvořený základ pro další pokračování. Příště si ukážeme, jak nasbírat data z kamer. K tomu si vytvoříme další samostatný skript, který přes gstreamer získá snímek z obou kamer, poskládá snímky do jednoho obrázku a odešle jej na PC, ve kterém budeme později trénovat neuronovou síť.

Hobrasoft s.r.o. | Kontakt