Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Junte-se a nós no Amazon Web Services re:Invent 2024! Saiba como usar o MongoDB para casos de uso de AI .
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Idiomaschevron-right
C++chevron-right

Red Mosquitto: Implemente um sensor de ruído com um cliente MQTT em um ESP32

Jorge D. Ortiz-Fuentes25 min read • Published Sep 17, 2024 • Updated Sep 17, 2024
RaspberryPiC++Rust
APLICATIVO COMPLETO
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Bem-vindo a mais um artigo da série "Aventuras em IoT". Até agora, definimos um projeto de ponta a ponta, escrevemos o microcódigo para uma placa Raspberry Pi Pico MCU para medir a temperatura e enviar o valor via desatualização por meio do sistema de banco de dados estação de coleta que conseguiu ler os dados BLE. Se ainda não teve tempo, leia-os ou assista aos vídeos.
Neste artigo, vamos escrever o microcódigo para uma placa diferente: uma cifrão32-C6-DevKitC-1. As diretorias32 são muito populares entre a comunidade faça você mesmo e para IoT em geral. O criador dessas placas, Espressif, está se esforçando bastante para oferecer suporte ao Rust como uma linguagem de desenvolvimento de primeira classe para elas. Estou agradecido por isso e aproveitarei as ferramentas que eles criaram para nós.
Podemos escrever código para o32 ESP que se comunica com o bare metal, também conhecido como núcleo, ou usar um sistema operacional que nos permita aproveitar ao máximo os recursos fornecidos pela biblioteca std. O ESP-IDF –ou seja, ESPressif IoT Development Framework– foi criado para simplificar esse desenvolvimento e não está disponível apenas em C/C++, mas também em Rust, que usaremos no restante deste artigo. Usando o ESP-IDF por meio das caixas correspondentes, podemos usar threads, mutexes e outras primitivas de sincronização, coleções, geração de números aleatórios, soquetes, etc. insira a descrição da imagem aqui Minha placa é uma6 ESP32-C que usa uma arquitetura RISC-V. Ele não possui nenhum sensor embutido, mas possui um LED RGB e é capaz de se comunicar sem fio com o resto do mundo de várias maneiras: WiFi, Bluetooth LE, Zigbee e Thread. Vamos ver como podemos usar esses recursos.

Conceitos

HAL incorporado

Em 9janeiro de 2024 – ou seja, alguns dias depois de começar a preparar este tutorial – embedded-hal v1.0 foi lançado. Ele fornece uma abstração para criar drivers independentes do MCU. Isso é muito útil para nós, desenvolvedores, pois nos permite desenvolver e manter o driver uma vez e usá-lo para as diversas Placas MCU que honram essa abstração.
Este kit de placa de desenvolvimento possui um LED neopixel – ou seja, um LED RGB controlado por um WS2812 – que usaremos para nossa iteração "Hello World!" e depois para informar o usuário sobre o estado do dispositivo. O WS2812 requer o envio de sequências de altas e baixas tensões que usam a duração desses valores altos e baixos para especificar os bits que definem os componentes de cor RGB do LED. O ESP32 possui um transceptor de controle remoto (RMT) que foi concebido como um transceptor infravermelho, mas pode ser reaproveitado para gerar os sinais necessários para o protocolo serial de linha única usado pelo WS1812. Nem o RMT nem os temporizadores estão disponíveis na versão recém-lançada do embedded-hal, mas o ESP-IDF fornecido pelo Expressif implementa a abstraçãoembedded-halcompleta e o driver WS2812 usa as abstrações disponíveis.

Configurar

As ferramentas

Existem algumas ferramentas que você precisará instalar em seu computador para poder acompanhar, compilar e instalar o firmware em sua placa. Eu os instalei no meu computador, mas antes de gastar tempo com essa configuração, considere usar o contêiner fornecido pelo Espressif se preferir essa opção.
A primeira coisa que pode ser diferente para você é que precisamos da versão de ponta da cadeia de ferramentas Rust. Estaremos usando a versão noturna dele:
1rustup toolchain install nightly --component rust-src
Quanto às ferramentas, é possível que você já tenha algumas delas em seu computador, mas verifique novamente se instalou todas elas:
  • Git (no macOS instalado com Code)
  • Algumas ferramentas para ajudar no processo de construção (brew install cmake ninja dfu-util python3 –Funciona no macOS, mas se você usar um sistema operacional diferente,confira a lista aqui)
  • Uma ferramenta para encaminhar argumentos do vinculador para o vinculador real (cargo install ldproxy)
  • Um utilitário para gravar o firmware na placa (cargo install espflash)
  • Uma ferramenta usada para produzir um novo projeto a partir de um modelo (cargo install cargo-generate)

Criação de projeto usando um modelo

Podemos então criar um projeto usando o modelo para stdlib projetos (esp-idf-template):
1cargo generate esp-rs/esp-idf-template cargo
E preenchemos estes dados:
  • Nome do projeto: mosquito-bzzz
  • MCU para o alvo: esp32c6
  • Configurar opções avançadas de modelo: false
cargo b produz a compilação. O alvo é riscv32imac-esp-espidf (arquitetura SISC-V com suporte para atômicos), então o binário é gerado em target/riscv32imac-esp-espidf/debug/mosquitto-bzzz. E pode ser executado no dispositivo usando este comando:
1espflash flash target/riscv32imac-esp-espidf/debug/mosquitto-bzzz --monitor
E no final do registro de saída, você pode encontrar estas linhas:
1I (358) app_start: Starting scheduler on CPU0
2I (362) main_task: Started on CPU0
3I (362) main_task: Calling app_main()
4I (362) mosquitto_bzzz: Hello, world!
5I (372) main_task: Returned from app_main()
Vamos entender o projeto que foi criado para que possamos aproveitar todas as peças:
  • Cargo.toml: É o arquivo de configuração principal do projeto. Além do que um cargo newcomum faria, vamos ver o seguinte:
    • Define algumas funcionalidades disponíveis que modificam a configuração de algumas das dependências.
    • Ele inclui algumas dependências: uma para a API de registro e outra para usar o ESP-IDF.
    • Ele adiciona uma dependência de compilação que fornece utilitários para criar aplicativos para sistemas embarcados.
    • Ele ajusta as configurações de perfil que modificam algumas opções do compilador, nível de otimização e símbolos de depuração, para depuração e versão.
  • build.rs: um script de compilação que não pertence ao aplicativo, mas é executado como parte do processo de compilação.
  • rust-toolchain.toml: Um arquivo de configuração para impor o uso do conjunto de ferramentas noturno, bem como uma cópia local docódigo-fonte da biblioteca padrão Rust.
  • sdkconfig.defaults: Um arquivo com alguns parâmetros de configuração para o esp-idf.
  • .cargo/config.toml: um arquivo de configuração para o Cargo, no qual temos a arquitetura, as ferramentas e os sinalizadores instáveis do compilador usado no processo de compilação e as variáveis de ambiente usadas no processo.
  • src/main.rs: A semente para o nosso código com o esqueleto mínimo.

Fundações do nosso software

A ideia é criar um firmware semelhante ao que escrevemos para o Raspberry Pi Pico, mas expondo os dados do sensor usando MQTT em vez de Bluetooth Low Energy. Isso significa que temos de nos conectar ao WiFi, depois ao corretor MQTT e começar a publicar os dados. Usaremos o LED RGB para mostrar o status do nosso sensor e usaremos um sensor de som para obter os dados desejados.

Controle o LED

Fazer um LED piscar é considerado o " hello world" da programação incorporada. Podemos ir um pouco além e usar cores em vez de apenas piscar.
  1. De acordo com a documentação da placa, o Led é controlado pelo pino gpio8 . Podemos obter acesso a esse pino usando o móduloPeripherals do esp-idf-svc, que expõe o hl adicionando use esp_idf_svc::hal::peripherals::Peripherals;:
    1let peripherals = Peripherals::take().expect("Unable to access device peripherals");
    2let led_pin = peripherals.pins.gpio8;
  2. Também usando o singleton Periféricos, podemos acessar o canal RMT que produzirá o sinal de forma de onda desejado necessário para definir cada um dos três componentes de cor do LED:
    1let rmt_channel = peripherals.rmt.channel0;
  3. Poderíamos fazer a codificação de cores RGB manualmente, mas há uma caixa que nos ajudará a conversar com o controlador WS2812 (neopixel) integrado que aciona o LED RGB. Asmart-ledsde criação poderia ser usada em cima dela se tivéssemos vários LEDs, mas não precisamos dela para esta placa.
    1cargo add ws2812-esp32-rmt-driver
  4. Criamos uma instância que se comunica com o WS2812 no pino 8 e usa o Transceiver de Controle Remoto - também conhecido como RMT - secundário no canal 0. Adicionamos o símbolo use ws2812_esp32_rmt_driver::Ws2812Esp32RmtDriver; e:
    1let mut neopixel =
    2 Ws2812Esp32RmtDriver::new(rmt_channel, led_pin).expect("Unable to talk to ws2812");
  5. Em seguida, definimos os dados de um ponto e os escrevemos com a instância do driver para que seja usado no Led. É importante não apenas importar o tipo para a cor do 24BI, mas também obter o traço com use ws2812_esp32_rmt_driver::driver::color::{LedPixelColor,LedPixelColorGrb24};:
    1let color_1 = LedPixelColorGrb24::new_with_rgb(255, 255, 0);
    2neopixel
    3 .write_blocking(color_1.as_ref().iter().cloned())
    4 .expect("Error writing to neopixel");
  6. Nesse momento, você pode executá-lo com cargo r e esperar que o LED fique aceso com a cor amarela.
  7. Vamos adicionar um loop e algumas mudanças para completar nosso " hello world. " Primeiro, definimos uma segunda cor:
    1let color_2 = LedPixelColorGrb24::new_with_rgb(255, 0, 255);
  8. Em seguida, adicionamos um loop no final, onde alternamos entre essas duas cores:
    1loop {
    2 neopixel
    3 .write_blocking(color_1.as_ref().iter().cloned())
    4 .expect("Error writing to neopixel");
    5 neopixel
    6 .write_blocking(color_2.as_ref().iter().cloned())
    7 .expect("Error writing to neopixel");
    8}
  9. Se não introduzirmos nenhum atraso, não poderemos perceber as cores mudando, por isso adicionamos use std::{time::Duration, thread}; e aguardamos meio segundo antes de cada alteração:
    1neopixel
    2 .write_blocking(color_1.as_ref().iter().cloned())
    3 .expect("Error writing to neopixel");
    4thread::sleep(Duration::from_millis(500));
    5neopixel
    6 .write_blocking(color_2.as_ref().iter().cloned())
    7 .expect("Error writing to neopixel");
    8thread::sleep(Duration::from_millis(500));
  10. Corremos e observamos o LED mudando de cor de roxo para amarelo e vice-versa a cada meio segundo.

Use o Led para se comunicar com o usuário

Vamos encapsular o uso do Led em seu próprio thread. Esse thread precisa estar ciente de quaisquer alterações no status do dispositivo e usar o atual para decidir como usar o Led adequadamente.
  1. Primeiro, precisaremos de um enum com todos os estados possíveis. Inicialmente, ele conterá uma variante para nenhum erro, uma variante para erro de WiFi e outra para erro MQTT:
    1enum DeviceStatus {
    2 Ok,
    3 WifiError,
    4 MqttError,
    5}
  2. E podemos adicionar uma implementação para converter de inteiros sem sinal de oito bits em uma variante desse enum:
    1impl TryFrom<u8> for DeviceStatus {
    2 type Error = &'static str;
    3
    4 fn try_from(value: u8) -> Result<Self, Self::Error> {
    5 match value {
    6 0u8 => Ok(DeviceStatus::Ok),
    7 1u8 => Ok(DeviceStatus::WifiError),
    8 2u8 => Ok(DeviceStatus::MqttError),
    9 _ => Err("Unknown status"),
    10 }
    11 }
    12}
  3. Gostaríamos de usar as variantesDeviceStatuspor nome quando um número for necessário. Conseguimos a conversão inversa adicionando uma anotação ao enum:
    1#[repr(u8)]
    2enum DeviceStatus {
  4. Em seguida, farei algo que será considerado ingênua por qualquer pessoa que tenha desenvolvido algo em Rust, além do mais simples " hello world! " No entanto, quero destacar uma das vantagens de usar o Rust, em vez da maioria das outras linguagens, para escrever firmware (e software em geral). Estou para definir uma variável na função principal que manterá o status atual do dispositivo e o compartilhará entre os threads.
    1let mut status = DeviceStatus::Ok as u8;
  5. Vamos definir duas threads. A primeira destina-se a relatar ao usuário o status do dispositivo. A segunda é necessária apenas para fins de teste e a substituiremos por alguma funcionalidade real em breve. Usaremos sequências de cores no LED para relatar o status do sensor. Vamos começar definindo cada uma das etapas dessas sequências de cores:
    1struct ColorStep {
    2 red: u8,
    3 green: u8,
    4 blue: u8,
    5 duration: u64,
    6}
  6. Também definimos um construtor como uma função associada para nossa própria conveniência:
    1impl ColorStep {
    2 fn new(red: u8, green: u8, blue: u8, duration: u64) -> Self {
    3 ColorStep {
    4 red,
    5 green,
    6 blue,
    7 duration,
    8 }
    9 }
    10}
  7. Podemos então usar essas etapas para transformar cada status em uma sequência diferente que podemos exibir no LED:
    1impl DeviceStatus {
    2 fn light_sequence(&self) -> Vec<ColorStep> {
    3 match self {
    4 DeviceStatus::Ok => vec![ColorStep::new(0, 255, 0, 500), ColorStep::new(0, 0, 0, 500)],
    5 DeviceStatus::WifiError => {
    6 vec![ColorStep::new(255, 0, 0, 200), ColorStep::new(0, 0, 0, 100)]
    7 }
    8 DeviceStatus::MqttError => vec![
    9 ColorStep::new(255, 0, 255, 100),
    10 ColorStep::new(0, 0, 0, 300),
    11 ],
    12 }
    13 }
    14}
  8. Iniciamos o thread inicializando o WS2812 que controla o LED:
    1use esp_idf_svc::hal::{
    2 gpio::OutputPin,
    3 peripheral::Peripheral,
    4 rmt::RmtChannel,
    5};
    6
    7fn report_status(
    8 status: &u8,
    9 rmt_channel: impl Peripheral<P = impl RmtChannel>,
    10 led_pin: impl Peripheral<P = impl OutputPin>,
    11) -> ! {
    12 let mut neopixel =
    13 Ws2812Esp32RmtDriver::new(rmt_channel, led_pin).expect("Unable to talk to ws2812");
    14 loop {}
    15}
  9. Podemos acompanhar o status anterior e a sequência atual, para que não tenhamos que regenerá-la depois de exibi-la uma vez. Isso não é necessário, mas é mais eficiente:
    1let mut prev_status = DeviceStatus::WifiError; // Anything but Ok
    2let mut sequence: Vec<ColorStep> = vec![];
  10. Em seguida, entramos em um loop infinito, no qual atualizamos o status, se ele mudou, e a sequência de acordo. De qualquer forma, usamos cada uma das etapas da sequência para exibi-la no LED:
    1loop {
    2 if let Ok(status) = DeviceStatus::try_from(*status) {
    3 if status != prev_status {
    4 prev_status = status;
    5 sequence = status.light_sequence();
    6 }
    7 for step in sequence.iter() {
    8 let color = LedPixelColorGrb24::new_with_rgb(step.red, step.green, step.blue);
    9 neopixel
    10 .write_blocking(color.as_ref().iter().cloned())
    11 .expect("Error writing to neopixel");
    12 thread::sleep(Duration::from_millis(step.duration));
    13 }
    14 }
    15}
  11. Observe que o status não pode ser comparado até que implementemos PartialEq, e atribuí-lo requer Clone e Copy, então os derivamos:
    1#[derive(Clone, Copy, PartialEq)]
    2enum DeviceStatus {
  12. Agora, vamos implementar a função que é executada no outro thread. Esta função alterará o status a cada 10 segundos. Como isso é para testar a capacidade de relatório, não faremos nada sofisticado para alterar o status, apenas passando de um status para o próximo e de volta ao início:
    1fn change_status(status: &mut u8) -> ! {
    2 loop {
    3 thread::sleep(Duration::from_secs(10));
    4 if let Ok(current) = DeviceStatus::try_from(*status) {
    5 match current {
    6 DeviceStatus::Ok => *status = DeviceStatus::WifiError as u8,
    7 DeviceStatus::WifiError => *status = DeviceStatus::MqttError as u8,
    8 DeviceStatus::MqttError => *status = DeviceStatus::Ok as u8,
    9 }
    10 }
    11 }
    12}
  13. Com as duas funções em vigor, só precisamos gerar dois threads, um com cada uma delas. Usaremos um escopo de thread que se encarregará de unir os threads que gerarmos:
    1thread::scope(|scope| {
    2 scope.spawn(|| report_status(&status, rmt_channel, led_pin));
    3 scope.spawn(|| change_status(&mut status));
    4});
  14. A compilação desse código resultará em erros. É a bênção/maldição do verificador de empréstimos, que é capaz de descobrir que estamos compartilhando memória de forma insegura. O status pode ser alterado em um thread enquanto está sendo lido pelo outro. Poderíamos usar um mutex, como fizemos no código C++ anterior, e envolvê-lo em um Arc para poder usar uma referência em cada thread, mas há uma maneira mais fácil de atingir o mesmo objetivo: podemos usar um tipo atômico. (use std::sync::atomic::AtomicU8;)
    1let status = &AtomicU8::new(0u8);
  15. Modificamos report_status() para usar a referência ao tipo atômico e adicionamos use std::sync::atomic::Ordering::Relaxed;:
    1fn report_status(
    2 status: &AtomicU8,
    3 rmt_channel: impl Peripheral<P = impl RmtChannel>,
    4 led_pin: impl Peripheral<P = impl OutputPin>,
    5) -> ! {
    6 let mut neopixel =
    7 Ws2812Esp32RmtDriver::new(rmt_channel, led_pin).expect("Unable to talk to ws2812");
    8 let mut prev_status = DeviceStatus::WifiError; // Anything but Ok
    9 let mut sequence: Vec<ColorStep> = vec![];
    10 loop {
    11 if let Ok(status) = DeviceStatus::try_from(status.load(Relaxed)) {
  16. E change_status(). Observe que, nesse caso, graças à mutabilidade interna, não precisamos de uma referência mutável, mas de uma referência regular. Além disso, precisamos especificar as garantias em termos de como as várias operações serão ordenadas. Como não temos nenhuma outra operação atômica no código, podemos usar o nível mais fraco, ou seja, Relaxed:
    1fn change_status(status: &AtomicU8) -> ! {
    2 loop {
    3 thread::sleep(Duration::from_secs(10));
    4 if let Ok(current) = DeviceStatus::try_from(status.load(Relaxed)) {
    5 match current {
    6 DeviceStatus::Ok => status.store(DeviceStatus::WifiError as u8, Relaxed),
    7 DeviceStatus::WifiError => status.store(DeviceStatus::MqttError as u8, Relaxed),
    8 DeviceStatus::MqttError => status.store(DeviceStatus::Ok as u8, Relaxed),
    9 }
    10 }
    11 }
    12}
  17. Finalmente, temos que alterar as linhas nas quais geramos os threads para refletir as mudanças que introduzimos:
    1scope.spawn(|| report_status(status, rmt_channel, led_pin));
    2scope.spawn(|| change_status(status));
  18. Você pode usar cargo r para compilar o código e executá-lo em seu painel. As semáforos devem exibir as sequências, que devem mudar a cada 10 segundos.

Obter o nível de ruído

É hora de interagir com um sensor de temperatura... Entretenimento. Desta vez, vamos usar um sensor de som. Não há mais medições de temperatura neste projeto. Promise.
O sensor que vou usar é um01 de som OSEPP que afirma ser "o sensor perfeito para detectar variações ambientais no ruído". Ele suporta uma tensão de entrada de 3V a 5V e fornece um sinal analógico. Vamos conectar o sinal ao pino 0 do GPIO, que também é o pino do primeiro canal do conversor analógico-digital (ADC1_CH0). Os outros dois pinos são conectados a 5V e GND (+ e -, respectivamente). insira a descrição da imagem aqui Você não precisa usar este sensor específico. Existem muitas outras opções no mercado. Alguns deles possuem pinos para saída digital, em vez de apenas analógicos como neste. Alguns sensores também possuem um potenciômetro que permite ajustar a sensibilidade do microfone.

Ler a partir do sensor

  1. Vamos executar esta tarefa em uma nova função:
    1fn read_noise_level() -> ! {
    2}
  2. Queremos usar o ADC no pino ao qual conectamos o sinal. Podemos obter acesso ao ADC1 usando o singletonperipherals na função principal.
    1let adc = peripherals.adc1;
  3. E também ao pino que receberá o sinal do sensor:
    1let adc_pin = peripherals.pins.gpio0;
  4. Modificamos a assinatura de nossa nova função para aceitar os parâmetros de que precisamos:
    1fn read_noise_level<GPIO>(adc1: ADC1, adc1_pin: GPIO) -> !
    2where
    3 GPIO: ADCPin<Adc = ADC1>,
  5. Agora, usamos esses dois parâmetros para anexar um driver que pode ser usado para ler a partir do ADC. Observe que o AdcDriver precisa de uma configuração, que criamos com o valor padrão. Além disso, AdcChannelDriver requer um parâmetro const genérico que é usado para definir o nível de atenuação. Inicialmente, usaria atenuação máxima para ter mais sensibilidade no micro, mas podemos alterá-la mais tarde, se necessário. Adicionamos use esp_idf_svc::hal::adc::{attenuation, AdcChannelDriver};:
    1let mut adc =
    2 AdcDriver::new(adc1, &adc::config::Config::default()).expect("Unable to initialze ADC1");
    3let mut adc_channel_drv: AdcChannelDriver<{ attenuation::DB_11 }, _> =
    4 AdcChannelDriver::new(adc1_pin).expect("Unable to access ADC1 channel 0");
  6. Com as peças necessárias posicionadas, podemos usar o adc_channel para obter amostras em um loop infinito. Um atraso de 10ms significa que a amostragem será a ~100hz:
    1loop {
    2 thread::sleep(Duration::from_millis(10));
    3 println!("ADC value: {:?}", adc.read(&mut adc_channel));
    4}
  7. Por último, geramos um tópico com esta função no mesmo escopo que usámos antes:
    1scope.spawn(|| read_noise_level(adc, adc_pin));

Calcule os níveis de ruído (Sorta!)

Para obter uma estimativa do nível de ruído, vou calcular a raiz quadrada média (RMS) de um buffer de 50ms, ou seja, cinco amostras em nossa taxa de amostragem atual. Eu sei que não é exatamente assim que os decibéis são medidos, mas será suficiente para nós e para os dados que queremos coletar.
  1. Vamos começar criando esse buffer onde colocaremos as amostras:
    1const LEN: usize = 5;
    2let mut sample_buffer = [0u16; LEN];
  2. Dentro do loop infinito, teremos um loop for que passa pelo buffer:
    1for i in 0..LEN {
    2}
  3. Modificamos a amostragem que fizemos antes, de modo que um valor zero seja usado se o ADC não conseguir obter uma amostra:
    1thread::sleep(Duration::from_millis(10));
    2if let Ok(sample) = adc.read(&mut adc_pin) {
    3 sample_buffer[i] = sample;
    4} else {
    5 sample_buffer[i] = 0u16;
    6}
  4. Antes de começar com as iterações do loop for, vamos definir uma variável para manter a adição dos quadrados das amostras:
    1let mut sum = 0.0f32;
  5. E cada amostra é elevada ao quadrado e adicionada à soma. Poderíamos fazer a conversão em floats após o quadrado, mas então, o valor do quadrado pode não caber em um u16:
    1sum += (sample as f32) * (sample as f32);
  6. E calculamos os decibéis (ou algo próximo a isso) após o loop for:
    1let d_b = 20.0f32 * (sum / LEN as f32).sqrt().log10();
    2println!(
    3 "ADC values: {:?}, sum: {}, and dB: {} ",
    4 sample_buffer, sum, d_b
    5);
  7. Compilamos e executamos com cargo r e devemos obter uma saída semelhante a:
    1ADC values: [0, 0, 0, 0, 0], sum: 0, and dB: -inf
    2ADC values: [0, 0, 0, 3, 0], sum: 9, and dB: 2.5527248
    3ADC values: [0, 0, 0, 11, 0], sum: 121, and dB: 13.838154
    4ADC values: [8, 0, 38, 0, 102], sum: 11912, and dB: 33.770145
    5ADC values: [64, 23, 0, 8, 26], sum: 5365, and dB: 30.305998
    6ADC values: [0, 8, 41, 0, 87], sum: 9314, and dB: 32.70166
    7ADC values: [137, 0, 79, 673, 0], sum: 477939, and dB: 49.804024
    8ADC values: [747, 0, 747, 504, 26], sum: 1370710, and dB: 54.379753
    9ADC values: [240, 0, 111, 55, 26], sum: 73622, and dB: 41.680374
    10ADC values: [8, 26, 26, 58, 96], sum: 13996, and dB: 34.470337

MQTT

Conceitos

Quando escrevemos nosso firmware anterior, usamos o Bluetooth Low Energy para disponibilizar os dados do sensor para o resto do mundo. Foi uma experiência interessante, mas teve algumas limitações. Algumas dessas limitações foram introduzidas pelo hardware que estávamos usando, como o fato de estarmos recebendo algumas interferências no sinal Bluetooth das comunicações WiFi no Raspberry Pi. Mas outros são inerentes à tecnologia Bluetooth, como a distância máxima entre o sensor e a estação de coleta.
Para este firmware, decidimos adotar uma abordagem diferente. Usaremos Wi-Fi para as comunicações dos sensores para a estação de coleta. O WiFi nos permitirá distribuir os sensores por uma área muito maior, especialmente se tivermos vários pontos de acesso. No entanto, isso tem um preço: os sensores consumirão mais energia e suas pilhas durarão menos.
Usar WiFi praticamente implica que nossas comunicações serão baseadas em TCP/IP. E isso abre uma ampla gama de possibilidades, que podemos resumir nesta lista em ordem crescente de probabilidade:
  • Implemente um protocolo TCP ou UDP personalizado.
  • Use um protocolo existente que seja comumente usado para escrever API. Existem outras opções, mas HTTP é o principal aqui.
  • Use um protocolo existente que seja mais personalizado com a finalidade de enviar dados de eventos que contenham valores.
Criar um protocolo personalizado é caro, demorado e sujeito a erros, especialmente sem experiência anterior. É provavelmente a pior ideia para uma prova de conceito, a menos que você tenha um requisito muito específico que não possa ser realizado de outra forma.
O HTTP vem à mente como uma ótima solução para trocar dados. As REST API são um exemplo disso. No entanto, ele tem algumas limitações, como o fluxo unidirecional de dados, a sobrecarga — tanto em termos do protocolo em si quanto no uso de uma nova conexão para cada nova solicitação — e até mesmo a falta de provisão para notificar clientes selecionados quando os dados nos quais eles estão interessados mudam.
Se quisermos usar um protocolo que foi projetado para isso, o MQTT é a escolha natural. Além de superar as limitações do HTTP para esse tipo de comunicação, ele foi testado em campo com muitos sensores que mudam com muita frequência e, pronto para uso, pode fazer coisas sofisticadas, como armazenar o último valor bom conhecido ou ter comandos de cliente específicos que permitem receber atualizações sobre valores específicos ou um conjunto deles. O MQTT foi projetado como um protocolo para publicação/assinatura (pub/sub) nos cenários comuns da IoT. O servidor que controla todas as comunicações é comumente chamado de broker, e nossos sensores serão seus clientes.

Conecte-se ao WiFi

Agora que entendemos melhor por que estamos usando o MQTT, vamos nos conectar ao nosso corretor e enviar os dados obtidos do nosso sensor para que sejam publicados lá.
No entanto, antes de poder fazer isso, precisamos nos conectar ao WiFi.
É importante ter em mente que a placa que estamos usando tem suporte para WiFi, mas apenas no 2.4GHz. Ele não poderá se conectar ao seu roteador usando a banda de 5GHz, não importa o quão gentilmente você peça para fazê-lo.
Além disso, a menos que você seja um milionário rico e tenha uma boa ilha para se concentrar em acompanhar esse conteúdo, seria sensato usar uma senha bastante forte para manter usuários não autorizados fora de sua rede.
  1. Vamos começar definindo alguma estrutura para manter os dados de autenticação para acessar a rede:
    1struct Configuration {
    2 wifi_ssid: &'static str,
    3 wifi_password: &'static str,
    4}
  2. Poderíamos definir os valores no código, mas gosto mais da abordagem sugerida pela Ferrous Systems. Usaremos a caixatoml_cfg. Teremos valores padrão (inúteis neste caso, a não ser para obter um erro) que serão substituídos pelo uso de um arquivo toml com os valores desejados. Primeiro, o mais importante: Vamos adicionar a caixa:
    1cargo add toml-cfg
  3. Vamos agora anotar a estrutura com algumas macros:
    1#[toml_cfg::toml_config]
    2struct Configuration {
    3 #[default("NotMyWifi")]
    4 wifi_ssid: &'static str,
    5 #[default("NotMyPassword")]
    6 wifi_password: &'static str,
    7}
  4. Agora podemos adicionar um arquivocfg.toml com os valoresreais desses parâmetros.
    1[mosquitto-bzzz]
    2wifi_ssid = "ThisAintEither"
    3wifi_password = "NorIsThisMyPassword"
  5. Por favor, lembre-se de adicionar esse nome de arquivo à configuração do .gitignore, para que ele não acabe em nosso repositório com nossos segredos mais caros:
    1echo "cfg.toml" >> .gitignore
  6. O código para se conectar ao WiFi é um pouco entediante. Faz sentido fazer isso em uma função diferente:
    1fn connect_to_wifi(ssid: &str, passwd: &str) {}
  7. Essa função deve ter uma maneira de nos informar se houve um problema, mas queremos simplificar o tratamento de erros, por isso adicionamos a caixaanyhow:
    1cargo add anyhow
  8. Agora podemos usar o tipoResult fornecido por de qualquer forma (import anyhow::Result;). Dessa forma, não precisamos nos cansar de criar e usar um tipo de erro personalizado.
    1fn connect_to_wifi(ssid: &str, passwd: &str) -> Result<()> {
    2 Ok(())
    3}
  9. Se a função não obtiver um SSID, ela não conseguirá se conectar ao WiFi, portanto, é melhor parar aqui e retornar um erro (import anyhow::bail;):
    1if ssid.is_empty() {
    2 bail!("No SSID defined");
    3}
  10. Se a função receber uma senha, assumiremos que a autenticação usa WPA2. Caso contrário, nenhuma autenticação será usada (use esp_idf_svc::wifi::AuthMethod;):
    1let auth_method = if passwd.is_empty() {
    2 AuthMethod::None
    3} else {
    4 AuthMethod::WPA2Personal
    5};
  11. Vamos precisar de uma instância do loop do sistema para manter a conexão com o WiFi vivo e funcionando, então acessamos o singleton do loop de eventos do sistema (use esp_idf_svc::eventloop::EspSystemEventLoop; e use anyhow::Context).
    1let sys_loop = EspSystemEventLoop::take().context("Unable to access system event loop.")?;
  12. Embora não seja necessário, o esp32 armazena alguns dados de conexões de rede anteriores no armazenamento não-volátil, portanto, obter acesso a ele simplificará e acelerará o processo de conexão (use esp_idf_svc::nvs::EspDefaultNvsPartition;).
    1let nvs = EspDefaultNvsPartition::take().context("Unable to access default NVS partition")?;
  13. A conexão com o WiFi é feita por meio do modem, que pode ser acessado por meio dos periféricos da placa. Passamos os periféricos, obtemos o modem e o usamos para primeiro envolvê-lo com um driver WiFi e, em seguida, obter uma instância que usaremos para gerenciar a conexão WiFi (use esp_idf_svc::wifi::{EspWifi, BlockingWifi};):
    1fn connect_to_wifi(ssid: &str, passwd: &str,
    2 modem: impl Peripheral<P = modem::Modem> + 'static,
    3) -> Result<()> {
    4 // Auth checks here and sys_loop ...
    5 let mut esp_wifi = EspWifi::new(modem, sys_loop.clone(), Some(nvs))?;
    6 let mut wifi = BlockingWifi::wrap(&mut esp_wifi, sys_loop)?;
  14. Em seguida, adicionamos uma configuração ao WiFi (use esp_idf_svc::wifi;):
    1wifi.set_configuration(&mut wifi::Configuration::Client(
    2 wifi::ClientConfiguration {
    3 ssid: ssid
    4 .try_into()
    5 .map_err(|_| anyhow::Error::msg("Unable to use SSID"))?,
    6 password: passwd
    7 .try_into()
    8 .map_err(|_| anyhow::Error::msg("Unable to use Password"))?,
    9 auth_method,
    10 ..Default::default()
    11 },
    12))?;
  15. Com a configuração instalada, iniciamos o rádio WiFi, conectamos à rede WiFi e esperamos que a conexão seja concluída. Qualquer erro aparecerá:
    1wifi.start()?;
    2wifi.connect()?;
    3wifi.wait_netif_up()?;
  16. É útil neste ponto exibir os dados da conexão.
    1let ip_info = wifi.wifi().sta_netif().get_ip_info()?;
    2log::info!("DHCP info: {:?}", ip_info);
  17. Também queremos retornar a variável que mantém a conexão. Caso contrário, a conexão será fechada quando sair do escopo no final desta função. Mudamos a assinatura para poder fazer isso:
    1) -> Result<Box<EspWifi<'static>>> {
  18. E retorne esse valor:
    1Ok(Box::new(wifi_driver))
  19. Vamos inicializar a conexão com o WiFi a partir de nossa função para ler o ruído, então vamos adicionar o modem como parâmetro:
    1fn read_noise_level<GPIO>(
    2 adc1: ADC1,
    3 adc1_pin: GPIO,
    4 modem: impl Peripheral<P = modem::Modem> + 'static,
    5) -> !
  20. Este novo parâmetro deve ser inicializado na função principal:
    1let modem = peripherals.modem;
  21. E passamos para a função quando geramos o tópico:
    1scope.spawn(|| read_noise_level(adc, adc_pin, modem));
  22. Dentro da função em que planejamos usar esses parâmetros, recuperamos a configuração. A constante CONFIGURATIONé gerada automaticamente pela caixacfg-toml usando o tipo da estrutura:
    1let app_config = CONFIGURATION;
  23. Em seguida, tentamos nos conectar ao WiFi usando esses parâmetros:
    1let _wifi = match connect_to_wifi(app_config.wifi_ssid, app_config.wifi_password, modem) {
    2 Ok(wifi) => wifi,
    3 Err(err) => {
    4
    5 }
    6};
  24. E, ao lidar com o caso de erro, alteramos o valor do status:
    1log::error!("Connect to WiFi: {}", err);
    2status.store(DeviceStatus::WifiError as u8, Relaxed);
  25. Esta função não aceita o estado como argumento, então o adicionamos à sua assinatura:
    1fn read_noise_level<GPIO>(
    2 status: &AtomicU8,
  26. Esse argumento é fornecido quando o tópico é gerado:
    1scope.spawn(|| read_noise_level(status, adc, adc_pin, modem));
  27. Não queremos mais que o status seja alterado sequencialmente, então removemos esse thread e a função que estava implementando essa alteração.
  28. Executamos este código com cargo r para verificar se podemos nos conectar à rede. No entanto, esta versão vai falhar. } Nossa função excederá o tamanho padrão da pilha de um thread que, por padrão, é 4Kbytes.
  29. Podemos usar um construtor de thread, em vez da funçãospawn, para alterar o tamanho da pilha:
    1thread::Builder::new()
    2 .stack_size(6144)
    3 .spawn_scoped(scope, || read_noise_level(status, adc, adc_pin, modem))
    4 .unwrap();
  30. Depois de realizar essa alteração, executamos novamente cargo r e ela deve funcionar conforme o esperado.

Configurar o corretor MQTT

A próxima etapa após a conexão com o WiFi é conectar-se ao corretor MQTT como cliente, mas ainda não temos um corretor MQTT. Nesta seção, mostrarei como instalar o Mosquitto, que é um projeto de código aberto da Eclipse Foundation.
  1. Para esta seção, precisamos ter um corretor MQTT. No meu caso, estarei instalando o Mosquitto, que implementa versões 3.1.1 e 5.0 do protocolo MQTT. Ele funcionará no mesmo Raspberry Pi que estou usando como estação de coleta.
    1sudo apt-get update && sudo apt-get upgrade
    2sudo apt-get install -y {mosquitto,mosquitto-clients,mosquitto-dev}
    3sudo systemctl enable mosquitto.service
  2. Modificamos a configuração do Mosquitto para permitir que os clientes se conectem de fora do host local. Precisamos de algumas credenciais e de uma configuração que imponha a autenticação:
    1sudo mosquitto_passwd -c -b /etc/mosquitto/passwd soundsensor "Zap\!Pow\!Bam\!Kapow\!"
    2sudo sh -c 'echo "listener 1883\nallow_anonymous false\npassword_file /etc/mosquitto/passwd" > /etc/mosquitto/conf.d/remote_access.conf'
    3sudo systemctl restart mosquitto
  3. Vamos testar se podemos assinar e publicar em um tópico. A convenção de nomenclatura tende a usar apenas letras minúsculas, números e traços e reserva os traços para separar os tópicos hierarquicamente. Em um terminal, assine o testTopic:
    1mosquitto_sub -t test/topic -u soundsensor -P "Zap\!Pow\!Bam\!Kapow\!"
  4. E em outro terminal, publique algo nele:
    1mosquitto_pub -d -t test/topic -m "Hola caracola" -u soundsensor -P "Zap\!Pow\!Bam\!Kapow\!"
  5. Você deve ver a mensagem que escrevemos no segundo terminal aparecer no primeiro. Isso significa que o Mosquitto está funcionando conforme o esperado.

Publicar no MQTT a partir do sensor

Com o corretor MQTT instalado e pronto, podemos escrever o código para conectar nosso sensor a ele como um cliente MQTT e publicar seus dados.
  1. Vamos precisar das credenciais que acabamos de criar para publicar dados no corretor MQTT, então os adicionamos à estruturaConfiguration:
    1#[toml_cfg::toml_config]
    2struct Configuration {
    3 #[default("NotMyWifi")]
    4 wifi_ssid: &'static str,
    5 #[default("NotMyPassword")]
    6 wifi_password: &'static str,
    7 #[default("mqttserver")]
    8 mqtt_host: &'static str,
    9 #[default("")]
    10 mqtt_user: &'static str,
    11 #[default("")]
    12 mqtt_password: &'static str,
    13}
  2. Você deve se lembrar de adicionar os valores que fazem sentido ao arquivocfg.toml para seu ambiente. Não espere obtê-los do meu repositório, porque solicitamos ao Git para ignorar este arquivo. No mínimo, você precisa do nome do host ou endereço IP do seu corretor MQTT. Copie o nome de usuário e senha que criamos anteriormente:
    1[mosquitto-bzzz]
    2wifi_ssid = "ThisAintEither"
    3wifi_password = "NorIsThisMyPassword"
    4mqtt_host = "mqttsystem"
    5mqtt_user = "soundsensor"
    6mqtt_password = "Zap!Pow!Bam!Kapow!"
  3. Retornando à função que criamos para ler o sensor de ruído, agora podemos inicializar um cliente MQTT após conectar ao Wi-Fi (use mqtt::client::{EspMqttClient, MqttClientConfiguration, QoS},:
    1let mut mqtt_client =
    2 EspMqttClient::new()
    3 .expect("Unable to initialize MQTT client");
  4. O primeiro parâmetro é um URL para o servidor MQTT que incluirá o usuário e a senha, se definido:
    1let mqtt_url = if app_config.mqtt_user.is_empty() || app_config.mqtt_password.is_empty() {
    2 format!("mqtt://{}/", app_config.mqtt_host)
    3} else {
    4 format!(
    5 "mqtt://{}:{}@{}/",
    6 app_config.mqtt_user, app_config.mqtt_password, app_config.mqtt_host
    7 )
    8};
  5. O segundo parâmetro é a configuração. Vamos adicioná-los à criação do cliente MQTT:
    1EspMqttClient::new(&mqtt_url, &MqttClientConfiguration::default(), |_| {
    2 log::info!("MQTT client callback")
    3})
  6. Para publicar, precisamos definir o tópico:
    1const TOPIC: &str = "home/noise sensor/01";
  7. E uma variável que será usada para conter a mensagem que publicaremos:
    1let mut mqtt_msg: String;
  8. Dentro do loop, formataremos o valor do ruído porque ele é enviado como uma string:
    1mqtt_msg = format!("{}", d_b);
  9. Publicamos este valor usando o cliente MQTT:
    1if let Ok(msg_id) = mqtt_client.publish(TOPIC, QoS::AtMostOnce, false, mqtt_msg.as_bytes())
    2{
    3 println!(
    4 "MSG ID: {}, ADC values: {:?}, sum: {}, and dB: {} ",
    5 msg_id, sample_buffer, sum, d_b
    6 );
    7} else {
    8 println!("Unable to send MQTT msg");
    9}
  10. Como fizemos quando publicamos a partir da linha de comando, precisamos assinar, em um terminal independente, o tópico no qual planejamos publicar. Neste caso, começaremos com home/noise sensor/01. Observe que representamos uma hierarquia, ou seja, há sensores de ruído em casa e cada um dos sensores tem um identificador. Além disso, observe que os níveis da hierarquia são separados por barras e podem incluir espaços em seus nomes.
    1mosquitto_sub -t "home/noise sensor/01" -u soundsensor -P "Zap\!Pow\!Bam\!Kapow\!"
  11. Por fim, compilamos e executamos o firmware com cargo r e poderemos ver esses valores aparecendo no terminal inscrito no tópico.

Use um ID exclusivo para cada sensor

Gostaria de terminar este firmware resolvendo um problema que não aparecerá até que tenhamos dois sensores ou mais. Nosso firmware usa um tema constante. Isso significa que dois sensores com o mesmo firmware usarão o mesmo tópico e não teremos como saber qual valor corresponde a qual sensor. Uma opção melhor é usar um identificador exclusivo que será diferente para cada placa6 ESP32-C. Podemos usar o endereço MAC para isso.
  1. Vamos começar criando uma função que retorne esse identificador:
    1fn get_sensor_id() -> String {
    2}
  2. Nossa função usará uma função insegura do ESP-IDF e formatará o resultado como String (use esp_idf_svc::sys::{esp_base_mac_addr_get, ESP_OK}; e use std::fmt::Write). A função que retorna o endereço MAC usa um ponteiro e, tendo sido escrita em C++, não poderia se importar menos com as regras de segurança que o código Rust deve obedecer. Essa função é considerada insegura e, como tal, o Rust exige que a usemos dentro do unsafe escopo . É a maneira deles de nos dizer: " Aqui estão os dragões... e você sabe disso " :
    1let mut mac_addr = [0u8; 8];
    2unsafe {
    3 match esp_base_mac_addr_get(mac_addr.as_mut_ptr()) {
    4 ESP_OK => {
    5 let sensor_id = mac_addr.iter().fold(String::new(), |mut output, b| {
    6 let _ = write!(output, "{b:02x}");
    7 output
    8 });
    9 log::info!("Id: {:?}", sensor_id);
    10 sensor_id
    11 }
    12 _ => {
    13 log::error!("Unable to get id.");
    14 String::from("BADCAFE00BADBEEF")
    15 }
    16 }
    17}
  3. Em seguida, usamos a função antes de definir o tópico e usamos seu resultado com ela:
    1let sensor_id = get_sensor_id();
    2let topic = format!("home/noise sensor/{sensor_id}");
  4. E mudamos um pouco a forma como publicamos os dados para usar o tópico:
    1if let Ok(msg_id) = mqtt_client.publish(&topic, QoS::AtMostOnce, false, mqtt_msg.as_bytes())
  5. Também precisamos alterar a assinatura para ouvirmos todos os tópicos que começam com home/sensor/ e ter mais um nível:
    1mosquitto_sub -t "home/noise sensor/+" -u soundsensor -P "Zap\!Pow\!Bam\!Kapow\!"
  6. Compilamos e executamos com cargo r e os valores começam a aparecer no terminal onde a assinatura foi iniciada.

Recapitulação e trabalho futuro

Neste artigo, usamos o Rust para escrever o microcódigo para uma placa32-C6-DevKitC-1 do início ao fim. Embora possamos concordar que o Python foi uma abordagem mais fácil para nosso primeiro software, considero o Rust uma linguagem mais robusta, acessível e útil para esse fim.
O firmware que criamos pode informar o usuário sobre qualquer problema usando um LED RGB, medir o ruído em algo próximo o suficiente aos decibéis, conectar nossa placa ao WiFi e depois ao nosso corretor MQTT como cliente e publicar as medidas do nosso sensor de ruído. Nada mal para um único tutorial.
Até nos adiantamos e adicionamos algum código para garantir que sensores diferentes com o mesmo firmware publiquem seus valores em tópicos diferentes. E, para isso, fizemos uma breve incursão no universo do unsafe Rust e sobrevivemos ao deserto. Agora você pode ir a um bar e dizer aos seus amigos: "Eu escrevi unsafe Rust." Muito bem!
Em nosso próximo artigo, escreveremos código C++ novamente para coletar os dados do intermediário MQTT e enviá-los para nossa instância do MongoDB Atlas na nuvem. Então, prepare-se!
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.
Iniciar a conversa

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

CMake + Conan + VS Code


Sep 04, 2024 | 6 min read
Tutorial

Armazenar dados binários com MongoDB e C++


Sep 18, 2023 | 6 min read
Tutorial

Série temporal MongoDB com C++


Sep 17, 2024 | 6 min read
Tutorial

Adote BLE: implementando sensores BLE com MCU Devkits


Apr 02, 2024 | 13 min read
Sumário