
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 = []
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:

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:

- 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:

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

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.

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