
Dans cette partie, nous parlerons du composant logiciel, de la façon dont la machine a pris vie. Quel système d'exploitation a été utilisé, quelle langue a été choisie, quels problèmes ont été rencontrés.
1. Comment ça marche en 2 mots
Le système se compose d'un serveur installé sur une machine à écrire et d'un client installé sur la console. Le serveur lève le point d'accès wifi et attend que le client se connecte. Le serveur exécute les commandes client et transmet également la vidéo de la caméra à celle-ci.
2. OS
Parlons maintenant des systèmes d'exploitation utilisés.
Étant donné que l'ensemble du système est basé sur Raspberry pi 3 , le système d'exploitation officiel a été utilisé. Au moment de la création, la dernière version était Stretch , elle était et a été sélectionnée pour être utilisée sur une machine à écrire et une télécommande. Mais il s'est avéré qu'il y a un bug (tourmenté pendant une semaine) à cause duquel il est impossible d'élever le point d'accès wifi. Par conséquent, pour augmenter le point d'accès, une version précédente de Jessie a été prise sans problèmes.
Article comment élever un point d'accès. Très détaillé, a tout fait dessus.
La télécommande se connecte automatiquement à la machine lorsqu'elle soulève le point d'accès.
Connexion automatique à notre point, dans le fichier / etc / network / interfaces ajoutez:
auto wlan0 iface wlan0 inet dhcp wpa-ssid {ssid} wpa-psk {password}
2. Langue
J'ai choisi python parce que c'est facile et simple.
3. Serveur
Par serveur dans cette section, je veux dire un logiciel écrit par moi pour contrôler la machine et travailler avec la vidéo.
Le serveur se compose de 2 parties. Serveur vidéo et serveur de gestion.
3.1 Serveur vidéo
Il y avait 2 options pour travailler avec une caméra vidéo. 1ère utilisation du module picamera et 2ème utilisation du logiciel mjpg-streamer . Sans y réfléchir à deux fois, j'ai décidé de les utiliser tous les deux, et lequel utiliser dans les paramètres de configuration.
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()
Puisqu'ils prennent les mêmes paramètres, ils travaillent à la même adresse. Il n'y a aucun problème de communication avec la télécommande lors du passage de l'un à l'autre. La seule chose que je pense que mjpg-streamer fonctionne plus rapidement.
3.2 Serveur de gestion
3.2.1 Interaction entre le client et le serveur
Les commandes d'échange serveur et client sous forme de chaînes 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 - 'distant' ou 'voiture' selon qui envoie la commande (client ou serveur)
- cmd - une chaîne avec le nom du bouton correspondant au nom du bouton sur le Game HAT , par exemple:
- Démarrer - bouton Démarrer
- Sélectionner - bouton Sélectionner
- Bouton Y - Y
- etc.
- tourner - la commande pour changer l'état du joystick, est responsable de tourner les roues
- état - Vrai ou faux, selon que le bouton est enfoncé ou non. Un événement d'état de bouton est distribué chaque fois que son état change.
- val - vitesse et sens de déplacement du moteur de -1 ... 1, valeur de type flotteur . Paramètre significatif pour les boutons de mouvement uniquement.
- x - écart du joystick le long de l'axe x de -100 ... 100, valeur de type int
- y - écart du joystick le long de l'axe y de -100 ... 100, valeur de type int
Vient ensuite ma honte, de refaire les mains qui n'atteignent pas. La machine soulève le socket du serveur et attend que le client s'y connecte. De plus, pour chaque nouvelle connexion, il crée un flux séparé, et chaque nouveau client qui se connectera à la machine pourra le contrôler)). Cela ne peut pas être si loin car personne d'autre n'a une telle télécommande, et je lève mon réseau wifi fermé.
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 = []
3.2.2 Gestion du fer
Lorsque vous travaillez avec Raspberry, le système de numérotation des broches GPIO.BCM a été utilisé.
La lumière est contrôlée via gpio 17, elle est connectée à la 2ème broche sur L293 . Ensuite, à chaque fois que la commande comprend:
GPIO.output(self.gpioLight, GPIO.HIGH)
GPIO.output(self.gpioLight, GPIO.LOW)
les commandes correspondantes sont appelées.
Le servo variateur est contrôlé via la carte PCA9685 via le bus I2C, nous avons donc besoin de la bibliothèque appropriée pour cela Adafruit_PCA9685 . Le PCA9685 est connecté au servo via 7 broches. La fréquence PWM requise pour travailler avec le servo est de 50 Hz ou une période de 20 ms.
Le principe de fonctionnement du servo:

Lorsqu'un signal de 1,5 ms est appliqué, les roues seront centrées. À 1 ms. le servo tournera le plus loin possible vers la droite, 2 ms. à gauche autant que possible. Les fusées de direction dans les ponts pour de tels virages ne sont pas conçues, donc l'angle de rotation a dû être sélectionné expérimentalement.
Les valeurs qui peuvent être transmises à la plage d'API Adafruit_PCA9685 vont de 0 à 4095, 0 aucun signal, 4095 plein. En conséquence, dans cette gamme, il a fallu choisir les valeurs adaptées à mes roues. Le moyen le plus simple de déterminer les valeurs pour des roues exactement définies est de transférer 1,5 ms à une valeur comprise entre ~ 307.
La valeur maximale pour la droite est 245, pour la gauche 369.
Les valeurs provenant du joystick prennent des valeurs de -100 ... 100, donc elles ont dû être traduites dans la plage de 245 à 369. Encore une fois, le centre est le plus facile, si 0 c'est 307. Gauche et droite selon la formule:
val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero))
- HardwareSetting._turnCenter - 307
- tour - valeur du joystick de -100 ... 100
- HardwareSetting._turnDelta - 62, la différence entre le centre et l'écart maximal sur le côté (307 - 245 = 62)
- HardwareSetting.yZero - 100, la valeur maximale reçue du joystick
Roues droites:
def turnCenter(self): val = int(HardwareSetting._turnCenter) self.pwm_servo.set(val) CarStatus.statusCar['car']['turn'] = val
Tournez à gauche:
def turnLeft(self, turn): val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero)) self.pwm_servo.set(val) CarStatus.statusCar['car']['turn'] = val
Tournez à droite:
def turnRight(self, turn): val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero)) self.pwm_servo.set(val) CarStatus.statusCar['car']['turn'] = val
Le contrôle du moteur s'effectue également via la carte PCA9685 via le bus I2C, nous utilisons donc Adafruit_PCA9685 . Les broches 10 à 15 du PCA9685 sont connectées au L298N (j'utilise 2 canaux dessus pour absorber la puissance). 10 et 11 à ENA et ENB (je les remplis de PWM pour contrôler la vitesse). 12, 13, 14, 15 à IN1, IN2, IN3, IN4 - sont responsables du sens de rotation du moteur. La fréquence PWM n'est pas très importante ici, mais j'utilise également 50 Hertz (ma valeur par défaut).
La machine reste immobile:
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)
Aller de l'avant:
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)
Mouvement vers l'arrière:
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. Client
4.1 Clavier
Il y avait certains problèmes avec elle, au début, je voulais la rendre mouvementée (a pris environ 2 semaines de tourments). Mais les boutons mécaniques ont contribué, le cliquetis des contacts a conduit à des défaillances constantes et imprévisibles (les algorithmes de contrôle que j'ai inventés fonctionnaient imparfaitement). Ensuite, mon collègue m'a dit comment les claviers sont fabriqués. Et j'ai décidé de faire de même, maintenant j'interroge l'état toutes les 0,005 secondes (pourquoi, et qui sait). Et s'il a changé, envoyez la valeur au serveur.
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 Joystick
La lecture des lectures a lieu via la carte ADS1115 via le bus I2C, par conséquent, la bibliothèque appropriée est Adafruit_PCA9685 . Le joystick est également enclin à claquer des contacts, donc j'en prends des lectures par analogie avec le clavier.
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)
Lorsqu'il est alimenté à partir de 3,3 volts, la plage de valeurs que l'ADS1115 donne avec un joystick de 0 à 26500. J'apporte cela à la gamme de -100 ... 100. Dans ma plage autour de 0, il fluctue toujours, donc si les valeurs ne dépassent pas 5, alors je considère que c'est 0 (sinon il va inonder). Dès que les valeurs changent, envoyez-les à la machine à écrire.
4.3 Connexion au serveur de gestion
La connexion au serveur est une chose simple:
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)
Mais je veux faire attention à une chose. Si vous n'utilisez pas de délai d'expiration dans la connexion, il peut se figer et attendre environ deux minutes (cela se produit lorsque le client a démarré avant le serveur). J'ai résolu cela de la manière suivante, j'ai défini le délai d'expiration de la connexion. Dès que la connexion a lieu, je supprime le délai d'expiration.
Je stocke également l'état de la connexion, afin de savoir si le contrôle est perdu et de l'afficher à l'écran.
4.4 Vérification de la connexion WiFi
Je vérifie l'état du wifi pour la connexion au serveur. Et si, que je m'informe également des problèmes.
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 Connexion à un serveur vidéo
Pour cela, toute la puissance de Qt5 était nécessaire , d'ailleurs sur la distribution Stretch, elle est plus récente et à mon avis montre mieux. sur jessie j'ai essayé aussi.
Pour l'affichage, j'ai utilisé:
self.videoWidget = QVideoWidget()
Et il en a déduit:
self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.LowLatency) self.mediaPlayer.setVideoOutput(self.videoWidget)
Connexion à la vidéo en streaming:
self.mediaPlayer.setMedia(QMediaContent(QUrl("http://{}:{}/?action=stream".format(conf.conf.ServerIP, conf.conf.videoServerPort)))) self.mediaPlayer.play()
Encore une fois, je m'excuse pour la tautologie). Je surveille l'état de la connexion vidéo pour la connexion au serveur vidéo. Et si, que je m'informe également des problèmes.
Voici à quoi cela ressemble quand tout ne fonctionne pas:

- W - signifie qu'il n'y a pas de connexion avec le wifi
- B - signifie pas de vidéo
- Y - signifie qu'il n'y a pas de contrôle
Sinon, il n'y a pas de lettres rouges, il y a une vidéo de la caméra. Je posterai une photo et une vidéo avec le travail dans le futur) J'espère que le support pour la caméra viendra dans un proche avenir et je vais enfin le fixer normalement.
5 Configuration du système d'exploitation Raspberry
Soit dit en passant, le travail avec la caméra et les autres choses nécessaires doivent être activés (à la fois sur le client et sur le serveur). Après avoir chargé l'OS:

Et allumez presque tout: appareil photo, ssh, i2c, gpio

Démonstration
Il n'y a qu'un canal vidéo (la caméra reste au travail). Je m'excuse de son absence, je la joindrai lundi.

Travail vidéo:
Code source
Code source du serveur et du client
Package de démarrage du serveur démon
Les références
Partie 1
3e partie