Brinquedo GAZ-66 no painel de controle. Parte 2

imagem


Nesta parte, falaremos sobre o componente de software, como a máquina ganhou vida. Qual sistema operacional foi usado, qual idioma foi escolhido, quais problemas foram enfrentados.


1. Como funciona em 2 palavras


O sistema consiste em um servidor que está instalado em uma máquina de escrever e um cliente que está instalado no console. O servidor eleva o ponto de acesso wifi e aguarda até o cliente se conectar. O servidor executa comandos do cliente e também transmite o vídeo da câmera para ela.


2. SO


Agora vamos falar sobre os sistemas operacionais usados.


Como todo o sistema é baseado no Raspberry pi 3 , o sistema operacional oficial foi usado. No momento da criação, a versão mais recente era Stretch , era e foi selecionada para uso em uma máquina de escrever e controle remoto. Mas aconteceu que ele tem um bug (atormentado por uma semana) por causa do qual é impossível aumentar o ponto de acesso wifi. Portanto, para aumentar o ponto de acesso, foi usada uma versão anterior de Jessie que não apresentava esses problemas.


Artigo como aumentar um ponto de acesso. Muito detalhado, fez tudo.


O controle remoto se conecta automaticamente à máquina quando eleva o ponto de acesso.
A conexão automática ao nosso ponto, no arquivo / etc / network / interfaces, adiciona:


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

2. Idioma


Eu escolhi o python porque é fácil e simples.


3. Servidor


Por servidor nesta seção, quero dizer software escrito por mim para controlar a máquina e trabalhar com vídeo.


O servidor consiste em 2 partes. Servidor de vĂ­deo e servidor de gerenciamento.


3.1 Servidor de vĂ­deo


Havia 2 opções de como trabalhar com uma câmera de vídeo. 1º uso do módulo picamera e 2º uso do software mjpg-streamer . Sem pensar duas vezes, decidi usar os dois e qual deles usar nas configurações.


 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 eles seguem as mesmas configurações, eles trabalham no mesmo endereço. Não há problemas na comunicação com o controle remoto ao alternar de um para outro. A única coisa que acho que o mjpg-streamer funciona mais rápido.


3.2 Servidor de Gerenciamento


3.2.1 Interação entre cliente e servidor


O servidor e o cliente trocam comandos na forma de json strings:


 {'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' ou 'carro', dependendo de quem envia o comando (cliente ou servidor)
  • cmd - uma string com o nome do botĂŁo correspondente ao nome do botĂŁo no Game HAT , por exemplo:
    • BotĂŁo Iniciar - Iniciar
    • BotĂŁo Selecionar - Selecionar
    • BotĂŁo Y - Y
    • etc.
    • turn - o comando para alterar o estado do joystick, Ă© responsável por girar as rodas
  • status - Verdadeiro ou Falso, dependendo de o botĂŁo estar pressionado ou nĂŁo. Um evento de status do botĂŁo Ă© despachado toda vez que seu estado muda.
  • val - velocidade e direção do movimento do motor de -1 ... 1, valor do tipo float . Parâmetro significativo apenas para botões de movimento.
  • x - desvio do joystick ao longo do eixo x de -100 ... 100, valor do tipo int
  • y - desvio do joystick ao longo do eixo y de -100 ... 100, valor do tipo int

Em seguida, vem a minha vergonha, refazer quais mãos não alcançam. A máquina levanta o soquete do servidor e aguarda até que o cliente se conecte a ele. Além disso, para cada nova conexão, ele cria um fluxo separado, e cada novo cliente que se conectar à máquina poderá controlá-la)). Isso não pode ser tão longe, porque ninguém mais tem esse controle remoto, e eu levanto minha rede wifi fechada.


 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 Gerenciamento de ferro


Ao trabalhar com o Raspberry, foi utilizado o sistema de numeração de pinos GPIO.BCM .


A luz Ă© controlada via gpio 17, Ă© conectada ao segundo pino no L293 . Em seguida, sempre que o comando incluir:


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

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

comandos correspondentes sĂŁo chamados.


O servoconversor é controlado através da placa PCA9685 através do barramento I2C, por isso precisamos da biblioteca apropriada Adafruit_PCA9685 . O PCA9685 está conectado ao servo via 7 pinos. A frequência PWM necessária para trabalhar com servo é de 50 Hz ou um período de 20 ms.


O princípio de operação do servo:


imagem


Quando um sinal de 1,5 ms é aplicado, as rodas serão centralizadas. Em 1 ms. o servo girará o mais longe possível para a direita, 2 ms. para a esquerda, tanto quanto possível. As juntas de direção nas pontes para essas voltas não são projetadas, portanto o ângulo de rotação teve que ser selecionado experimentalmente.


Os valores que podem ser passados ​​para a API Adafruit_PCA9685 variam de 0..4095, 0 sem sinal, 4095 cheio. Portanto, nessa faixa, foi necessário escolher os valores adequados para minhas rodas. A maneira mais fácil de determinar os valores para as rodas exatamente definidas é transferir 1,5 ms para um valor na faixa de ~ 307.


O valor máximo para a direita é 245, para a esquerda 369.


Os valores provenientes do joystick assumem valores de -100 ... 100, portanto, eles tiveram que ser traduzidos no intervalo de 245 a 369. Novamente, o centro é o mais fácil, se 0 for 307. Esquerda e direita de acordo com a fórmula:


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

  • HardwareSetting._turnCenter - 307
  • turn - valor do joystick de -100 ... 100
  • HardwareSetting._turnDelta - 62, a diferença entre o centro e o desvio máximo para o lado (307 - 245 = 62)
  • HardwareSetting.yZero - 100, o valor máximo recebido do joystick

Rodas retas:


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

Vire Ă  esquerda:


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

Vire Ă  direita:


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

O controle do motor também ocorre através da placa PCA9685 através do barramento I2C, por isso usamos Adafruit_PCA9685 . Os pinos 10 a 15 no PCA9685 estão conectados ao L298N (eu uso 2 canais nele para absorver energia). 10 e 11 para ENA e ENB (eu os preencho com PWM para controlar a velocidade). 12, 13, 14, 15 a IN1, IN2, IN3, IN4 - são responsáveis ​​pelo sentido de rotação do motor. A frequência PWM não é muito importante aqui, mas eu também uso 50 Hertz (meu valor padrão).


A máquina fica parada:


 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) 

Avançando:


 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) 

Movimento para trá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


Havia alguns problemas com ela, no começo eu queria torná-la agitada (foram necessárias ~ 2 semanas de tormento). Mas os botões mecânicos contribuíram, o barulho dos contatos levou a falhas constantes e imprevisíveis (os algoritmos de controle que eu inventei funcionavam imperfeitamente). Então meu colega me contou como os teclados são feitos. E eu decidi fazer o mesmo, agora eu pesquiso o estado a cada 0.005 segundos (por que sim e quem sabe). E se ele mudou, envie o valor ao 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


A leitura das leituras ocorre através da placa ADS1115 através do barramento I2C; portanto, a biblioteca apropriada para ela é Adafruit_PCA9685 . O joystick também é propenso a contatos trepidantes, por isso tomo leituras dele por analogia com o 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) 

Quando alimentado a partir de 3,3 volts, a faixa de valores que o ADS1115 fornece com um joystick de 0 a 26500. Trago isso para o intervalo de -100 ... 100. No meu intervalo em torno de 0, ele sempre flutua; portanto, se os valores não excederem 5, considero que é 0 (caso contrário, ele inundará). Assim que os valores mudarem, envie-os para a máquina de escrever.


4.3 Conectando ao servidor de gerenciamento


Conectar-se ao servidor Ă© uma coisa simples:


 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) 

Mas quero prestar atenção a uma coisa. Se você não usar o tempo limite na conexão, ele poderá congelar e você terá que esperar alguns minutos (isso acontece quando o cliente foi iniciado antes do servidor). Resolvi isso da seguinte maneira, defino o tempo limite para a conexão. Assim que a conexão ocorre, removo o tempo limite.


Também guardo o estado da conexão, para saber se o controle está perdido e exibi-lo na tela.


4.4 Verificando a conexĂŁo WiFi


Verifico o status do wifi para conexão com o servidor. E se eu também 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 Conectando a um servidor de vĂ­deo


Para isso, todo o poder do Qt5 era necessário , a propósito, na distribuição Stretch , é mais recente e, na minha opinião, mostra-se melhor. Jessie eu tentei também.


Para exibição eu usei:


 self.videoWidget = QVideoWidget() 

E ele deduziu:


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

ConexĂŁo ao streaming de vĂ­deo:


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

Mais uma vez, peço desculpas pela tautologia). Monitoro o status da conexão de vídeo para conexão com o servidor de vídeo. E se eu também me notifico de problemas.


É assim que parece quando tudo não funciona:


imagem


  • W - significa que nĂŁo há conexĂŁo com wifi
  • B - significa que nĂŁo há vĂ­deo
  • Y - significa que nĂŁo há controle

Caso contrário, não há letras vermelhas, há um vídeo da câmera. Vou postar uma foto e um vídeo com o trabalho no futuro) Espero que a montagem da câmera chegue no futuro próximo e finalmente a anexarei normalmente.


5 Configurando o Raspberry OS


A propósito, o trabalho com a câmera e outras coisas necessárias devem estar ativadas (no cliente e no servidor). Depois de carregar o sistema operacional:


imagem


E ligue quase tudo: camera, ssh, i2c, gpio


imagem


Demonstração


Existe apenas um canal de vídeo (a câmera permanece em funcionamento). Peço desculpas por sua ausência, vou anexá-lo na segunda-feira.


Chapéu de jogo

Trabalho em vĂ­deo:



CĂłdigo fonte


CĂłdigo-fonte do servidor e do cliente
Pacote de inicialização do servidor Daemon


ReferĂŞncias


Parte 1
Parte 3

Source: https://habr.com/ru/post/pt462331/


All Articles