Pular para conteúdo

4.0. Embarcado com Padrões de Projetos

Introdução

O desenvolvimento de sistemas embarcados, como o proposto neste projeto, transcende a mera seleção de componentes de hardware. A arquitetura do software embarcado (firmware) desempenha um papel crucial na robustez, manutenibilidade e escalabilidade da solução. Neste contexto, a aplicação de Padrões de Projeto de Software torna-se uma prática valiosa, mesmo em ambientes com recursos limitados.

Este capítulo detalha a abordagem adotada para integrar Padrões de Projeto no desenvolvimento do firmware para o microcontrolador ESP32-C3, focado na coleta de dados do sensor MPU6050. A simulação através da plataforma Wokwi foi instrumental, permitindo o design, implementação e teste iterativo do hardware virtualizado e do código associado, facilitando a aplicação efetiva dos padrões discutidos a seguir.

2. Escolha Estratégica de Hardware

A fundação de um sistema embarcado eficiente reside na seleção criteriosa de seus componentes. Para este projeto, a escolha foi guiada pela necessidade de conectividade, capacidade de processamento adequada, eficiência energética e viabilidade de simulação.

  • Microcontrolador ESP32-C3: Optou-se pelo ESP32-C3 da Espressif Systems devido ao seu excelente balanço entre custo e benefício. Seu núcleo RISC-V de 32 bits, conectividade Wi-Fi e Bluetooth LE 5.0 integrados, e foco em baixo consumo energético o tornam ideal para aplicações IoT. A disponibilidade de ferramentas de desenvolvimento robustas, como o ESP-IDF e o suporte no Arduino IDE, além da sua presença no simulador Wokwi, foram fatores decisivos.

  • Sensor MPU6050: Este sensor combina um acelerômetro e um giroscópio de 3 eixos (6-DOF), comunicando-se via I2C. Sua popularidade, baixo custo, disponibilidade de bibliotecas e a capacidade de simulação no Wokwi o tornaram uma escolha natural para o projeto.

A imagem abaixo demostra a conexão do Hardware.



Autores: Altino Arthur, Márcio Henrique e Daniel de Sousa

3. Padrões de Projeto Aplicados ao Firmware

Para estruturar o firmware do ESP32-C3 de forma organizada, flexível e de fácil manutenção, foram considerados e aplicados os seguintes padrões de projeto:

3.1. Builder

3.1.1. Explicação do Padrão Builder

O padrão Builder é um padrão de projeto criacional que visa separar a construção de um objeto complexo de sua representação, permitindo que o mesmo processo de construção possa criar diferentes representações. Ele é particularmente útil quando um objeto pode ter múltiplas configurações opcionais ou quando o processo de criação do objeto é composto por várias etapas. Em vez de utilizar construtores com uma longa lista de parâmetros (muitos dos quais podem ser opcionais), o Builder permite definir esses parâmetros passo a passo de forma fluida e legível, culminando na construção do objeto final. Isso melhora a clareza do código, facilita a criação de objetos com configurações variadas e evita o problema dos "construtores telescópicos".

Principais Benefícios:

  • Permite variar a representação interna do produto.
  • Encapsula o código de construção e representação.
  • Fornece controle mais preciso sobre o processo de construção.
  • Melhora a legibilidade ao criar objetos complexos.

3.1.2. Aplicação do Builder no Projeto

Esse padrão foi aplicado levando em conta a necessidade de gerar diferentes tipos de mensagens para o sistema de monitoramento: UpdateMessage (para atualizações periódicas de medições), StatusMessage (para informar o estado operacional do sistema) e AlertMessage (para notificar eventos críticos, como uma queda detectada). Todas essas mensagens compartilham uma estrutura base, mas diferem em seu conteúdo específico (payload).

Com isso, foram definidos os seguintes componentes para a implementação do padrão Builder:

Produto Abstrato (BaseMessage.h): Define a interface comum para todas as mensagens. Inclui atributos como timestamp e messageType, e um método virtual getFormattedMessage() que delega a formatação do conteúdo específico para um método virtual puro getPayloadDetails().

// Trecho de BaseMessage.h
#ifndef BASEMESSAGE_H
#define BASEMESSAGE_H

#include <string>
#include <memory>
#include <Arduino.h>

class BaseMessage {
public:
    unsigned long timestamp;
    std::string messageType;

    BaseMessage(std::string type) : timestamp(millis()), messageType(std::move(type)) {}
    virtual ~BaseMessage() = default;

    virtual std::string getFormattedMessage() const {
        std::string formattedMsg = "\n╔══════════════════════════════════════════════════════════╗\n";
        formattedMsg += "\r║ Tipo      : " + messageType + "\r\n";
        // ... (formatação do timestamp) ...
        formattedMsg += "║ Payload   :\r\n" + getPayloadDetails();
        formattedMsg += "\r\n╚══════════════════════════════════════════════════════════╝\n";
        return formattedMsg;
    }

protected:
    virtual std::string getPayloadDetails() const = 0;
};
#endif // BASEMESSAGE_H


Produtos Concretos (AlertMessage.h, UpdateMessage.h, StatusMessage.h): São as classes que herdam de BaseMessage e implementam getPayloadDetails() para fornecer seu conteúdo específico. Por exemplo, AlertMessage armazena os dados do sensor no momento do alerta e uma razão.

// Trecho de AlertMessage.h
#ifndef ALERTMESSAGE_H
#define ALERTMESSAGE_H

#include "BaseMessage.h"
#include "Singleton/Sensor.h" // Assumindo que Sensor.h está em um subdiretório Singleton
#include <Arduino.h>

class AlertMessage : public BaseMessage {
public:
    Sensor::SensorData criticalSensorData;
    std::string reason;

    AlertMessage() : BaseMessage("ALERTA_QUEDA") {}

protected:
    std::string getPayloadDetails() const override {
        char buffer[500];
        snprintf(buffer, sizeof(buffer),
            "║  ================ [ ALERTA ] ================\r\n"
            "║  Motivo                  : %s\r\n"
            // ... (restante da formatação) ...
            , reason.c_str(), criticalSensorData.ax_g /* ... etc ... */);
        return buffer;
    }
};
#endif // ALERTMESSAGE_H

Builder Abstrato (MessageBuilder.h): Define a interface para criar as partes do objeto BaseMessage. Possui métodos virtuais puros como reset(), buildSpecificProductParts() e getProduct(). O método buildTimestamp() foi incluído na interface, mas como o timestamp é tratado no construtor de BaseMessage, sua implementação nos builders concretos é vazia.

// Trecho de MessageBuilder.h
#ifndef MESSAGE_BUILDER_H
#define MESSAGE_BUILDER_H

#include <memory>
#include "BaseMessage.h"
#include "Singleton/Sensor.h" // Assumindo que Sensor.h está em um subdiretório Singleton

class MessageBuilder {
public:
    virtual ~MessageBuilder() = default;
    virtual void reset() = 0;
    virtual void buildTimestamp() = 0;
    virtual void buildSpecificProductParts(const Sensor::SensorData& data) = 0;
    virtual std::unique_ptr<BaseMessage> getProduct() = 0;
};
// ... (definições dos Builders Concretos abaixo) ...
#endif // MESSAGE_BUILDER_H

Builders Concretos (AlertMessageBuilder, UpdateMessageBuilder, StatusMessageBuilder em MessageBuilder.h): Implementam a interface MessageBuilder para construir um tipo específico de mensagem. Cada builder é responsável por instanciar seu produto (e.g., AlertMessage) e preencher seus campos específicos através do método buildSpecificProductParts().

// Trecho de MessageBuilder.h - Exemplo com AlertMessageBuilder
#include "AlertMessage.h" // E outros produtos concretos

class AlertMessageBuilder : public MessageBuilder {
private:
    std::unique_ptr<AlertMessage> product;
public:
    void reset() override { product = std::make_unique<AlertMessage>(); }
    void buildTimestamp() override { /* Timestamp tratado em BaseMessage */ }
    void buildSpecificProductParts(const Sensor::SensorData& data) override {
        if (!product) reset();
        product->criticalSensorData = data;
        product->reason = "Valores de aceleracao anomalos";
    }
    std::unique_ptr<BaseMessage> getProduct() override { return std::move(product); }
};

Director (MessageDirector.h): Orquestra o processo de construção utilizando um objeto MessageBuilder fornecido. O método constructMessage() do MessageDirector chama sequencialmente os passos de construção (reset, buildTimestamp, buildSpecificProductParts) no builder configurado e, por fim, obtém o produto final.

// Trecho de MessageDirector.h
#ifndef MESSAGE_DIRECTOR_H
#define MESSAGE_DIRECTOR_H

#include "MessageBuilder.h"
#include "Singleton/Sensor.h" // Assumindo que Sensor.h está em um subdiretório Singleton

class MessageDirector {
private:
    MessageBuilder* builder_ = nullptr;
public:
    void setBuilder(MessageBuilder* builder) { this->builder_ = builder; }
    std::unique_ptr<BaseMessage> constructMessage(const Sensor::SensorData& data) {
        if (!builder_) {
            if (Serial) { Serial.println("ERRO: Builder nao configurado no Director!"); }
            return nullptr;
        }
        builder_->reset();
        builder_->buildTimestamp(); // Chamada mantida por conformidade com a interface
        builder_->buildSpecificProductParts(data);
        return builder_->getProduct();
    }
};
#endif // MESSAGE_DIRECTOR_H

Esta estrutura permite que o sistema crie diferentes tipos de mensagens de forma flexível e desacoplada. O MessageDirector padroniza o processo de construção, enquanto os ConcreteBuilders encapsulam a lógica específica de cada tipo de mensagem. A criação efetiva das mensagens é simplificada pelo uso do padrão Facade, como será visto adiante.

3.2. Singleton

3.2.1. Explicação do Padrão Singleton

O padrão Singleton é um padrão de projeto criacional que garante que uma classe tenha apenas uma única instância em todo o sistema e fornece um ponto de acesso global a essa instância. É frequentemente utilizado para gerenciar recursos compartilhados, como conexões de banco de dados, gerenciadores de logging, configurações de aplicação ou drivers de hardware que não devem ser instanciados múltiplas vezes. Ao restringir a instanciação, o Singleton evita o uso inconsistente ou conflituoso de um recurso global.

Principais Benefícios:

  • Garante uma única instância de uma classe.
  • Fornece um ponto de acesso global a essa instância.
  • Pode ser inicializado de forma tardia (lazy initialization).
  • Controla o acesso concorrente à instância única (importante em ambientes multithread).

3.2.2. Aplicação do Singleton no Projeto

No projeto, o padrão Singleton é aplicado à classe Sensor, que encapsula a lógica de interação com o sensor MPU6050. Esta abordagem garante que exista apenas uma instância controlando e acessando o hardware do sensor MPU6050 em todo o sistema, prevenindo múltiplas inicializações ou acessos conflitantes ao mesmo recurso físico.

Os componentes da implementação do Singleton na classe Sensor são:

Ponteiro Estático Privado para a Instância: A classe Sensor declara um ponteiro estático privado instance para si mesma. Este ponteiro armazenará o endereço da única instância da classe.

// Trecho de Sensor.h
private:
    static Sensor* instance;

A inicialização deste ponteiro é feita no ficheiro .cpp:

// Trecho de Sensor.cpp
Sensor* Sensor::instance = nullptr;

Construtor Privado: O construtor da classe Sensor é declarado como private. Isso impede que objetos Sensor sejam criados através do operador new de fora da própria classe, garantindo que a instanciação seja controlada internamente.

// Trecho de Sensor.h
private:
    Sensor(int sda, int scl); // Construtor privado

Método Estático Público getInstance(): Um método público estático getInstance() serve como o único ponto de acesso para obter a instância da classe Sensor. Na primeira vez que este método é chamado, ele cria a instância (se ainda não existir) e a armazena no ponteiro estático instance. Em chamadas subsequentes, ele simplesmente retorna o ponteiro para a instância já existente. Os pinos SDA e SCL para a comunicação I2C são passados como parâmetros na primeira chamada para configurar o sensor.

// Trecho de Sensor.h
public:
    static Sensor* getInstance(int sda_pin = 21, int scl_pin = 22); // Valores padrão para ESP32 comum

// Trecho de Sensor.cpp
Sensor* Sensor::getInstance(int sda_pin, int scl_pin) {
    if (instance == nullptr) {
        instance = new Sensor(sda_pin, scl_pin);
    }
    return instance;
    }

No main.cpp, os pinos SDA_PIN (8) e SCL_PIN (9) são usados, adequados para o ESP32-C3 no Wokwi.

Prevenção de Cópia e Atribuição: Para garantir estritamente a unicidade da instância, o construtor de cópia e o operador de atribuição de cópia são deletados.

// Trecho de Sensor.h
private:
    // Previne cópia
    Sensor(const Sensor&) = delete;
    Sensor& operator=(const Sensor&) = delete;

Com esta implementação, qualquer parte do sistema que precise interagir com o sensor MPU6050 (para inicializá-lo ou ler dados) o fará através da chamada Sensor::getInstance(), garantindo o acesso à mesma e única instância do objeto Sensor. Isto é demonstrado no main.cpp:

// Trecho de main.cpp (setup)
Sensor* sensorInstance = Sensor::getInstance(SDA_PIN, SCL_PIN);
if (sensorInstance == nullptr || !sensorInstance->init()) {
    // ... tratamento de erro ...
}

// Trecho de main.cpp (loop)
Sensor* sensor = Sensor::getInstance(); // Não precisa passar os pinos novamente
if (!sensor || !sensor->isInitialized()) {
    // ... tratamento de erro ...
}
sensor->readSensorData();
Sensor::SensorData data = sensor->getSensorData();

3.3 Facade

3.3.1. Explicação do Padrão Facade

O padrão Facade é um padrão de projeto estrutural que fornece uma interface unificada e simplificada para um conjunto de interfaces em um subsistema mais complexo. A Facade define uma interface de nível mais alto que torna o subsistema mais fácil de usar, ocultando suas complexidades internas. Ele promove o baixo acoplamento entre o cliente e o subsistema, pois o cliente interage apenas com a Facade, e não diretamente com os múltiplos componentes do subsistema.

Principais Benefícios: * Simplifica o uso de subsistemas complexos. * Desacopla o cliente das classes internas do subsistema. * Fornece um ponto de entrada único para a funcionalidade do subsistema. * Pode ajudar a organizar um sistema em camadas

3.3.2. Aplicação do Facade no Projeto

No projeto, a classe MessageFacade atua como uma fachada para o subsistema de criação de mensagens. Este subsistema, como detalhado na seção do padrão Builder, envolve o MessageDirector e múltiplos MessageBuilders concretos (AlertMessageBuilder, UpdateMessageBuilder, StatusMessageBuilder). A MessageFacade simplifica a interação com este subsistema, fornecendo uma interface de alto nível para o cliente (o ficheiro main.cpp).

A implementação da MessageFacade consiste em:

Encapsulamento do Subsistema: A MessageFacade possui instâncias internas do MessageDirector e de todos os MessageBuilders concretos necessários. Estes componentes são a complexidade que a fachada visa ocultar.

// Trecho de MessageFacade.h
#ifndef MESSAGE_FACADE_H
#define MESSAGE_FACADE_H

#include <memory> // Para std::unique_ptr
#include "Builder/MessageDirector.h"
#include "Builder/MessageBuilder.h"
#include "Builder/BaseMessage.h"
#include "Singleton/Sensor.h" // Assumindo que Sensor.h está em um subdiretório Singleton

class MessageFacade {
private:
    // A Facade possui as instâncias do Director e dos Builders
    MessageDirector director_;
    AlertMessageBuilder alertBuilder_;
    UpdateMessageBuilder updateBuilder_;
    StatusMessageBuilder statusBuilder_;

public:
    MessageFacade() {
        // Construtor pode ser usado para inicializações, se necessário
    }
    // ... (métodos da facade abaixo) ...
};
#endif // MESSAGE_FACADE_H

Interface Simplificada: A MessageFacade expõe métodos públicos simples para cada tipo de mensagem que o cliente pode precisar criar. Por exemplo, createAlertMessage(const Sensor::SensorData& data) lida internamente com a configuração do MessageDirector para usar o AlertMessageBuilder e orquestra a construção da mensagem de alerta.

// Trecho de MessageFacade.h (continuação)
public:
// Método para criar uma mensagem de alerta
std::unique_ptr<BaseMessage> createAlertMessage(const Sensor::SensorData& data) {
    director_.setBuilder(&alertBuilder_);
    return director_.constructMessage(data);
}

// Método para criar uma mensagem de atualização
std::unique_ptr<BaseMessage> createUpdateMessage(const Sensor::SensorData& data) {
    director_.setBuilder(&updateBuilder_);
    return director_.constructMessage(data);
}

// Método para criar uma mensagem de status
std::unique_ptr<BaseMessage> createStatusMessage(const Sensor::SensorData& data) {
    director_.setBuilder(&statusBuilder_);
    return director_.constructMessage(data);
}

Uso pelo Cliente (main.cpp): O código cliente (main.cpp) interage apenas com a MessageFacade para criar os diferentes tipos de mensagens. Ele não precisa conhecer os detalhes internos do MessageDirector ou dos MessageBuilders específicos. Uma instância global da MessageFacade é criada para facilitar o acesso.

// Trecho de main.cpp
#include "Facade/MessageFacade.h" // Assumindo que MessageFacade.h está em um subdiretório Facade

// --- Instância Global da Facade ---
MessageFacade messageFacade;

// ... Dentro da função loop() ...
void loop() {
    Sensor* sensor = Sensor::getInstance();
    // ... (leitura do sensor e lógica de detecção de queda) ...
    Sensor::SensorData data = sensor->getSensorData();

    if (quedaDetectada) {
        Serial.println("\n\r[EVENTO] Queda detectada! Construindo mensagem de ALERTA...");
        std::unique_ptr<BaseMessage> alertMsg = messageFacade.createAlertMessage(data);
        displayMessage(alertMsg);
        delay(2000);
    }

    // ... (lógica para mensagens de atualização e status usando messageFacade) ...
    if (currentTime - lastUpdateTime > 5000) {
        Serial.println("\n\r[INFO] Construindo mensagem de ATUALIZACAO DE MEDICOES...");
        std::unique_ptr<BaseMessage> updateMsg = messageFacade.createUpdateMessage(data);
        displayMessage(updateMsg);
        lastUpdateTime = currentTime;
    }
}

A MessageFacade efetivamente simplifica a interface para o cliente, promove o baixo acoplamento e torna o subsistema de criação de mensagens mais fácil de usar e manter.

Demostração

O vídeo abaixo mostra o código rodando na prática no ambiente de simulação Wokwi, demonstrando a inicialização do sensor, a leitura dos dados e a geração das diferentes mensagens (Alerta, Atualização, Status) conforme os eventos ocorrem.

Implementações das Classes

As implementações completas das classes mencionadas neste documento podem ser conferidas no repositório oficial do projeto, disponível em:

https://github.com/UnBArqDsw2025-1-Turma01/2025.1-T01-_G1_Embarcado_Entrega_03/tree/main/Embarcado

O diretório src/monitora contém o código-fonte Java estruturado em pacotes.

Referências

REFACTORING GURU. Padrões de projeto comportamentais. Disponível em: https://refactoring.guru/pt-br/design-patterns/behavioral-patterns. Acesso em: 30 de maio de 2025.

Histórico de Versões

Versão Commit da Versão Data Descrição Autor(es) Revisor(es) Descrição da Revisão Commit da Revisão
1.0 Ver Commit 02/06/2025 Adição de Padrões de Projeto Aplicado no Código do Hardware Altino Arthur, Márcio Henrique e Daniel de Sousa Revisor