Estratégias no FARIA
No FARIA, a lógica de tomada de decisão foi completamente isolada da infraestrutura de hardware através do Padrão de Projeto comportamental Strategy (Estratégia).
O objetivo desta arquitetura é permitir que os desenvolvedores foquem puramente na matemática, no controle e na lógica de luta, sem nunca precisarem se preocupar com como um motor gira, como uma thread é travada ou como um arquivo JSON é lido.
É onde os membros mais vão trabalhar.
Isolamento de Domínio
É aqui que o Blackboard entra em ação. As classes de estratégia do FARIA são cegas e surdas para o mundo real. Elas não interagem com a HAL. Em vez disso, elas recebem uma referência para o Blackboard a cada ciclo de execução (chamamos de tick, igual no Minecraft).
Toda a inteligência do robô se resume a uma equação simples:
- Ler os dados do Blackboard (vindos da Thread de Percepção).
- Processar a lógica de controle e decidir o que fazer.
- Escrever os comandos resultantes de volta no Blackboard, para a Thread de Ação (ex: velocidade de -100 a 100 para os motores).
A Anatomia de uma Estratégia
Para que o framework reconheça uma nova estratégia, o desenvolvedor precisa criar uma classe que cumpra rigorosamente um contrato. Este contrato é garantido pela herança da classe base puramente virtual Strategy.
Todo arquivo de estratégia (um .hpp dentro de include/strategies/) deve conter o seguinte boilerplate (código base obrigatório):
- Herança: A classe deve herdar publicamente de
Strategy:class EstrategiaMuitoFoda : public Strategy. - O Método
execute: Devemos sobrescrever o métodovoid execute(Blackboard& bb) override. É dentro dele que a lógica roda. Ele é chamado continuamente pela Thread de Ação do framework. - O Método
get_name: Devemos sobrescrever o métodostd::string getName() const override. Ele retorna uma string com o nome da estratégia para fins de logs, depuração e telemetria.
Por conveção, todas as estratégias devem ser implementadas em arquivos
.hpp(C++ Header) e comStrategyno final do nome da classe. Exemplo:AgressivoMalucoStrategy.hpp.
Exemplo de Estratégias
Estratégia simples para evitar obstáculos:
// Arquivo include/strategies/EvitaObstaculoStrategy.hpp
#pragma once
#include "../Interfaces.hpp"
// A classe herda do contrato base
class EvitaObstaculo : public Strategy {
public:
// O método execute é onde a lógica opera a cada ciclo
void execute(Blackboard& bb) override {
// Passo 1: Ler o sensor usando o alias definido no JSON
// Utilizamos std::optional pois o hardware pode ter falhado
auto distOpt = bb.getSensorValue("ultrassomFrente");
// Se o sensor não estiver lendo, assumimos uma distância segura (ex: 999mm)
int distMm = distOpt.value_or(999);
// Passo 2: Lógica de controle simples
if (distMm < 200) {
// Obstáculo muito perto: Girar no próprio eixo para a direita
bb.setMotorSpeed("motorEsquerdo", 50);
bb.setMotorSpeed("motorDireito", -50);
} else if (distMm < 500) {
// Obstáculo se aproximando: Reduzir velocidade
bb.setMotorSpeed("motorEsquerdo", 30);
bb.setMotorSpeed("motorDireito", 30);
} else {
// Caminho livre: Mete marcha
bb.setMotorSpeed("motorEsquerdo", 100);
bb.setMotorSpeed("motorDireito", 100);
}
}
// O método getName identifica a estratégia no sistema
[[nodiscard]] std::string getName() const override {
return "EvitaObstaculo";
}
};
O Strategy Registry
Se você criasse uma nova estratégia, como o FARIA saberia instanciá-la a partir de uma string recebida por um menu ou pela rede, sem criar um bloco de if / else if gigantesco?
O desenvolvedor do FARIA é muito foda, então ele criou um mecanismo de registro de estratégias. Trata-se de um mapa (ou Dicionário) que associa uma string a uma função de fábrica (uma função lambda que sabe como criar o objeto na memória).
Para acoplar a estratégia recém-criada ao robô, o desenvolvedor só precisa adicioná-la ao Registro no ponto de entrada (main.cpp).
Exemplo de registro no main.cpp:
#include "strategies/EvitaObstaculo.hpp" // Importar o header da nova estratégia
// ... dentro do main() ...
StrategyRegistry registry;
// Registrando a estratégia através de uma lambda factory
registry.registerStrategy("EvitaObstaculo", []() {
return std::make_unique<EvitaObstaculo>();
});
// Solicitando a criação dinâmica e injetando no robô
std::string chosenStrategy = "EvitaObstaculo";
robot.setStrategy(registry.create(chosenStrategy));
Strategy Composite
É uma exigência tática comum que o robô execute uma manobra de abertura nos primeiros milissegundos da luta (Estratégia Inicial) e, em seguida, mude para a sua rotina de batalha principal.
A classe DualStrategy atua como um wrapper. Ela assina o mesmo contrato da classe base Strategy, o que a torna invisível para a Engine do robô. Seu único papel é encapsular dois ponteiros de estratégia distintos e transferir o controle do Blackboard da estratégia inicial para a principal após um tempo predeterminado de execução.
Por padrão esse tempo é de 2000 ms.
Em vez de sobrecarregar o StrategyRegistry com o registro de todas as combinações possíveis de estratégias iniciais e principais, a montagem da DualStrategy deve ser feita sob demanda de forma declarativa, por exemplo:
if (!cfg.initialStrategy.empty()) {
// Caso uma manobra inicial tenha sido selecionada na interface,
// a DualStrategy envelopa as duas lógicas.
finalStrategy = std::make_unique<DualStrategy>(
"EstrategiaMuitoFoda",
registry.create(cfg.initialStrategy),
registry.create(cfg.mainStrategy),
2000
);
} else {
// Caso a abertura seja dispensada, a principal assume com zero overhead.
finalStrategy = registry.create(cfg.mainStrategy);
}
robot.setStrategy(std::move(finalStrategy));
Máquinas de Estados
Na robótica de tempo real, bloquear a thread principal com comandos como sleep() ou laços while interrompe a leitura dos sensores e "cega" o robô. O FARIA soluciona manobras sequenciais complexas (como as estratégias Olezinho e AutoAsas) através de Máquinas de Estado Finito (FSM).
Conceito
Uma FSM divide o comportamento do robô em "Estados" discretos e mutuamente exclusivos. Em vez de esperar uma ação terminar bloqueando o código, a estratégia verifica rapidamente em qual estado está, envia o pulso correspondente para os motores e devolve o controle para o loop de execução.
Aplicação
Utilizamos um enum class State privado dentro da estratégia (ex: OPEN_WINGS, COMBAT). O método execute() contém um bloco switch/case que roteia a lógica baseado no estado atual.
Transição e Sincronização
Quando a meta física do estado atual é atingida (ex: o encoder bateu o alvo numérico), a variável de estado é atualizada, acionando o próximo bloco case no ciclo seguinte. Para evitar Race Conditions no instante zero, é ideal que a FSM sempre valide se o Blackboard já recebeu a primeira leitura de hardware utilizando std::optional::has_value() antes de calcular o alvo, como na estratégia AutoAsas.
Táticas
Anteriormente, sempre que queriamos que o robô abrisse as asas ou fizesse uma varredura de busca, acabávamos copiando e colando blocos inteiros de código de movimentação de motores dentro das novas estratégias. Isso inflava os arquivos, quebrava o princípio DRY (Don't Repeat Yourself) e multiplicava bugs.
Nossa solução foi utilizar o princípio de Composição sobre Herança. Em vez de escrever lógicas gigantescas em cada estratégia, criaremos Táticas, classes modulares que obedecem a um contrato restrito e atuam como Sub-Máquinas de Estado. A Estratégia Principal deixa de calcular encoders e passa a atuar apenas como uma "Orquestradora".
Como implementar uma Tática?
Toda tática deve ser criada na pasta include/tactics/ e herdar da interface base Tactic. O contrato exige a implementação de três métodos cruciais:
class Tactic {
public:
// Processa a lógica usando a memória central
virtual void execute(Blackboard& bb) = 0;
// Avisa a Estratégia Orquestradora que a manobra acabou
[[nodiscard]] virtual bool isFinished() const = 0;
// Limpa as variáveis para a tática ser reutilizada no mesmo round
virtual void reset() = 0;
};
Ao criar uma tática concreta, você deve declarar suas próprias variáveis de controle de estado e meta de encoder. Quando o motor atingir o alvo, envie velocidade zero e mude sua flag interna para que o isFinished() retorne verdadeiro.
Como uso uma Tática?
Na sua Estratégia Principal, o uso é extremamente limpo. Você declara a Tática como uma variável membro, o que garante segurança de memória. Durante o estado correspondente da sua Máquina de Estados, você apenas delega a execução para a tática e aguarda ela sinalizar que terminou:
class AutoAsasStrategy : public Strategy {
private:
OpenWingsTactic m_wingTactic; // 1. Instancia a tática por Composição
public:
void execute(Blackboard& bb) override {
switch (m_currentState) {
case State::OPEN_WINGS:
// 2. Delega o controle do hardware para a tática
m_wingTactic.execute(bb);
// 3. Monitora o fim para avançar de estado
if (m_wingTactic.isFinished()) {
m_currentState = State::COMBAT;
}
break;
// ...
}
}
};