Хабрахабр

Игрушка ГАЗ-66 на пульте управления. Часть 2

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

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

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

2. ОС

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

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

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

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

2. Язык

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

3. Сервер

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

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

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

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

`
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 Управление железом

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

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

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

или выключить:

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

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

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

5 мс колеса будут стоять по центру. При подаче сигнала длиной 1. серво повернется максимально вправо, 2 мс. При 1 мс. Поворотные кулаки в мостах на такие повороты не рассчитаны, поэтому угол поворота пришлось подбирать экспериментально.
Значения которые можно передавать в API Adafruit_PCA9685 составляют от 0.. максимально влево. Соответственно из этого диапазона нужно было подобрать значения подходящие для моих колес. 4095, 0 отсутствие сигнала, 4095 полное заполнение. 5 мс перевести в значение из диапазона ~ 307.
Максимальное значение для права 245, для лева 369. Самое простое определить значения для ровно выставленных колес, это 1.

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

Пины от 10 до 15 на PCA9685 подключены к L298N(использую на нем 2 канала, для увлечения мощности). Управление двигателем происходит также через плату PCA9685 по I2C шине, поэтому используем Adafruit_PCA9685. 12, 13, 14, 15 к IN1, IN2, IN3, IN4 — отвечают за направление вращения мотора. 10 и 11 к ENA и ENB(наполняю их ШИМ-ом для регулирования скорости движения). Частота ШИМ здесь не особа важно, но я так же использую 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 вольт диапазон значений которые выдает ADS1115 с джойстика от 0... При питании от 3. Привожу это к диапазону от -100... 26500. В моем диапазоне в районе 0 он всегда колеблется, поэтому если значения не превышают 5, то я считаю что это 0 (иначе будет флудить). 100. Как только значения изменяются, посылаю их машинке.

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 Подключение к видео серверу

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

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

`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()
`

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

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

remote

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

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

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

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

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

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

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

Ссылки

Часть 1

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть