
En esta parte hablaremos sobre el componente de software, cómo la máquina cobró vida. Qué sistema operativo se utilizó, qué idioma se eligió, qué problemas se enfrentaron.
1. Cómo funciona en 2 palabras
El sistema consta de un servidor que está instalado en una máquina de escribir y un cliente que está instalado en la consola. El servidor eleva el punto de acceso wifi y espera hasta que el cliente se conecta. El servidor ejecuta comandos del cliente y también transmite video desde la cámara.
2. OS
Ahora hablemos sobre los sistemas operativos utilizados.
Dado que todo el sistema se basa en Raspberry pi 3 , se utilizó el sistema operativo oficial. En el momento de la creación, la última versión era Stretch , fue y fue seleccionada para su uso en una máquina de escribir y un panel de control. Pero resultó que tiene un error (atormentado durante una semana) debido a que es imposible elevar el punto de acceso wifi. Por lo tanto, para elevar el punto de acceso, se tomó una versión anterior de Jessie que no tenía tales problemas.
Artículo sobre cómo elevar un punto de acceso. Muy detallado, hizo todo en él.
El control remoto se conecta automáticamente a la máquina cuando levanta el punto de acceso.
Conexión automática a nuestro punto, en el archivo / etc / network / interfaces agregue:
auto wlan0 iface wlan0 inet dhcp wpa-ssid {ssid} wpa-psk {password}
2. Idioma
Elegí python porque es fácil y simple.
3. Servidor
Por servidor en esta sección me referiré al software escrito por mí para controlar la máquina y trabajar con video.
El servidor consta de 2 partes. Servidor de video y servidor de gestión.
3.1 Servidor de video
Había 2 opciones de cómo trabajar con una cámara de video. Primer uso del módulo picamera y segundo uso del software mjpg-streamer . Sin pensarlo dos veces, decidí usar ambos y cuál usar en la configuración.
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()
Como toman la misma configuración, funcionan en la misma dirección. No hay problemas para comunicarse con el control remoto al cambiar de uno a otro. Lo único que creo que mjpg-streamer funciona más rápido.
3.2 Servidor de gestión
3.2.1 Interacción entre cliente y servidor
El servidor y el cliente intercambian comandos en forma de cadenas 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}
- tipo - 'remoto' o 'auto' dependiendo de quién envía el comando (cliente o servidor)
- cmd: una cadena con el nombre del botón correspondiente al nombre del botón en el SOMBRERO del juego , por ejemplo:
- Inicio - Botón de inicio
- Seleccionar - Botón Seleccionar
- Y - botón Y
- etc.
- girar - el comando para cambiar el estado del joystick, es responsable de girar las ruedas
- estado: verdadero o falso, dependiendo de si el botón está presionado o no. Se envía un evento de estado de botón cada vez que cambia su estado.
- val - velocidad y dirección de movimiento del motor desde -1 ... 1, valor de tipo float . Parámetro significativo solo para botones de movimiento.
- x - desviación del joystick a lo largo del eje x de -100 ... 100, valor de tipo int
- y - desviación del joystick a lo largo del eje y de -100 ... 100, valor de tipo int
Luego viene mi vergüenza, rehacer qué manos no alcanzan. La máquina eleva el socket del servidor y espera hasta que el cliente se conecta a él. Además, para cada nueva conexión, crea una secuencia separada, y cada nuevo cliente que se conectará a la máquina podrá controlarla)). Esto no puede estar tan lejos porque nadie más tiene ese control remoto, y levanto mi red wifi cerrada.
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 Gestión del hierro
Al trabajar con Raspberry, se utilizó el sistema de numeración de pin GPIO.BCM .
La luz se controla a través de gpio 17, está conectada al segundo pin en L293 . A continuación, cada vez que el comando viene a incluir:
GPIO.output(self.gpioLight, GPIO.HIGH)
GPIO.output(self.gpioLight, GPIO.LOW)
Se llaman los comandos correspondientes.
El servodrive se controla a través de la placa PCA9685 a través del bus I2C, por lo que necesitamos la biblioteca adecuada para ello Adafruit_PCA9685 . PCA9685 está conectado al servo a través de 7 pines. La frecuencia PWM requerida para trabajar con servo es de 50 Hz o un período de 20 ms.
El principio de funcionamiento del servo:

Cuando se aplica una señal de 1.5 ms, las ruedas estarán centradas. A 1 ms. el servo girará lo más posible hacia la derecha, 2 ms. a la izquierda tanto como sea posible. Los nudillos de dirección en los puentes para tales giros no están diseñados, por lo que el ángulo de rotación tuvo que seleccionarse experimentalmente.
Los valores que se pueden pasar a la API Adafruit_PCA9685 van desde 0..4095, 0 sin señal, 4095 lleno. En consecuencia, de este rango fue necesario elegir los valores adecuados para mis ruedas. La forma más fácil de determinar los valores para ruedas establecidas exactamente es transferir 1,5 ms a un valor del rango de ~ 307.
El valor máximo para la derecha es 245, para la izquierda 369.
Los valores que provienen del joystick toman valores de -100 ... 100, por lo que tuvieron que traducirse en el rango de 245 a 369. Nuevamente, el centro es el más fácil, si 0 es 307. Izquierda y derecha según la fórmula:
val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero))
- HardwareSetting._turnCenter - 307
- giro - valor del joystick desde -100 ... 100
- HardwareSetting._turnDelta - 62, la diferencia entre el centro y la desviación máxima hacia un lado (307 - 245 = 62)
- HardwareSetting.yZero - 100, el valor máximo recibido del joystick
Ruedas rectas:
def turnCenter(self): val = int(HardwareSetting._turnCenter) self.pwm_servo.set(val) CarStatus.statusCar['car']['turn'] = val
Girar a la izquierda:
def turnLeft(self, turn): val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero)) self.pwm_servo.set(val) CarStatus.statusCar['car']['turn'] = val
Girar a la derecha:
def turnRight(self, turn): val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero)) self.pwm_servo.set(val) CarStatus.statusCar['car']['turn'] = val
El control del motor también se realiza a través de la placa PCA9685 a través del bus I2C, por lo que utilizamos Adafruit_PCA9685 . Los pines 10 a 15 en el PCA9685 están conectados al L298N (uso 2 canales para absorber energía). 10 y 11 a ENA y ENB (los relleno con PWM para controlar la velocidad). 12, 13, 14, 15 a IN1, IN2, IN3, IN4: son responsables de la dirección de rotación del motor. La frecuencia PWM no es muy importante aquí, pero también uso 50 Hertz (mi valor predeterminado).
La máquina se detiene:
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)
Avanzando:
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)
Movimiento hacia atrás:
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. Cliente
4.1 Teclado
Hubo ciertos problemas con ella, al principio quería que estuviera llena de acontecimientos (tardó ~ 2 semanas de tormento). Pero los botones mecánicos contribuyeron, el traqueteo de los contactos condujo a fallas constantes e impredecibles (los algoritmos de control que inventé funcionaron de manera imperfecta). Entonces mi colega me contó cómo se hacen los teclados. Y decidí hacer lo mismo, ahora sondeo el estado cada 0.005 segundos (por qué y quién sabe). Y si ha cambiado, envíe el valor al servidor.
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 lectura de las lecturas se realiza a través de la placa ADS1115 a través del bus I2C, por lo tanto, la biblioteca adecuada es Adafruit_PCA9685 . El joystick también es propenso a charlar, por lo que tomo lecturas por analogía con el teclado.
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)
Cuando se alimenta desde 3.3 voltios, el rango de valores que el ADS1115 entrega con un joystick de 0 ... 26500. Traigo esto al rango de -100 ... 100. En mi rango alrededor de 0 siempre fluctúa, por lo que si los valores no exceden de 5, entonces considero que es 0 (de lo contrario, se inundará). Tan pronto como cambien los valores, envíelos a la máquina de escribir.
4.3 Conexión al servidor de administración
Conectarse al servidor es una cosa 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)
Pero quiero prestar atención a una cosa. Si no usa el tiempo de espera en la conexión, se puede congelar y tendrá que esperar unos minutos (esto sucede cuando el cliente se inició antes que el servidor). Resolví esto de la siguiente manera, configuré el tiempo de espera para la conexión. Tan pronto como se produce la conexión, elimino el tiempo de espera.
También almaceno el estado de la conexión, para saber si se pierde el control y mostrarlo en la pantalla.
4.4 Comprobación de la conexión WiFi
Compruebo el estado de wifi para la conexión al servidor. Y si, eso también me notifico de problemas.
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 Conexión a un servidor de video
Para esto, se necesitaba todo el poder de Qt5 , por cierto en la distribución Stretch es más nuevo y en mi opinión se muestra mejor. en Jessie también lo intenté.
Para la exhibición usé:
self.videoWidget = QVideoWidget()
Y dedujo:
self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.LowLatency) self.mediaPlayer.setVideoOutput(self.videoWidget)
Conexión a la transmisión de video:
self.mediaPlayer.setMedia(QMediaContent(QUrl("http://{}:{}/?action=stream".format(conf.conf.ServerIP, conf.conf.videoServerPort)))) self.mediaPlayer.play()
Una vez más, me disculpo por la tautología). Superviso el estado de la conexión de video para la conexión al servidor de video. Y si, eso también me notifico de problemas.
Así es como se ve cuando todo no funciona:

- W : significa que no hay conexión con wifi
- B - significa que no hay video
- Y - significa que no hay control
De lo contrario, no hay letras rojas, hay un video de la cámara. Publicaré una foto y un video con el trabajo en el futuro) Espero que el soporte para la cámara venga en un futuro próximo y finalmente lo adjuntaré normalmente.
5 Configuración del sistema operativo Raspberry
Por cierto, el trabajo con la cámara y otras cosas necesarias deben estar activadas (tanto en el cliente como en el servidor). Después de cargar el sistema operativo:

Y encienda casi todo: cámara, ssh, i2c, gpio

Demostración
Solo hay un canal de video (la cámara permanece en funcionamiento). Pido disculpas por su ausencia, lo adjuntaré el lunes.

Video de trabajo:
Código fuente
Código fuente del servidor y del cliente
Paquete de inicio del servidor Daemon
Referencias
Parte 1
Parte 3