Site menu IoT: controle remoto 315/433MHz

IoT: controle remoto 315/433MHz

Mesmo em face de tecnologias melhores como ZigBee, o bom e velho controle remoto de 315 ou 433MHz ainda é muito utilizado em portões de garagem e automação residencial. O hardware é barato, funciona surpreendentemente bem (embora não seja muito seguro) e é fácil entendê-lo e brincar com ele.

Tecnologias

Na maior parte do mundo, inclusive no Brasil, os "chaveiros" ou keyfobs de controle remoto funcionam na banda de 433MHz e são sintonizados em 433,92MHz. Em alguns países, eles estão na banda de 315MHz e são sintonizados em 314,0MHz ou 313,1MHz. Em todo caso, essas bandas de rádio têm bom alcance e penetração.

Em algumas áreas (inclusive no Brasil) modulação de banda larga não é permitida nessas bandas, então os keyfobs digitais têm de usar alguma modulação de banda estreita, como ASK/OOK, ASK ou FSK. A maioria usa OOK.

Os sistemas de controle remoto digitais são resistentes a inteferência. Já a resistência a colisão (ou seja, a chance de um keyfob aleatório acionar uma estação) depende do comprimento do código enviado, que pode variar de 8 a 24 bits, a depender do chip. Um punhado de chips (EV1527, PT2262, HT12E, HT6P20) basicamente dominam esse mercado.

Os keyfobs de código fixo são ainda largamente usados, mas eles são presa fácil de ataques de repetição. Também não são bons para estações compartilhadas por centenas de usuários (e.g. garagem de um edifício comercial ou condomínio) pois não é possível revogar keyfobs individuais.

Tecnologias "rolling code" ou código rotativo dificultam o ataque de repetição enviando um código diferente a cada transmissão, usando algum método criptográfico para que a estação consiga discernir o transmissor. Um chip muito popular é o HCS301, embora sua criptografia já tenha sido quebrada.

A solução perfeita envolve comunicação bidirecional entre keyfob e estação. É o que os keyfobs Zigbee fazem, mas eles mais caros, mais difíceis de obter e mais difíceis de trabalhar (ZigBee é um pesadelo de interoperabilidade).

Assim, ficamos com a tecnologia de código fixo por ser barata e fácil de trabalhar no contexto maker/IoT. Há uma grande variedade de módulos OOK para Arduino, todos baratos e fáceis de obter. A segurança (ou melhor, a falta dela) não é um problema se o controle remoto não controla nenhum recurso crítico.

Se você quer experimentar com esses módulos, considere adquirir um analisador lógico, que facilita muito a depuração.

Figura 1: Receptor 433MHz conectado a um analisador lógico.

Um pouco sobre a modulação ASK/OOK

No reino dos keyfobs digitais de código fixo, a modulação ASK/OOK é rainha. A sigla significa amplitude modulada / chaveamento on/off. O transmissor só pode emitir uma portadora fixa, ou silenciar. A presença de portadora significa "bit 1", a ausência dela significa "bit 0". É o equivalente digital do código Morse.

Um transmissor OOK pode ser muito simples, às custas de maior complexidade na recepção (RX). O problema do lado RX é distinguir "bit 0" de "transmissão não existe". Quase todos os receptores possuem AGC (controle automático de ganho). Quando não há um transmissor por perto, eles aumentam o ganho até receber algum sinal, seja qual for, que provavelmente será ruído aleatório.

Em código Morse, o telegrafista envia pontos e traços cuja duração é mais ou menos de conhecimento de todos os interlocutores. De forma análoga, os keyfobs OOK empregam seqüências de chirps, isto é, grupos de bits físicos que representam os bits lógicos do código a ser transmitido.

As seqüências de chirps são de tal forma que o receptor tem uma boa chance de distinguir sinal de ruído. O protocolo do chip EV1527 é:

O chip HT6P20, também largamente utilizado, tem um protocolo semelhante:

Outros chips implementam variações desse mesmo tema. A biblioteca Arduino rc-switch é uma referência valiosa na hora de tentar identificar o protocolo de um keyfob desconhecido.

A maioria, se não todos, os keyfobs enviam múltiplas vezes o mesmo código a fim de aumentar a chance de recepção bem-sucedida, mesmo num pressionamento curto do botão.

Recepção

SYN480R e RXB-8 são dois módulos Arduino que recebem sinais OOK. Ambos têm excelente custo/benefício, sendo o RXB-8 mais sensível.

Eles lidam com toda a parte de rádio e têm um único pino de saída digital que diz "há portadora" ou "não há portadora". Podemos conectar esse pino diretamente a um analisador de sinal e ver o que aparece, conforme a imagem abaixo:

Figura 2: Saída de um módulo receptor e.g. SYN480R ou RXB-8. Lado esquerdo: recebendo ruído. Lado direito: recebendo sinal legítimo.

Do lado esquerdo, podemos ver que, na ausência de transmissão, o receptor aumenta a sensibilidade e aceita ruído. Sabemos que é ruído porque é um sinal de freqüência mais alta que a esperada, e visivelmente aleatório. Em torno do instante +1126ms, um keyfob envia o preâmbulo que calibra a sensibilidade.

Do lado direito, com o AGC calibrado, o receptor consegue distinguir a presença ou ausência da portadora. A forma de onda é regular, com os chirps apresentando o comprimento esperado.

O longo silêncio no preâmbulo é uma forma simples e efetiva de detectar "início de pacote", pois é quase impossível que aconteça por acaso. O "fim de pacote" pode ser igualmente detectado pelo silêncio pós-transmissão.

Os chirps têm um comprimento dentro de uma faixa bem definida, entre 200µs e 600µs para keyfobs típicos. O decodificador deve checar se os chirps estão dentro da faixa, e também se eles têm comprimento estável dentro de um pacote. Do contrário, o decodificador pode ser enganado, "decodificando" seqüências de ruído como se fossem código. E uma vez que ruído é aleatório, cedo ou tarde haverá uma colisão.

Infelizmente, nenhum keyfob que eu tenha checado envia código CRC ou mesmo código de paridade, que ajudariam na checagem da integridade do pacote. O HT6P20 envia ao menos um sufixo constante que permite pegar alguns erros. O chip PT2262 tem um esquema de codificação "tribit", ou seja, 2 bits representam 3 valores possíveis, sendo o quarto valor proibido.

Trivia: os módulos RX alegam funcionar tanto em 3.3V quanto em 5V, mas na prática vão melhor em 5V. Isto significa que você precisa de um conversor de nível lógico entre o módulo e o ESP32. Nem todo conversor funciona; os bidirecionais tendem a falhar, pelo menos para mim. Conversores unidirecionais, ou simples divisores de tensão com resistores, funcionam melhor.

Decodificação

Receptores como o SYN480R e ou RXB-8 são "burros", eles apenas indicam a presença ou ausência de uma portadora de rádio. A decodificação OOK em si tem de ser implementada por outro componente.

A taxa de transmissão OOK usada em keyfobs é baixa o suficiente para permitir decodificação por software em um microcontrolador. Escrever tal software é um desafiozinho interessante, e realizar a tarefa em software nos permite suportar vários protocolos facilmente.

Mas, se você não quer se incomodar com isso, saiba que existem módulos receptores "faz-tudo", como o RX480E. O pessoal que motoriza portões de garagem usa esse módulo para adaptar sistemas analógicos antigos aos controles digitais. O pino de saída digital do RX480E indica "código detectado", que pode ser conectado diretamente ao dispositivo controlado, ou então tratado por um microcontrolador.

Nós preferimos a incomodação e ainda reinventamos a roda em vez de usar bibliotecas como VirtualWire ou RC switch. Para tornar as coisas mais interessantes, usamos MicroPython, que é meio lento para a tarefa, mesmo rodando num ESP32.

Decodificação usando MicroPython e ESP32

Quase toda implementação de decodificador OOK em software adota a seguinte estratégia:

Nosso primeiro decodificador "baixo" usa interrupções para ser o mais eficiente possível. Mas usar interrupções em MicroPython não sai de graça: o tratador de interrupção deve trabalhar rápido e não pode alocar memória.

O tratador é configurado para detectar transições de sinal i.e. ele é chamado quando o nível lógico "cru" do receptor muda de 0 para 1 ou vice-versa. Cada chamada recebe um timestamp a fim de medir por quanto tempo o nível lógico anterior foi mantido.

Por conta disso, o tratador de interrupção deve possuir baixo jitter. Jitter é a variação do atraso. Sempre haverá um atraso entre o evento de hardware e a chamada do tratador, mas esse atraso deve ser o mais constante possível. Em In MicroPython/ESP32, o jitter é de ~25µs (a não ser que o Wi-Fi esteja ativo, conforme veremos a seguir).

Uma vez que estamos medindo chirps de comprimento não menor que 150µs, o jitter de 25µs é aceitável, por ora.

O decodificador "alto" lida com a saída do decodificador "baixo". No momento ele entende os protocolos EV1527 e HT6P20. Uma vez que ele pede um arquivo como entrada, ele é feito para rodar num computador, não no ESP32. Se você não tem nenhum hardware 433MHz, ainda pode testar o decodificador "alto" com estas amostras de seqüências.

Uma vez feitos os testes com ambos os decodificadores parciais e satisfeitos com seu desempenho, juntamos tudo no script sniffer.py. Este pode rodar no ESP32 e faz o serviço completo, podendo funcionar até como um "sniffer" se você quiser.

Figura 3: Protótipo de receptor/sniffer 433MHz.

Qual o alcance e sensibilidade desse receptor? O fator mais influente é a antena. Qualquer fio conectado ao pino ANT do módulo receptor já funciona, mas o comprimento ótimo é de 17cm para 433MHz. Se a antena deve ficar separada do módulo, uma antena fabricada especificamente para a banda conectada por cabo coaxial é a melhor pedida.

Integração com MQTT

Em meu framework de automação, uso MQTT (e portanto Wi-Fi) para comunicação entre as diversas caixinhas IoT. O objetivo de brincar com keyfobs 433MHz é construir uma caixinha que publique um tópico MQTT quando um keyfob é detectado. Naturalmente, outra caixa subscreve o mesmo tópico MQTT e causa um efeito colateral.

O grosso do código para a caixa 433MHz pode ser encontrado aqui. O histórico de commits corrobora a história contada a seguir.

Primeiro problema: o decodificador/sniffer que desenvolvemos para os primeiros testes recusou-se a funcionar quando integrado no framework. Apenas um em dez acionamentos de keyfob era detectado. Por que isso? Descobrimos que, no MicroPython para ESP32, ativar o Wi-Fi piora muito o jitter do tratador de interrupção, para uns 500µs.

Este é um problema conhecido do porte ESP32 do MicroPython. Uma alternativa é empregar um ESP32 separado para o decodificador de "baixo nível" (assumindo que vamos insistir em usar MicroPython em tudo; sempre poderíamos trocar de linguagem). Outra saída é não usar mais interrupções, apelando ao busy loop.

Busy loops são feios e não havia garantias que resolveriam o problema. Mas resolveram, restaurando o jitter para 25µs, que parece ser inerente ao interpretador, não só ao tratador de interrupção.

Neste ponto, tínhamos um receptor 433MHz com integração MQTT que satisfez nosso objetivo intrínseco. (A saber, acionar algumas luzes exernas da casa quando o keyfob do portão da garagem é acionado pelo botão secundário.)

Figura 4: Receptor 433MHz - forma final.

Livrando-se do busy loop

O microcontrolador ESP32 possui um recurso chamado RMT, originalmente pensado para suportar controles remotos infravermelho. Ele permite tratar quase todo o "baixo nível" de um decodificador OOK em hardware, liberando a CPU e abolindo a necessidade do busy loop.

Infelizmente, a versão corrente do MicroPython só dá acesso aos recursos de TX do RMT, não de RX. Para nosso próprio uso, implementamos suporte a RMT RX e funciona muito bem. Para que isto seja aceito no projeto MicroPython, precisamos antes atualizar o suporte RMT TX para a versão mais atual do ESP-IDF. Também elaboramos um PR para este fim, que está pendente de aprovação.

Recepção de rádio com SDR

Os módulos Arduino que usamos até aqui (e.g. SYN480R e RXB-8) são muito práticos porque lidam com toda a complexidade do rádio e do bloco analógico. Eles entregam um simples sinal digital indicando presência ou ausência de portadora. Mas, e se quisermos lidar também com a parte analógica?

Uma possibilidade é usar um SDR — rádio definido por software. É um receptor de rádio universal que delega a demodulação ao software, algo como uma placa de som para sinais de rádio. O modelo mais conhecido é o RTL-SDR. Há SDRs melhores, porém são muito mais caros.

TL;DR nosso receptor de keyfobs baseado em RTL-SDR está aqui: um script Bash que invoca o programa principal em Python. Se você planeja rodar esse código, clone o repositório inteiro pois há uns módulos auxiliares de que o programa depende.

Nossa intenção aqui não é criar um "produto acabado" para usar em produção como fizemos na versão ESP32, e os scripts são feitos para rodar num PC. A ideia é apenas sujar as mãos com a parte analógica do processo.

Front-end SDR

A fim de tratar um sinal de 433MHz em software, a princípio precisaríamos de uma taxa de amostragem 2× maior, idealmente 4× maior. Porém, tratar um sinal com amostragem a 1600MHz é inviável para um PC. Em geral, não se usa o SDR no modo "amostragem direta". Faz-se uma translação da banda de interesse para a banda-base, que então pode ser digitalizada numa taxa bem menor.

A taxa de amostragem da banda-base tem duplo significado. Ela determina a carga de trabalho do PC, e também define a largura de banda do sinal. Pelo teorema de Nyquist, uma taxa de 200kHz entrega uma banda de 100kHz. Se for um sinal complexo, entrega 200kHz no total (−100kHz a +100kHz).

Deve-se usar uma taxa maior que a estritamente necessária e então aplicar um filtro passa-baixas, pois o processo de amostragem introduz distorções nos extremos da banda. Em nosso caso, estamos interessados numa banda de 200kHz em torno da freqüência 433.92MHz. (Precisamos ouvir toda essa banda pois os keyfobs são não são muito precisos.) Nossa taxa de amostragem deve então ser bem maior que 200kHz.

O RTL-SDR não aceita taxas de amostragem entre 300kHz e 900kHz, então decidimos ser pródigos e usamos a taxa de 960kHz. (Esse valor não tem nada de especial, foi herdado de outros scripts que escrevemos no passado e aproveitamos algumas partes.)

Para manter o programa principal simples, usamos a ferramenta de linha de comando do RTL-SDR para lidar com o hardware, e as amostras I/Q são repassadas via pipe:

rtl_sdr -f 433920000 -g $GAIN -s 960k - | ./ook_433.py

SDR: amostras I/Q

No geral, os SDRs fazem demodulação complexa e entregam um sinal banda-base composto de números complexos. Uma explicação mais aprofundada pode ser encontrada aqui. A ideia geral é que o demodulador complexo é um canivete suíço que atende qualquer tecnologia de rádio.

A saída de rtl_sdr é uma seqüência de amostras I/Q, ou seja, uma seqüência de números complexos. Cada número complexo é composto de 2 bytes, com bias de 127.4 e escala 128.0. Por exemplo, uma amostra com valores (128, 125) vale mesmo 0.0046875-0.01875j.

A biblioteca NumPy é muito conveniente pois permite fazer operações aritméticas por atacado, sobre listas de números:

    # Parse RTL-SDR I/Q format
    iqdata = numpy.frombuffer(data, dtype=numpy.uint8)
    iqdata = iqdata - 127.4
    iqdata = iqdata / 128.0
    iqdata = iqdata.view(complex)

Nossa taxa de amostragem é 960kHz porém só queremos ouvir uma banda de 200kHz, então aplicamos um filtro passa-baixas:

INPUT_RATE = 960000
BANDWIDTH = 200000
...
if_filter = filters.low_pass(INPUT_RATE, BANDWIDTH / 2, 48)
    ...
    iqdata = if_filter.feed(iqdata)

Dividimos BANDWIDTH por dois, já que um sinal complexo possui freqüências negativas. Um filtro com corte em 100kHz deixa passar uma banda entre −100kHz e +100kHz, total 200kHz conforme desejávamos.

SDR: detecção de envelope

Na modulação de rádio AM (quase extinta mas historicamente importante), o conteúdo original (geralmente áudio) pode ser extraído com base no contorno do sinal de rádio. Esta é a base do "detector de envelope" que pode ser implementado com apenas um retificador e um capacitor.

(Este não é nem o único nem o melhor método de receber AM. Porém é o mais barato, e pode ser improvisado até com uma lâmina de barbear enferrujada.)

Figura 5: Modulação AM: o conteúdo original é multiplicado ou `misturado` com a portadora, e então somado com a mesma portadora. O contorno formado pelos picos da onda de rádio segue o formato do conteúdo original.
Figura 6: Demodulação AM: para reconstituir o conteúdo original, um método é retificar a onda de rádio e passar um filtro passa-baixas. Este processo é conhecido por `detecção de envelope`.

A modulação ASK/OOK é conceitualmente similar, porém ainda mais simples, porque o conteúdo só tem dois valores possíveis: 1 e 0.

Figura 7: Modulação ASK/OOK: o conteúdo digital aciona a presença ou ausência de transmissão da portadora.
Figura 8: Demodulação ASK/OOK: a onda de rádio é retificada e filtrada para reconstituir o conteúdo digital. Note que o resultado não é uma onda quadrada perfeita.

Esta é nossa implementação de software de um detector de envelope:

TRANS_MIN_US = 150
...
PREAMBLE_MIN_US = 5000
...
GLITCH_US = min(TRANS_MIN_US, PREAMBLE_MIN_US) / 4
...
envelope_filter = filters.low_pass(INPUT_RATE, 1000000 / GLITCH_US)
    ...
    iqdata = numpy.absolute(iqdata)
    # Detect envelope using a low-pass filter
    iqdata = envelope_filter.feed(iqdata)

A função numpy.absolute() é nosso diodo retificador que converte o sinal complexo em real. Isto "destrói" o sinal, mas não importa, pois só queremos medir sua força.

A freqüência de corte do filtro é um compromisso entre um envelope "reto" quando há portadora, e uma transição rápida quando muda o estado da portadora. GLITCH_US tem dimensão de tempo, e níveis lógicos com duração menor que essa são considerados transientes, que não nos interessam.

Por que não configurar o filtro com base em TRANS_MIN_US, que é o chirp válido mais curto possível? Porque é interessante para nós deixar parte do ruído de alta freqüência passar, pois esta é uma forma simples do decodificador OOK distinguir sinal de ruído: em havendo alta freqüência (transições mais rápidas que TRANS_MIN_US), é ruído.

SDR: decodificação OOK

Neste ponto, o sinal extraído do SDR é quase o mesmo que obteríamos de um módulo receptor OOK, exceto que ele ainda é "analógico" (possui mais que dois valores). Precisamos estabelecer um nível de corte para convertê-lo para digital.

Em nossa implementação, o processo gira em torno da variável bgnoise que é uma média móvel do ruído de fundo. Assumimos que sinais são raramente recebidos, e estamos ouvindo apenas ruído de fundo quase todo o tempo. Consideramos então que um sinal "n" vezes mais forte que o ruído de fundo indica presença de portadora, portanto é igual 1, do contrário igual a 0.

Para isto funcionar, o ganho do RTL-SDR deve ser fixo e.g. 40dB. Operar o SDR em modo AGC faria o valor de bgnoise estacionar num patamar mais alto, fazendo o decodificador ignorar sinais fracos. Se quiséssemos operar com AGC, teríamos de usar um algoritmo diferente.

Deste ponto em diante, o decodificador OOK é igual ao que implementamos para ESP32. O código é praticamente idêntico.

Pode o SDR ser um receptor melhor que um módulo especializado?

Objetivamente falando, não. Os módulos OOK para Arduino citados no início do texto têm sensibilidade entre -106dBm e -114dBm. A sensibilidade do RTL-SDR é em torno de -95dBm. Isto significa um alcance oito vezes menor.

É bem verdade que os módulos OOK melhoraram muito. Os de 15 anos atrás mal conseguiam atravessar uma parede. Comparados com aqueles, o RTL-SDR provavelmente é melhor.

Em nossos testes, o SDR funcionou surpreendentemente bem, detectando em torno de 50% dos acionamentos que foram recebidos pelo receptor ESP32 "oficial". Talvez o SDR pudesse até ser usável em produção se conectado a uma antena boa e bem-posicionada, quiçá inserindo um filtro 433MHz na entrada.

A graça de utilizar um SDR é, como dissemos, conhecer e manipular os parâmetros da parte analógica da recepção, a fim de melhor entender como um módulo receptor "caixa-preta" funciona por dentro.

Transmissão: alto nível

Nosso objetivo intrínseco era usar keyfobs disponíveis no mercado para nossa automação residencial. Assim, compramos keyfobs e módulos de recepção. Mas o módulo SYN480R veio com um transmissor SYN115 no mesmo pacotinho, de graça, então por que não? Transmitir é mais fácil que receber, pois basta seguir cegamente o protocolo.

Para começar, rascunhamos um script de TX de "alto nível" Ele é de "alto nível" pois apenas traduz dados para uma seqüência de chirps. A ideia era testar essas seqüências com nosso receptor de alto nível preexistente, antes de chegar perto do hardware.

A propósito, aqui está uma versão levemente modificada do decodificador de alto nível. Ele aceita dados via stdin, não de um arquivo.

Transmissão: baixo nível

O módulo SYN115 é o inverso exato do SYN480R, apresentando uma única entrada digital que comuta a portadora. Uma diferença importante: ele é 3.3V, então não precisamos de um conversor de nível para microcontroladores de 3.3V como o ESP32. O consumo é muito baixo, então ele pode ser alimentado pelo regulador 3.3V do próprio microcontrolador.

Aqui podemos usar o recurso RMT do ESP32, pois MicroPython dá acesso ao TX do RMT sem modificações. Isso nos poupa de fazer bitbanging. Apenas forneça uma lista de níveis lógicos para o RMT e ele faz o resto. Este é o script TX completo.

Descobrimos que é melhor enviar o código duas ou três vezes a cada acionamento (exatamente como fazem os keyfobs), de outra forma o receptor tende a perder mensagens. Parece ser uma característica de todos os receptores OOK. Usamos o script acima para operar nosso portão de garagem e aconteceu a mesma coisa, então não é problema exclusivo dos nossos módulos Arduino, nem do nosso decodificador OOK de software.

Uma vez que a saída RMT tem alta precisão (12.5ns) o transmissor de software foi uma ferramente útil para determinar o jitter na recepção. Foi assim que encontramos o número 25µs citado lá em cima.

Bônus: módulo TX118SA-4

O módulo TX118SA-4 é o exato oposto do RX480E. Ambos são oferecidos pelo mesmo fabricante (QIACHIP). Este é um transmissor "caixa-preta" que emula um keyfob EV1527 e possui quatro pinos digitais de acionamento, correspondentes aos 4 botões de um keyfob típico. O módulo pode ser atuado por um microcontrolador ou por uma simples chave pulsante.

Até onde sabemos, o código enviado é de fábrica e não pode ser reprogramado. O pessoal que trabalha com portões de garagem liga este módulo na luz alta do automóvel para acionar o portão de garagem sem tirar as mãos do volante. O módulo funciona com uma ampla gama de tensões, de 3V a 24V.

Ele veio junto no mesmo pacotinho com outros módulos, então a nobreza obrigou-nos a testá-lo. Fizemos uso dele como transmissor de teste para determinar alcance. Este script simples aciona um TX118SA-4 a cada 10 segundos. Note que o script foi feito para o ESP8266 e usa o pino 2, que no módulo NodeMCU tem lógica inversa.