image


В этой части поговорим о программной составляющей, как оживлялась машинка. Какая ОС использовались, какой язык был выбран, с какими проблемами сталкивался.


1. Как работает в 2-х словах


Система состоит из сервера который установлен на машинке, и клиента который установлен на пульт. Сервер поднимает wifi точку доступа и ждет пока клиент не подключится. Сервер выполняет команды клиента, а так же передает на него видео с камеры.


2. ОС


Теперь поговорим об используемых операционных системах.
Поскольку вся система базируется на Raspberry pi 3, то и использовалась официальная ОС под неё. На момент создания последней версией была Stretch, она была и выбрана для использования на машинке и пульте управления. Но оказалось, что в ней есть баг(промучился неделю) из-за которого невозможно поднять wifi точку доступа. Поэтому для поднятия точки доступа была взята предыдущая версия Jessie не имевшая таких проблем.


Статья как поднять точку доступа. Очень подробная, делал все по ней.


Пульт автоматически подключается к машинке, когда она поднимает точку доступа.
Автоматическое подключение к нашей точке, в файл /etc/network/interfaces добавить:


auto wlan0
iface wlan0 inet dhcp 
wpa-ssid {ssid}
wpa-psk  {password}

2. Язык


Выбрал python потому что легко и просто.


3. Сервер


Под сервером в этом разделе будет иметься ввиду программное обеспечение написанное мной для управления машинкой и работой с видео.


Сервер состоит из 2-х частей. Видео сервера и сервера управления.


3.1 Видео сервер


Было 2 варианта, как работать с видео камерой. 1-ый использовать модуль picamera и 2-ой использовать ПО mjpg-streamer. Долго не думая я решил использовать их оба, а какой именно использовать вынести в настройки конфига.


if conf.conf.VideoServerType == 'm' :
    cmd = "cd /home/pi/projects/mjpg-streamer-experimental && "
    cmd += './mjpg_streamer -o "./output_http.so -p {0} -w ./www" -i "./input_raspicam.so -x {1} -y {2} -fps 25 -ex auto -awb auto -vs -ISO 10"'.format(conf.conf.videoServerPort, conf.conf.VideoWidth, conf.conf.VideoHeight)

    print(cmd)
    os.system(cmd)

else :
    with picamera.PiCamera(resolution = str(conf.conf.VideoWidth) + 'x' + str(conf.conf.VideoHeight) , framerate = conf.conf.VideoRate) as Camera:
        output = camera.StreamingOutput()
        camera.output = output
        Camera.start_recording(output, format = 'mjpeg')
        try:
            address = (conf.conf.ServerIP, conf.conf.videoServerPort)
            server = camera.StreamingServer(address, camera.StreamingHandler)
            server.serve_forever()
        finally:
            Camera.stop_recording()

Поскольку они берут одни и теже настройки, работают они на одном и том же адресе. Нет ни каких проблем при коммуникации с пультом при переходе с одного на другой. Единственное, как мне кажется mjpg-streamer работает побыстрее.


3.2 Сервер управления


3.2.1 Взаимодействие между клиентом и сервером


Сервер и клиент обмениваются командами в виде json строк:


{'type': 'remote', 'cmd': 'Start', 'status': True, 'val': 0.0}
{'type': 'remote', 'cmd': 'Y', 'status': True, 'val': 0.5}
{'type': 'remote', 'cmd': 'turn', 'x': 55, 'y': 32}

  • type — 'remote' или 'car' в зависимости от того кто шлет команду(клиент или сервер)
  • cmd — строка с именем кнопки, соответствующей имени кнопки на Game HAT, например:
    • Start — кнопка Start
    • Select — кнопка Select
    • Y — кнопка Y
    • и т.д.
    • turn — команда изменения состояния джойстика, отвечает за поворот колес
  • status — True или False, в зависимости от того нажата кнопка или отжата. Событие о статусе кнопки отправляется каждый раз когда меняется ее состояние.
  • val — скорость и направление движения мотора от -1...1, значение типа float. Значащий параметр только для кнопок движения.
  • x — отклонение джойстика по оси x от -100...100, значение типа int
  • y — отклонение джойстика по оси y от -100...100, значение типа int

Дальше идет мой позор, переделать который руки не доходят. Машинка поднимает server socket и ожидает пока к ней не подключится клиент. При чем для каждого нового подключения она создает отдельный поток, и каждый новый клиент который будет подключаться к машинке сможет управлять ей )). Такого пока не может быть потому что, больше нет такого пульта ни у кого, и я поднимаю свою закрытую wifi сеть.


def run(self): 
        TCP_IP = conf.conf.ServerIP
        TCP_PORT = conf.conf.controlServerPort
        BUFFER_SIZE = conf.conf.ServerBufferSize
        self.tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
        self.tcpServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
        self.tcpServer.bind((TCP_IP, TCP_PORT)) 
        threads = [] 

        # Максимальное колличество подключений в очереди.
        self.tcpServer.listen(1)
        while True:
            print("Car server up : Waiting for connections from TCP clients...") 
            (conn, (ip, port)) = self.tcpServer.accept() 
            newthread = ClientThread(conn, ip, port) 
            newthread.start() 
            self.threads.append(newthread)     

3.2.2 Управление железом


При работе с Raspberry использовалась система нумерации выводов GPIO.BCM.


Управление светом осуществляется через gpio 17, оно соединено со 2-ым пином на L293. Далее каждый раз как приходит команда включить:


GPIO.output(self.gpioLight, GPIO.HIGH)

GPIO.output(self.gpioLight, GPIO.LOW)

вызываются соответствующие команды.


Управление сервоприводом происходит через плату PCA9685 по I2C шине, поэтому нужна соответствующая библиотека для неё Adafruit_PCA9685. PCA9685 подключена к серво через 7 pin. Необходимая частота ШИМ для работы с серво составляет 50 Герц или период 20 мс.


Принцип работы серво:


image


При подаче сигнала длиной 1.5 мс колеса будут стоять по центру. При 1 мс. серво повернется максимально вправо, 2 мс. максимально влево. Поворотные кулаки в мостах на такие повороты не рассчитаны, поэтому угол поворота пришлось подбирать экспериментально.


Значения которые можно передавать в API Adafruit_PCA9685 составляют от 0..4095, 0 отсутствие сигнала, 4095 полное заполнение. Соответственно из этого диапазона нужно было подобрать значения подходящие для моих колес. Самое простое определить значения для ровно выставленных колес, это 1.5 мс перевести в значение из диапазона ~ 307.


Максимальное значение для права 245, для лева 369.


Значения приходящие от джойстика принимают значения от -100...100, поэтому их нужно было транслировать в диапазон от 245 до 369. Опять центр самое легкое, если 0 то это 307. Влево и вправо по формуле:


val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero))

  • HardwareSetting._turnCenter — 307
  • turn — значение от джойстика от -100...100
  • HardwareSetting._turnDelta — 62, разница между центром и максимальным отклонением в сторону (307 — 245 = 62)
  • HardwareSetting.yZero — 100, максимальное значение получаемое от джойстика

Колеса прямо:


def turnCenter(self):
        val = int(HardwareSetting._turnCenter)
        self.pwm_servo.set(val)
        CarStatus.statusCar['car']['turn'] = val

Поворот влево:


def turnLeft(self, turn):
        val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero))
        self.pwm_servo.set(val)
        CarStatus.statusCar['car']['turn'] = val   

Поворот вправо:


def turnRight(self, turn):
        val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero))
        self.pwm_servo.set(val)
        CarStatus.statusCar['car']['turn'] = val    

Управление двигателем происходит также через плату PCA9685 по I2C шине, поэтому используем Adafruit_PCA9685. Пины от 10 до 15 на PCA9685 подключены к L298N(использую на нем 2 канала, для увлечения мощности). 10 и 11 к ENA и ENB(наполняю их ШИМ-ом для регулирования скорости движения). 12, 13, 14, 15 к IN1, IN2, IN3, IN4 — отвечают за направление вращения мотора. Частота ШИМ здесь не особа важно, но я так же использую 50 Герц(мое значение по умолчанию).


Машинка стоит на месте:


def stop(self):
        """
        Остановка мотора.
        """

        self.pwm.set_pwm(self.ena, 0, self.LOW)
        self.pwm.set_pwm(self.enb, 0, self.LOW)

        self.pwm.set_pwm(self.in1, 0, self.LOW)
        self.pwm.set_pwm(self.in4, 0, self.LOW)
        self.pwm.set_pwm(self.in2, 0, self.LOW)
        self.pwm.set_pwm(self.in3, 0, self.LOW)

Движение вперед:


def back(self, speed):
        """
        Движение назад.

        Args:
            speed: Задаест скорость движение от 0 до 1.
        """

        self.pwm.set_pwm(self.ena, 0, int(speed * self.HIGH))
        self.pwm.set_pwm(self.enb, 0, int(speed * self.HIGH))

        self.pwm.set_pwm(self.in1, 0, self.LOW)
        self.pwm.set_pwm(self.in4, 0, self.LOW)
        self.pwm.set_pwm(self.in2, 0, self.HIGH)
        self.pwm.set_pwm(self.in3, 0, self.HIGH)   

Движение назад:


def forward(self, speed):
        """
        Движение вперед.

        Args:
            speed: Задаест скорость движение от 0 до 1.
        """

        self.pwm.set_pwm(self.ena, 0, int(speed * self.HIGH))
        self.pwm.set_pwm(self.enb, 0, int(speed * self.HIGH))

        self.pwm.set_pwm(self.in1, 0, self.HIGH)
        self.pwm.set_pwm(self.in4, 0, self.HIGH)
        self.pwm.set_pwm(self.in2, 0, self.LOW)
        self.pwm.set_pwm(self.in3, 0, self.LOW)    

4. Клиент


4.1 Клавиатура


С ней были определенные проблемы, вначале мне хотелось сделать её событийной(заняло ~ 2 недели мучений). Но механические кнопки внесли свою лепту, дребежание контактов приводило к постоянным и непредсказуемым сработкам(алгоритмы борьбы придуманные мной работали неидеально). Затем мой коллега рассказал мне как сделаны клавиатуры. И я решил сделать так же, теперь опрашиваю состояние каждые 0.005 секунды(почему так, а кто его знает). И если оно изменилось посылаю значение на сервер.


def run(self):
        try:
            while True:
                time.sleep(0.005)
                for pin in self.pins :
                    p = self.pins[pin]

                    status = p['status']

                    if GPIO.input(pin) == GPIO.HIGH :
                        p['status'] = False
                    else :
                        p['status'] = True

                    if p['status'] != status :
                        p['callback'](pin)

        except KeyboardInterrupt:
            GPIO.cleanup()

4.2 Джойстик


Чтение показаний происходит через плату ADS1115 по I2C шине, поэтому нужна соответствующая библиотека для неё Adafruit_PCA9685. Джойстик также подвержен дребезгу контактов, поэтому снимаю с него показания по аналогии с клавиатурой.


def run(self):
        while True:
            X = self.adc.read_adc(0, gain=self.GAIN) / HardwareSetting.valueStep
            Y = self.adc.read_adc(1, gain=self.GAIN) / HardwareSetting.valueStep

            if X > HardwareSetting.xZero :
                X = X - HardwareSetting.xZero
            else :
                X = -1 * (HardwareSetting.xZero - X)

            if Y > HardwareSetting.yZero :
                Y = Y - HardwareSetting.yZero
            else :
                Y = -1 * (HardwareSetting.yZero - Y)

            if (abs(X) < 5) :
                X = 0
            if (abs(Y) < 5) :
                Y = 0

            if (abs(self.x - X) >= 1.0 or abs(self.y - Y) >= 1.0) :
                self.sendCmd(round(X), round(Y))

            self.x = X
            self.y = Y

            time.sleep(0.005)

При питании от 3.3 вольт диапазон значений которые выдает ADS1115 с джойстика от 0...26500. Привожу это к диапазону от -100...100. В моем диапазоне в районе 0 он всегда колеблется, поэтому если значения не превышают 5, то я считаю что это 0 (иначе будет флудить). Как только значения изменяются, посылаю их машинке.


4.3 Подключение к серверу управления


Коннект к серверу простая вещь:


try :
    tcpClient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcpClient.settimeout(2.0)
    tcpClient.connect((conf.conf.ServerIP, conf.conf.controlServerPort))
    self.signalDisplayPrint.emit("У+")
    carStatus.statusRemote['network']['control'] = True

    self.tcpClient = tcpClient
except socket.error as e:
    self.signalDisplayPrint.emit("У-")
    carStatus.statusRemote['network']['control'] = False

    time.sleep(conf.conf.timeRecconect)
    self.tcpClient = None

    continue        

if self.tcpClient :
    self.tcpClient.settimeout(None)

Но хочу обратить внимание на одну вещь. Если не использовать timeout в коннекте, то он может подвиснуть и придется ждать порядка пары минут(такое бывает когда клиент запустился раньше сервера). Решил это следующим способом, устанавливаю timeout на соединение. Как только соединение происходит, то убираю timeout.


Так же я храню состояние соединения, что бы знать если будет потеряно управление и вывожу это на экран.


4.4 Проверка подключения к WiFi


Проверяю состояние wifi, на предмет подключения к серверу. И если, что так же уведомляю себя о проблемах.


def run(self):
        while True:
            time.sleep(1.0)

            self.ps = subprocess.Popen(['iwgetid'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

            try:
                output = subprocess.check_output(('grep', 'ESSID'), stdin=self.ps.stdout)
                if re.search(r'djvu-car-pi3', str(output)) :
                    self.sendStatus('wifi+')
                    continue

            except subprocess.CalledProcessError:
                pass

            self.sendStatus('wifi-')

            self.ps.kill()

4.5 Подключение к видео серверу


Для этого понадобилась вся мощь Qt5, кстати на дистрибутиве Stretch он поновее и на мой взгляд лучше показывает т.к. на Jessie я пробовал тоже.


Для отображения использовал:


self.videoWidget = QVideoWidget()


И на него вывел:


self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.LowLatency)
self.mediaPlayer.setVideoOutput(self.videoWidget)

Подключение к потоковому видео:


self.mediaPlayer.setMedia(QMediaContent(QUrl("http://{}:{}/?action=stream".format(conf.conf.ServerIP, conf.conf.videoServerPort))))
self.mediaPlayer.play()

Извиняюсь в очередной раз за тавтологию ). Контролирую состояние видео связи, на предмет подключения к видео серверу. И если, что так же уведомляю себя о проблемах.


Вот так выглядит когда все не работает:


image


  • W — означает, что нет соединения с wifi
  • В — означает, что нет видео
  • У — означает, что нет управления

Иначе нет красных букв, идет видео с камеры. Фотку и видео с работой выложу в дальнейшем ) Надеюсь, что крепление для камеры придет в ближайшее время и я её наконец прикреплю нормально.


5 Настройка Raspberry ОС


Кстати работу с камерой и прочими нужными вещами нужно включить(как на клиенте так и на сервере). После загрузки ОС:


image


И включаем почти все: камеру, ssh, i2c, gpio


image


Демонстрация


Нет только видео канала(камера на работе осталась). За его отсутствие извиняюсь, приделаю в понедельник.


Game HAT

Видео работы:



Исходные коды


Исходный код сервера и клиента
Пакет запуска сервера демоном


Ссылки


Часть 1

Комментарии (25)


  1. algotrader2013
    03.08.2019 09:47
    +2

    Что, даже видео, как машинка ездит, и как выглядит поездка со встроенной камеры не будет? После прочтения обоих частей чувствую себя обманутым)


    1. Jessy_James Автор
      03.08.2019 13:06

      Вставил, в конец статьи. С видео каналом сделаю в понедельник на работе. Если крепление для камеры сегодня с почты не заберу, посажу на металлический уголок ))


  1. einhander
    03.08.2019 10:21

    Классно! Присоединяюсь к предыдущему оратору по поводу видео, очень хочется посмотреть.
    Такой вопрос, зачем используется иксы, они же нагружают и без того не быструю малинку? Я не очень в курсе есть ли у малинки аппаратное сжатие видео, если есть используется ли?


    1. Jessy_James Автор
      03.08.2019 13:08

      X — потому что я использую Qt для отображения. За аппаратное сжатие уже не скажу(с видео разбирался где-то в феврале(многое позабыл)), но я пробовал камеру с аппаратным сжатием, на ней видео летало.


      1. alex1478
        03.08.2019 14:03

        Qt умеет рисовать во фреймбуфер


      1. einhander
        03.08.2019 14:22

        На контролеере понятно, что требуется отображение, а на машинке то зачем, или я не так понял скриншот.
        Какие задержки у видео, у сяоми микродрона были достаточно сильные, управлять не комфортно.


        1. Jessy_James Автор
          03.08.2019 14:25

          На машинке стоит видео камера, на пульте управления экран на который это видео с камеры выводится. На машинке нет отображения.

          Задержка есть, я бы её на глазок оценил до 0.5 секунды (в худшем случае, в нормальных условиях работы). Сильно тормозит когда вокруг много wifi точек.


    1. blacksoul000
      03.08.2019 22:25
      +1

      в h264 малинка умеет аппаратно жать


  1. fmkit
    03.08.2019 12:49
    +1

    машинка с камерой требует управления как танком чтоб можно было развернутся на месте
    серво на руле обычно у гоночных (не FPV) моделей
    мой вариант, на каждом колесе мотор:
    www.youtube.com/watch?v=UBO8Uwzi1Q8


    1. Alex_ME
      03.08.2019 20:48

      Какой у вас котейка красивый.


    1. zoldaten
      06.08.2019 13:53

      как вы сделали зум и микрофон повесили? есть описание проекта?


  1. trapwalker
    03.08.2019 18:32

    Можно было сделать сервер на машинке на каком-нибудь aiohttp. Управление и телеметрию туда-сюда слать через веб-сокеты. Тогда можно было бы управлять из браузера с любого смартфона.


    1. Jessy_James Автор
      03.08.2019 22:26

      Была такая мысль, месяца 1.5 назад. Возможно реализую её.


      1. Jessy_James Автор
        03.08.2019 23:09

        Точнее не мысль, мне так советовали )


  1. Alex_ME
    03.08.2019 20:53

    Возможно, этот момент освещался, но я пропустил. Почему бы не использовать смартфон для отображения видео, вместо малинки? Он и побыстрее, и не надо заморачиваться с малинкой и дисплеем. Малинки же у вас соединяются по WiFi, да? Управление либо вообще полностью с телефона, либо джойстики и прочие кнопки можно подключить (с помощью контроллера) к USB-OTG и там уже обрабатывать.


    1. Jessy_James Автор
      03.08.2019 22:29

      Тоже была такая идея вначале, управлять с телефона. Только я посчитал, что купить пульт для меня будет легче(поскольку там будет linux), чем начать учиться писать под android.


  1. achubutkin
    03.08.2019 22:27

    Были мысли добавить инерцию при остановке? Т.е. ее имитацию на уровне логики, для физически реалистичной остановки машинки весом в несколько тонн?


    1. Jessy_James Автор
      03.08.2019 22:30

      Проскакивала мысль, и это не так сложно сделать. Строчек в 20 кода можно уложиться.
      Возьму себе на заметку.


      1. vitsam
        03.08.2019 23:55

        В таком случае и в разгон надо добавить инерцию. А то очень резво дергается


        1. Jessy_James Автор
          03.08.2019 23:56

          Из-за этого сейчас проблема есть, крепление штатных мостов не выдерживает нагрузки с карданов.


          1. vitsam
            03.08.2019 23:58

            Да, мне тоже подумалось, что очень большие нагрузки при таких стартах.
            В остальном проект выглядит очень замечательно!


            1. Jessy_James Автор
              04.08.2019 00:04

              Только 4 снятое видео было удачным, на первых 3 карданы от мостов отлетали )) Пока со стоковыми мостами не заморачиваюсь, жду когда приедут металлические.


  1. njandieri
    04.08.2019 00:48

    Где вы приобрели металлические редукторы и дифференциалы? Дайте, пожалуйста, ссылку!



    1. njandieri
      04.08.2019 00:52

      Спасибо!