agrobot_base/Firmware/Modulos/CanService.h

549 lines
19 KiB
C++

#include "SerialService.h"
#ifndef CANSERVICE_H
#define CANSERVICE_H
#include <Arduino.h>
#include <driver/twai.h>
#include <esp_task_wdt.h>
// Garante o core de aplicação (ESP32-S3)
#ifndef APP_CPU_NUM
#define APP_CPU_NUM 1
#endif
class CanService {
public:
typedef void (*OnReceiveCallback)(int packetSize, int senderId,
CanMessagePosicaoDados posicao, byte* data, int dataLength);
// --- IDs especiais do teu protocolo ---
uint8_t ID_Num_sMOD = 0;
uint8_t ID_Num_sTOD = 250;
uint8_t ID_Num_sLRA = 251;
CanService(uint8_t nodeId, uint32_t baudRate = 250000)
: _nodeId(nodeId), _baudRate(baudRate) {}
void begin() {
PrintTela("[CAN] Iniciando TWAI...");
// Modo NORMAL (com ACK)
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(GPIO_NUM_5, GPIO_NUM_4, TWAI_MODE_NORMAL);
// Aumenta filas internas do driver para evitar enrosco
g_config.tx_queue_len = 32;
g_config.rx_queue_len = 32;
// (alerts_enabled será reconfigurado depois de start)
twai_timing_config_t t_config;
// 500 kbit/s (ajuste se precisar)
if (_baudRate == 250000)
t_config = TWAI_TIMING_CONFIG_250KBITS();
else if (_baudRate == 500000)
t_config = TWAI_TIMING_CONFIG_500KBITS();
// Começa aceitando tudo para depurar; depois afinamos a máscara
twai_filter_config_t f_config = {
.acceptance_code = (uint32_t(_nodeId) << 21),
.acceptance_mask = ~(0x7FFu << 21),
.single_filter = true
};
esp_err_t err = twai_driver_install(&g_config, &t_config, &f_config);
if (err != ESP_OK) {
PrintTela("[CAN] Erro ao instalar driver: ", false); PrintTela(String(err));
return;
}
err = twai_start();
if (err != ESP_OK) {
PrintTela("[CAN] Erro ao iniciar TWAI: ", false); PrintTela(String(err));
return;
}
// Habilita alerts úteis
uint32_t alert_mask = 0;
alert_mask |= TWAI_ALERT_BUS_OFF;
alert_mask |= TWAI_ALERT_RX_QUEUE_FULL;
alert_mask |= TWAI_ALERT_TX_FAILED;
#if defined(TWAI_ALERT_PERIPH_ERR)
alert_mask |= TWAI_ALERT_PERIPH_ERR;
#elif defined(TWAI_ALERT_PERIPH_RESET)
alert_mask |= TWAI_ALERT_PERIPH_RESET;
#endif
#if defined(TWAI_ALERT_RECOVERY_COMPLETE)
alert_mask |= TWAI_ALERT_RECOVERY_COMPLETE;
#elif defined(TWAI_ALERT_RECOVERY_IN_PROGRESS)
alert_mask |= TWAI_ALERT_RECOVERY_IN_PROGRESS;
#endif
twai_reconfigure_alerts(alert_mask, nullptr);
// --- Filas da nossa arquitetura ---
callbacksQ = xQueueCreate(64, sizeof(twai_message_t)); // jobs de RX -> callback
keysQ = xQueueCreate(64, sizeof(uint16_t)); // chaves coalescentes para TX
if (!callbacksQ || !keysQ) { PrintTela("[CAN] Falha ao criar filas"); }
// --- Tasks ---
xTaskCreatePinnedToCore(CanTaskRxWrapper, "CanTaskRx", 4096, this, 6, &CanTaskRxHandle, APP_CPU_NUM);
xTaskCreatePinnedToCore(CallbackWorkerWrapper,"CallbackWorker",6144,this,5,&CallbackWorkerHandle,APP_CPU_NUM);
xTaskCreatePinnedToCore(CanTaskTxWrapper, "CanTaskTx", 4096, this, 4, &CanTaskTxHandle, APP_CPU_NUM);
xTaskCreatePinnedToCore(HealthTaskWrapper, "CanHealth", 4096, this, 3, &HealthTaskHandle, APP_CPU_NUM);
// Watchdog por task (vamos resetar dentro dos loops)
esp_task_wdt_init(5, true);
esp_task_wdt_add(CanTaskRxHandle);
esp_task_wdt_add(CallbackWorkerHandle);
esp_task_wdt_add(CanTaskTxHandle);
esp_task_wdt_add(HealthTaskHandle);
PrintTela("[CAN] TWAI iniciado com sucesso");
}
// --- Filas e tasks ---
QueueHandle_t callbacksQ = nullptr; // fila para o callback worker
QueueHandle_t keysQ = nullptr; // fila de chaves para TX coalescente
// (Opcional) Se quiser manter uma RX intermediária: QueueHandle_t canQueueRx = nullptr;
TaskHandle_t CanTaskRxHandle = NULL;
TaskHandle_t CallbackWorkerHandle = NULL;
TaskHandle_t CanTaskTxHandle = NULL;
TaskHandle_t HealthTaskHandle = NULL;
// --- Métricas/health ---
volatile uint64_t last_rx_ts = 0; // us
volatile uint64_t last_tx_ts = 0; // us
volatile uint32_t rx_drops = 0;
volatile uint32_t tx_retries = 0;
// --- TX coalescing (tabela simples) ---
static const int MAX_KEYS = 64; // ajuste conforme nº (pos,addr)
struct Slot {
uint16_t key = 0;
bool used = false;
bool pending = false; // 🔧 NOVO: chave já enfileirada para envio
twai_message_t msg;
uint32_t gen = 0;
};
Slot latestByKey[MAX_KEYS];
// Helpers de chave (pos,addr -> 16 bits)
static inline uint16_t make_key(const twai_message_t& m) {
return (m.data_length_code >= 2) ? ( (uint16_t(m.data[0])<<8) | uint16_t(m.data[1]) ) : 0xFFFF;
}
bool put_latest(uint16_t key, const twai_message_t& m) {
int idx = key % MAX_KEYS;
for (int i=0; i<MAX_KEYS; ++i, idx=(idx+1)%MAX_KEYS) {
if (!latestByKey[idx].used || latestByKey[idx].key == key) {
latestByKey[idx].used = true;
latestByKey[idx].key = key;
latestByKey[idx].msg = m;
latestByKey[idx].gen++;
return true;
}
}
return false;
}
bool get_latest(uint16_t key, twai_message_t* out) {
int idx = key % MAX_KEYS;
for (int i=0; i<MAX_KEYS; ++i, idx=(idx+1)%MAX_KEYS) {
if (latestByKey[idx].used && latestByKey[idx].key == key) { *out = latestByKey[idx].msg; return true; }
if (!latestByKey[idx].used) break;
}
return false;
}
static void CanTaskRxWrapper(void *pvParameters) {
static_cast<CanService*>(pvParameters)->CanTaskRx(pvParameters);
}
void CanTaskRx(void* pvParameters) {
twai_message_t message;
for (;;) {
// WDT
esp_task_wdt_reset();
if (twai_receive(&message, pdMS_TO_TICKS(50)) == ESP_OK) {
// ignore frames que não vamos tratar
if (message.data_length_code == 0 || message.extd || message.rtr) {
continue;
}
// marca vida do barramento SEMPRE que recebeu algo válido
last_rx_ts = esp_timer_get_time();
if (DebugMode) {
PrintTela("[CAN RX] ID: 0x" + String(message.identifier, HEX) + " Len: " + String(message.data_length_code));
}
// tenta enfileirar pro worker
if (callbacksQ && xQueueSend(callbacksQ, &message, 0) != pdTRUE) {
if (DebugMode) {
PrintTela("Erro ao adicionar mensagem recebida na fila: " + String(rx_drops));
}
rx_drops++;
// (opcional) política "drop oldest": remove 1 e tenta de novo
twai_message_t dump;
if (xQueueReceive(callbacksQ, &dump, 0) == pdTRUE) {
xQueueSend(callbacksQ, &message, 0);
}
vTaskDelay(pdMS_TO_TICKS(1)); // dá uma respirada
}
}
vTaskDelay(1);
}
}
static void CallbackWorkerWrapper(void *pvParameters) {
static_cast<CanService*>(pvParameters)->CallbackWorker(pvParameters);
}
void CallbackWorker(void* pvParameters) {
twai_message_t msg;
for (;;) {
// WDT
esp_task_wdt_reset();
if (xQueueReceive(callbacksQ, &msg, portMAX_DELAY) == pdTRUE) {
if (_callback && msg.data_length_code > 0) {
CanMessagePosicaoDados posicao = (CanMessagePosicaoDados)msg.data[0];
_callback(msg.data_length_code, msg.identifier, posicao, &msg.data[1], msg.data_length_code - 1);
}
}
// yield leve
taskYIELD();
}
}
void PublicarTx(const twai_message_t& msg) {
// Sanidade básica
if (msg.data_length_code < 2 || msg.data_length_code > 8) return;
if (msg.extd || msg.rtr) return; // trabalhamos com standard data frames
uint16_t key = make_key(msg);
if (key == 0xFFFF) return;
// Atualiza a versão mais nova na tabela
int idx = key % MAX_KEYS;
bool stored = false;
for (int i=0; i<MAX_KEYS; ++i, idx=(idx+1)%MAX_KEYS) {
if (!latestByKey[idx].used || latestByKey[idx].key == key) {
latestByKey[idx].used = true;
latestByKey[idx].key = key;
latestByKey[idx].msg = msg;
latestByKey[idx].gen++;
stored = true;
// Se ainda não está pendente, enfileira a chave
if (!latestByKey[idx].pending && keysQ) {
if (xQueueSend(keysQ, &key, 0) == pdTRUE) {
latestByKey[idx].pending = true;
} else {
// fila cheia: sem pânico; mantemos a versão mais nova no slot
}
}
break;
}
}
if (!stored) {
// tabela cheia (raríssimo se MAX_KEYS está adequado) — poderia logar/contar.
}
}
// --- Wrapper padrão ---
static void CanTaskTxWrapper(void *pvParameters) {
static_cast<CanService*>(pvParameters)->CanTaskTx(pvParameters);
}
// --- Task de TX com retry/backoff e limpeza do "pending" ---
void CanTaskTx(void* pvParameters) {
uint16_t key;
for (;;) {
// WDT
esp_task_wdt_reset();
if (xQueueReceive(keysQ, &key, portMAX_DELAY) == pdTRUE) {
// busca a msg mais nova dessa chave
twai_message_t m{};
int idx = key % MAX_KEYS;
int foundIdx = -1;
for (int i=0; i<MAX_KEYS; ++i, idx=(idx+1)%MAX_KEYS) {
if (latestByKey[idx].used && latestByKey[idx].key == key) {
m = latestByKey[idx].msg;
foundIdx = idx;
break;
}
if (!latestByKey[idx].used) break;
}
if (foundIdx < 0) {
// chave desapareceu; nada a fazer
continue;
}
// Garantias do frame (caso quem chamou não tenha setado)
m.extd = 0; // standard frame
m.rtr = 0; // data frame
bool ok = false;
for (int tent=0; tent<3; ++tent) {
if (enviarDadosCan(m, pdMS_TO_TICKS(50))) {
last_tx_ts = esp_timer_get_time();
ok = true;
break;
}
tx_retries++;
vTaskDelay(pdMS_TO_TICKS(5));
}
// Limpa o "pending" SEMPRE; se falhou, a app pode chamar PublicarTx de novo
latestByKey[foundIdx].pending = false;
if (!ok) {
// Re-enfileira a chave só se ainda houver interesse (a app pode ter publicado algo novo)
// Re-check: se a slot ainda está usada e não está pendente, refile
if (latestByKey[foundIdx].used && !latestByKey[foundIdx].pending) {
if (xQueueSend(keysQ, &key, 0) == pdTRUE) {
latestByKey[foundIdx].pending = true;
}
}
}
}
taskYIELD();
}
}
// --- Versão de enviarDadosCan com timeout custom ---
bool enviarDadosCan(const twai_message_t& msg, TickType_t timeoutTicks) {
// Aqui você pode normalizar o ID/dados se precisar
// Ex.: msg.identifier já deve estar correto para o PC
esp_err_t err = twai_transmit(&msg, timeoutTicks);
if (err != ESP_OK) {
PrintTela("[CAN TX] Falha transmit (" + String((int)err) + ")");
return false;
}
return true;
}
static void HealthTaskWrapper(void *pvParameters) {
static_cast<CanService*>(pvParameters)->HealthTask(pvParameters);
}
bool reinit_twai_unsafe_() {
// ⚠️ Use os mesmos pinos/bitrate do begin(); ajuste se necessário
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(GPIO_NUM_5, GPIO_NUM_4, TWAI_MODE_NORMAL);
g_config.tx_queue_len = 32;
g_config.rx_queue_len = 32;
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) return false;
if (twai_start() != ESP_OK) { twai_driver_uninstall(); return false; }
uint32_t alert_mask = 0;
alert_mask |= TWAI_ALERT_BUS_OFF;
alert_mask |= TWAI_ALERT_RX_QUEUE_FULL;
alert_mask |= TWAI_ALERT_TX_FAILED;
#if defined(TWAI_ALERT_PERIPH_ERR)
alert_mask |= TWAI_ALERT_PERIPH_ERR;
#elif defined(TWAI_ALERT_PERIPH_RESET)
alert_mask |= TWAI_ALERT_PERIPH_RESET;
#endif
#if defined(TWAI_ALERT_RECOVERY_COMPLETE)
alert_mask |= TWAI_ALERT_RECOVERY_COMPLETE;
#elif defined(TWAI_ALERT_RECOVERY_IN_PROGRESS)
alert_mask |= TWAI_ALERT_RECOVERY_IN_PROGRESS;
#endif
twai_reconfigure_alerts(alert_mask, nullptr);
return true;
}
void HealthTask(void* pvParameters) {
const uint32_t RX_TIMEOUT_MS = 3000; // ajuste conforme seu tráfego esperado
const uint32_t TX_STALL_MS = 2000; // quanto tempo uma chave pode ficar sem sair
const uint8_t MAX_REC_FAILS = 3; // depois disso, reboot
static uint8_t consecutive_rec_fails = 0;
for (;;) {
esp_task_wdt_reset();
uint64_t now = esp_timer_get_time();
// ---- Alerts do driver ----
uint32_t alerts = 0;
if (twai_read_alerts(&alerts, 0) == ESP_OK && alerts) {
if (alerts & TWAI_ALERT_BUS_OFF) {
PrintTela("[CAN ALERT] BUS OFF → iniciando recovery...");
if (twai_initiate_recovery() != ESP_OK) {
PrintTela("[CAN ALERT] Falha ao iniciar recovery; reiniciando driver...");
twai_stop(); twai_driver_uninstall();
if (!reinit_twai_unsafe_()) consecutive_rec_fails++;
else consecutive_rec_fails = 0;
}
}
#if defined(TWAI_ALERT_RECOVERY_COMPLETE)
if (alerts & TWAI_ALERT_RECOVERY_COMPLETE) {
PrintTela("[CAN ALERT] Recovery completo.");
consecutive_rec_fails = 0;
}
#elif defined(TWAI_ALERT_RECOVERY_IN_PROGRESS)
// Algumas versões só têm "em progresso"; trate como sinal de vida do recovery
if (alerts & TWAI_ALERT_RECOVERY_IN_PROGRESS) {
PrintTela("[CAN ALERT] Recovery em progresso (OK).");
// opcional: zere contador se quiser considerar como sinal saudável
consecutive_rec_fails = 0;
}
#endif
if (alerts & TWAI_ALERT_RX_QUEUE_FULL) {
PrintTela("[CAN ALERT] RX interno do driver CHEIO.");
}
if (alerts & TWAI_ALERT_TX_FAILED) {
PrintTela("[CAN ALERT] TX FAILED.");
}
#if defined(TWAI_ALERT_PERIPH_ERR)
if (alerts & TWAI_ALERT_PERIPH_ERR) {
PrintTela("[CAN ALERT] ERRO no periférico CAN. Reiniciando driver...");
twai_stop(); twai_driver_uninstall();
if (!reinit_twai_unsafe_()) consecutive_rec_fails++;
else consecutive_rec_fails = 0;
}
#elif defined(TWAI_ALERT_PERIPH_RESET)
if (alerts & TWAI_ALERT_PERIPH_RESET) {
PrintTela("[CAN ALERT] RESET do periférico CAN detectado. Reconfigurando...");
twai_stop(); twai_driver_uninstall();
if (!reinit_twai_unsafe_()) consecutive_rec_fails++;
else consecutive_rec_fails = 0;
}
#endif
}
// ---- Liveness de RX ----
if (last_rx_ts && ((now - last_rx_ts) / 1000ULL) > RX_TIMEOUT_MS) {
PrintTela("[CAN HLTH] Sem RX há " + String((now - last_rx_ts)/1000ULL) + " ms → reiniciando driver...");
twai_stop(); twai_driver_uninstall();
if (!reinit_twai_unsafe_()) consecutive_rec_fails++;
else consecutive_rec_fails = 0;
last_rx_ts = esp_timer_get_time(); // evita loop de reinicialização
}
// ---- Stall de TX (chaves pendentes) ----
// Se houver muitas chaves esperando por muito tempo, force drenagem
UBaseType_t pendentes = keysQ ? uxQueueMessagesWaiting(keysQ) : 0;
if (pendentes > 0 && last_tx_ts && ((now - last_tx_ts) / 1000ULL) > TX_STALL_MS) {
PrintTela("[CAN HLTH] TX aparentemente parado (" + String(pendentes) + " chaves pendentes).");
// Estratégia: deixa o CanTaskTx re-enfileirar; se persistir, reinicia driver
static uint8_t tx_stall_count = 0;
tx_stall_count++;
if (tx_stall_count >= 3) {
PrintTela("[CAN HLTH] TX stall persistente → reiniciando driver...");
twai_stop(); twai_driver_uninstall();
if (!reinit_twai_unsafe_()) consecutive_rec_fails++;
else { consecutive_rec_fails = 0; tx_stall_count = 0; }
last_tx_ts = esp_timer_get_time();
}
}
// ---- Escalonamento final: reboot do chip se não recuperar ----
if (consecutive_rec_fails >= MAX_REC_FAILS) {
PrintTela("[CAN HLTH] Falhas consecutivas ao reiniciar TWAI. Reiniciando ESP32...");
vTaskDelay(pdMS_TO_TICKS(100));
esp_restart();
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void setReceiveCallback(OnReceiveCallback cb) {
_callback = cb;
}
bool adicionarMensagemFila(const std::vector<uint8_t>& payload) {
// 1) Sanidade
if (payload.empty() || payload.size() > 8) {
PrintTela("[CAN] Payload inválido (0 ou >8 bytes).");
return false;
}
if (payload.size() < 2) { // precisa de pos (byte0) e addr (byte1) para formar a chave
PrintTela("[CAN] Payload precisa ter pelo menos pos e addr (2 bytes).");
return false;
}
// 2) Monta frame
twai_message_t msg{};
// ⚠️ ID: use o ID que o PC espera para respostas desse nó (ajuste se necessário)
msg.identifier = _nodeId; // ou outro mapeamento do seu protocolo
msg.extd = 0; // standard frame
msg.rtr = 0; // data frame
msg.ss = 0;
msg.data_length_code = payload.size();
for (uint8_t i = 0; i < payload.size(); ++i) {
msg.data[i] = payload[i];
}
// 3) Publica via coalescência (substitui a antiga fila TX)
PublicarTx(msg);
return true;
}
F_Code FuncaoPorPosicao(CanMessagePosicaoDados posicao) {
F_Code funcao = F_Code::Nda;
int pos = static_cast<int>(posicao);
if (pos >= 0 && pos <= 50) {
funcao = F_Code::ReqTx;
}
else if (pos > 50 && pos <= 100) {
funcao = F_Code::CfgTx;
}
else if (pos > 100 && pos <= 150) {
funcao = F_Code::CmdTx;
}
//PrintTela("Funcao parseada para posicao: " + String(pos) + ", funcao: " + String(funcao));
return funcao;
}
std::vector<uint8_t> MontarFrameReqStatusMod(T_Code D_Code, bool Conectado, int Versao) {
std::vector<uint8_t> data;
data.push_back(static_cast<uint8_t>(CanMessagePosicaoDados::Status));
data.push_back(ID_Num_sMOD);
data.push_back(static_cast<uint8_t>(D_Code));
data.push_back(Conectado ? 1 : 0);
data.push_back(static_cast<uint8_t>(Versao));
return data;
}
std::vector<uint8_t> MontarFrameReqDadosFim(int latencia) {
std::vector<uint8_t> data;
data.push_back(static_cast<uint8_t>(CanMessagePosicaoDados::DadosAll));
data.push_back(ID_Num_sTOD);
data.push_back(latencia >> 8); data.push_back(latencia & 0xFF);
return data;
}
private:
uint32_t _baudRate = 250000;
uint8_t _nodeId = 0;
OnReceiveCallback _callback = nullptr;
bool DebugMode = false;
};
#endif // CANSERVICE_H