ajustado mapsync e parada ao detectar fim dos pontos no MPC

This commit is contained in:
Diego Freitas 2026-09-18 09:02:10 -03:00
parent 82d19dc003
commit 933135d00a
9 changed files with 786 additions and 95 deletions

1
.gitignore vendored
View File

@ -97,3 +97,4 @@ Python/OAK/datasets/oak-fcc-3/audit/
/Python/OAK/datasets/oak-fcc-3/benchmarks_async /Python/OAK/datasets/oak-fcc-3/benchmarks_async
/Python/OAK/datasets/oak-d/benchmark_corridor /Python/OAK/datasets/oak-d/benchmark_corridor
/AgroBase/livox_visual_debugger/x64/Debug /AgroBase/livox_visual_debugger/x64/Debug
/AgroBase/AgroBase/.vs/AgroBase.csproj.dtbcache.json

File diff suppressed because one or more lines are too long

View File

@ -1414,7 +1414,21 @@ namespace AgroBase.Models
op.IniciarTimerLogsOperacao(); op.IniciarTimerLogsOperacao();
RedisService.Publish(CmdKey.ManagerWorkerRx, JsonConvert.SerializeObject(new { cmd = ManagerWorkerCommandType.IniciarMPC })); // Barreira crítica: a operação só pode seguir para a fase de
// execução depois que o Manager/Python confirmar exatamente a
// trajetória completa projetada neste OperacaoModel. O método
// faz retry enquanto esta operação continuar sendo a atual.
bool mapaConfirmado = await HealthWorkerService
.SincronizarMapaCriticoAsync(op)
.ConfigureAwait(false);
if (!mapaConfirmado)
{
Variaveis.MostrarLog(
"[OperacaoModel.IniciarOperacao] Mapa MPC não confirmado. Operação permanece bloqueada."
);
return;
}
await op.RealizarCalibragemInicialAsync(true); await op.RealizarCalibragemInicialAsync(true);
} }
@ -1533,7 +1547,21 @@ namespace AgroBase.Models
op.Trajetoria?.ProjetarTrajetoriaFixa(); op.Trajetoria?.ProjetarTrajetoriaFixa();
op.Trajetoria.VerificacaoInicialMeioRuaConcluida = false; op.Trajetoria.VerificacaoInicialMeioRuaConcluida = false;
RedisService.Publish(CmdKey.ManagerWorkerRx, JsonConvert.SerializeObject(new { cmd = ManagerWorkerCommandType.IniciarMPC }));
// Simulação usa a mesma barreira transacional do campo. Isso é
// importante porque foi justamente no simulador que uma revisão
// parcial (um único corredor) conseguiu iniciar o MPC.
bool mapaConfirmado = await HealthWorkerService
.SincronizarMapaCriticoAsync(op)
.ConfigureAwait(false);
if (!mapaConfirmado)
{
Variaveis.MostrarLog(
"[OperacaoModel.IniciarSimulacao] Mapa MPC não confirmado. Simulação permanece bloqueada."
);
return;
}
} }
public void ReiniciarSimulacao() public void ReiniciarSimulacao()

View File

@ -309,10 +309,24 @@ namespace AgroBase.Services.Operadores
var pControle = _Parametros.Controle; var pControle = _Parametros.Controle;
var _Controle = op.Controle; var _Controle = op.Controle;
bool exigeConfirmacaoMapaMpc =
(op.Parametros?.ControleAutomatico ?? false) &&
pControle != null &&
pControle.DirTipoMovimento == TiposControladorDirecional.MPC &&
(op.Trajetoria?._TrajetoriaFixa?.Any() ?? false);
// Sempre que esta chamada solicita sincronizacao de um mapa MPC,
// a operacao fica estruturalmente NAO configurada ate o ACK exato
// da mesma revisao voltar do Manager/Python. Isto elimina a janela
// em que uma trajetoria nova ja foi publicada, mas o MPC ainda
// executa a revisao anterior.
bool configuradoPublicado =
configurado && !(sincronizarMapa && exigeConfirmacaoMapaMpc);
RedisService.AtualizarCampos( RedisService.AtualizarCampos(
CtxKey.DadosOperacao, CtxKey.DadosOperacao,
("id", op.ID ?? ""), ("id", op.ID ?? ""),
("configurado", configurado), ("configurado", configuradoPublicado),
("modo", op.Parametros?.Modo ?? ModoOperacao.NaoDefinido), ("modo", op.Parametros?.Modo ?? ModoOperacao.NaoDefinido),
("tempo_aguardar_inicio_operacao", op.TempoIniciarOperacao), ("tempo_aguardar_inicio_operacao", op.TempoIniciarOperacao),
("tempo_aguardar_retomada", op.TempoAguardarRetomada), ("tempo_aguardar_retomada", op.TempoAguardarRetomada),
@ -414,7 +428,19 @@ namespace AgroBase.Services.Operadores
if (!sucesso) if (!sucesso)
{ {
Variaveis.MostrarLog( Variaveis.MostrarLog(
"[MAPSYNC] Sincronização não crítica não foi confirmada." "[MAPSYNC] Sincronização não crítica não foi confirmada. Operação permanece bloqueada."
);
}
else if (
configurado &&
ReferenceEquals(op, Variaveis.OperacaoEmAndamento)
)
{
// Só publica configurado=true DEPOIS que o mapa foi
// confirmado pelo Manager/MPC.
RedisService.AtualizarCampos(
CtxKey.DadosOperacao,
("configurado", true)
); );
} }
} }
@ -438,13 +464,98 @@ namespace AgroBase.Services.Operadores
} }
private static readonly SemaphoreSlim _mapaSync = new SemaphoreSlim(1, 1); private static readonly SemaphoreSlim _mapaSync = new SemaphoreSlim(1, 1);
private static long _mapaRevisionSeq = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
private static long ProximaRevisaoMapa()
{
return Interlocked.Increment(ref _mapaRevisionSeq);
}
private static string ObterIdOperacaoMapa(OperacaoModel op)
{
if (op == null)
return "";
if (!string.IsNullOrWhiteSpace(op.ID))
return op.ID;
long ticks = op.Sensoriamento?.Operacao?.DataInicio.Ticks ?? 0L;
return $"local-{ticks}-{op.GetHashCode()}";
}
public static async Task<bool> SincronizarMapaCriticoAsync(OperacaoModel op) public static async Task<bool> SincronizarMapaCriticoAsync(OperacaoModel op)
{ {
return await SolicitarAtualizacaoMapaAsync( int ciclo = 0;
// Intertravamento publicado ANTES de esperar o gate. Mesmo que uma
// sincronizacao antiga termine nesse intervalo, ela nao pode liberar
// a operacao enquanto esta barreira estiver ativa.
RedisService.AtualizarCampos(
CtxKey.DadosOperacao,
("mapa_barreira_critica", true),
("mapa_confirmado", false),
("configurado", false)
);
/*
* Barreira critica de partida:
* enquanto esta mesma operacao existir, nao desistimos de sincronizar.
* A operacao permanece configurado=false ate o ACK exato voltar.
*/
while (ReferenceEquals(op, Variaveis.OperacaoEmAndamento))
{
ciclo++;
// Publica no Redis o snapshot mais recente da trajetoria, sem
// disparar outra sincronizacao em paralelo e sem liberar operacao.
AtualizarDadosOperacao(
forcar: true,
sincronizarMapa: false,
configurado: false
);
bool sucesso = await SolicitarAtualizacaoMapaAsync(
op, op,
aguardarGate: true aguardarGate: true
).ConfigureAwait(false); ).ConfigureAwait(false);
if (sucesso)
{
// Somente agora a configuracao pode ser considerada pronta.
AtualizarDadosOperacao(
forcar: true,
sincronizarMapa: false,
configurado: true
);
// A barreira e a ultima coisa a cair. Nesse instante a
// revisao, a quantidade e o ACK do Manager ja foram validados.
RedisService.AtualizarCampos(
CtxKey.DadosOperacao,
("mapa_barreira_critica", false)
);
Variaveis.MostrarLog(
$"[MAPSYNC/CRITICO] Mapa confirmado. ciclo={ciclo}. Operacao liberavel."
);
return true;
}
if (!ReferenceEquals(op, Variaveis.OperacaoEmAndamento))
return false;
int atrasoMs = Math.Min(2000, 250 + ciclo * 250);
Variaveis.MostrarLog(
$"[MAPSYNC/CRITICO] Mapa ainda nao confirmado. " +
$"retry em {atrasoMs}ms | ciclo={ciclo}."
);
await Task.Delay(atrasoMs).ConfigureAwait(false);
}
return false;
} }
private static async Task<bool> SolicitarAtualizacaoMapaAsync(OperacaoModel op, bool aguardarGate = false) private static async Task<bool> SolicitarAtualizacaoMapaAsync(OperacaoModel op, bool aguardarGate = false)
@ -505,39 +616,81 @@ namespace AgroBase.Services.Operadores
return false; return false;
} }
var pontosSnapshot = op.Trajetoria?._TrajetoriaFixa?.ToList();
int quantidadeEsperada = pontosSnapshot?.Count ?? 0;
if (quantidadeEsperada <= 0)
{
Variaveis.MostrarLog(
"[MAPSYNC] Snapshot vazio. Operacao continua bloqueada."
);
return false;
}
int primeiroIdxEsperado = pontosSnapshot.First().idxPonto;
int ultimoIdxEsperado = pontosSnapshot.Last().idxPonto;
/* /*
* A REVISÃO identifica o MAPA. * A REVISAO identifica o snapshot logico do mapa.
* * commandId identifica apenas cada tentativa de transporte.
* Ela permanece a mesma durante todos os retries.
*/ */
long mapRevision = long mapRevision = ProximaRevisaoMapa();
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); string operationId = ObterIdOperacaoMapa(op);
string operationId = op.ID ?? "";
// Invalida qualquer confirmacao anterior ANTES de publicar a
// nova tentativa. Assim uma revisao velha jamais libera a atual.
RedisService.AtualizarCampos( RedisService.AtualizarCampos(
CtxKey.DadosOperacao, CtxKey.DadosOperacao,
("configurado", false),
("mapa_revisao_solicitada", mapRevision), ("mapa_revisao_solicitada", mapRevision),
("mapa_operacao_id", operationId), ("mapa_operacao_id", operationId),
("mapa_quantidade_pontos_esperada", quantidadeEsperada),
("mapa_primeiro_idx_esperado", primeiroIdxEsperado),
("mapa_ultimo_idx_esperado", ultimoIdxEsperado),
("mapa_revisao_processada", 0L),
("mapa_quantidade_pontos_processada", 0),
("mapa_revisao_mpc_aplicada", 0L),
("mapa_quantidade_pontos_mpc_aplicada", 0),
("mapa_command_id_mpc_aplicado", ""),
("mapa_status_mpc", "pendente"),
("mapa_confirmado", false),
("mapa_status", "pendente") ("mapa_status", "pendente")
); );
for (int tentativa = 1; tentativa <= 3; tentativa++) for (int tentativa = 1; tentativa <= 3; tentativa++)
{ {
if (!ReferenceEquals( if (!ReferenceEquals(op, Variaveis.OperacaoEmAndamento))
op, return false;
Variaveis.OperacaoEmAndamento))
var atual = op.Trajetoria?._TrajetoriaFixa;
int qtdAtual = atual?.Count ?? 0;
int primeiroAtual = qtdAtual > 0 ? atual.First().idxPonto : int.MinValue;
int ultimoAtual = qtdAtual > 0 ? atual.Last().idxPonto : int.MinValue;
// Se a trajetoria mudou enquanto esta transacao estava viva,
// nao tentamos "encaixar" a revisao antiga no mapa novo.
if (
qtdAtual != quantidadeEsperada ||
primeiroAtual != primeiroIdxEsperado ||
ultimoAtual != ultimoIdxEsperado
)
{ {
Variaveis.MostrarLog(
$"[MAPSYNC] Trajetoria mudou durante sincronizacao | " +
$"esperado={quantidadeEsperada}({primeiroIdxEsperado}->{ultimoIdxEsperado}) | " +
$"atual={qtdAtual}({primeiroAtual}->{ultimoAtual}). Nova revisao sera criada."
);
RedisService.AtualizarCampos(
CtxKey.DadosOperacao,
("mapa_status", "alterado_durante_sync"),
("mapa_confirmado", false)
);
return false; return false;
} }
/* string commandId = Guid.NewGuid().ToString("N");
* commandId identifica a TENTATIVA de transporte.
*
* A revisão continua a mesma.
*/
string commandId =
Guid.NewGuid().ToString("N");
RedisService.AtualizarCampos( RedisService.AtualizarCampos(
CtxKey.DadosOperacao, CtxKey.DadosOperacao,
@ -548,27 +701,32 @@ namespace AgroBase.Services.Operadores
$"[MAPSYNC] Tentativa={tentativa} | " + $"[MAPSYNC] Tentativa={tentativa} | " +
$"cmd={commandId} | " + $"cmd={commandId} | " +
$"rev={mapRevision} | " + $"rev={mapRevision} | " +
$"pontos={op.Trajetoria._TrajetoriaFixa?.Count}" $"pontos={quantidadeEsperada} | " +
$"idx={primeiroIdxEsperado}->{ultimoIdxEsperado}"
); );
bool atualizado = bool atualizado = await AtualizarPontosMapa(
await AtualizarPontosMapa(
op, op,
commandId, commandId,
operationId, operationId,
mapRevision mapRevision,
quantidadeEsperada
).ConfigureAwait(false); ).ConfigureAwait(false);
if (atualizado) if (atualizado)
{ {
RedisService.AtualizarCampos( RedisService.AtualizarCampos(
CtxKey.DadosOperacao, CtxKey.DadosOperacao,
("mapa_status", "aplicado") ("mapa_status", "aplicado"),
("mapa_confirmado", true),
("mapa_quantidade_pontos_confirmada", quantidadeEsperada),
("mapa_revisao_confirmada", mapRevision)
); );
Variaveis.MostrarLog( Variaveis.MostrarLog(
$"[MAPSYNC] SUCESSO | " + $"[MAPSYNC] SUCESSO EXATO | " +
$"rev={mapRevision} | " + $"rev={mapRevision} | " +
$"pontos={quantidadeEsperada} | " +
$"tentativa={tentativa}" $"tentativa={tentativa}"
); );
@ -579,19 +737,18 @@ namespace AgroBase.Services.Operadores
$"[MAPSYNC] Falha/timeout | " + $"[MAPSYNC] Falha/timeout | " +
$"tentativa={tentativa} | " + $"tentativa={tentativa} | " +
$"cmd={commandId} | " + $"cmd={commandId} | " +
$"rev={mapRevision}" $"rev={mapRevision} | " +
$"pontos={quantidadeEsperada}"
); );
if (tentativa < 3) if (tentativa < 3)
{ await Task.Delay(250).ConfigureAwait(false);
await Task.Delay(250)
.ConfigureAwait(false);
}
} }
RedisService.AtualizarCampos( RedisService.AtualizarCampos(
CtxKey.DadosOperacao, CtxKey.DadosOperacao,
("mapa_status", "erro") ("mapa_status", "erro"),
("mapa_confirmado", false)
); );
return false; return false;
@ -603,7 +760,13 @@ namespace AgroBase.Services.Operadores
} }
} }
private static async Task<bool> AtualizarPontosMapa(OperacaoModel op, string commandId, string operationId, long mapRevision) private static async Task<bool> AtualizarPontosMapa(
OperacaoModel op,
string commandId,
string operationId,
long mapRevision,
int quantidadeEsperada
)
{ {
if ( if (
!(op?.Parametros?.ControleAutomatico ?? false) || !(op?.Parametros?.ControleAutomatico ?? false) ||
@ -618,7 +781,8 @@ namespace AgroBase.Services.Operadores
cmd = HealthWorkerCommandType.AtualizarPontosMapa, cmd = HealthWorkerCommandType.AtualizarPontosMapa,
command_id = commandId, command_id = commandId,
operation_id = operationId, operation_id = operationId,
map_revision = mapRevision map_revision = mapRevision,
expected_point_count = quantidadeEsperada
}; };
long assinantes = RedisService.Publish( long assinantes = RedisService.Publish(
@ -630,13 +794,9 @@ namespace AgroBase.Services.Operadores
$"[MAPSYNC/TX] cmd={commandId} | " + $"[MAPSYNC/TX] cmd={commandId} | " +
$"rev={mapRevision} | " + $"rev={mapRevision} | " +
$"subs={assinantes} | " + $"subs={assinantes} | " +
$"pontos={op.Trajetoria._TrajetoriaFixa.Count}" $"pontos={quantidadeEsperada}"
); );
/*
* Se ninguém ouviu o Publish, não existe motivo para
* esperar quatro segundos por um ACK impossível.
*/
if (assinantes <= 0) if (assinantes <= 0)
{ {
Variaveis.MostrarLog( Variaveis.MostrarLog(
@ -647,10 +807,6 @@ namespace AgroBase.Services.Operadores
return false; return false;
} }
/*
* O Python agora espera o ACK do Manager (~2,5 s).
* Portanto 3 s ficou apertado demais.
*/
bool sucesso = await AguardarMapaAplicadoAsync( bool sucesso = await AguardarMapaAplicadoAsync(
commandId, commandId,
mapRevision, mapRevision,
@ -659,15 +815,13 @@ namespace AgroBase.Services.Operadores
if (!sucesso) if (!sucesso)
{ {
string status = string status = RedisService.GetField(
RedisService.GetField(
CtxKey.DadosHealthWorker, CtxKey.DadosHealthWorker,
"mapa_status", "mapa_status",
"" ""
); );
string erro = string erro = RedisService.GetField(
RedisService.GetField(
CtxKey.DadosHealthWorker, CtxKey.DadosHealthWorker,
"mapa_erro", "mapa_erro",
"" ""
@ -680,9 +834,62 @@ namespace AgroBase.Services.Operadores
$"status={status} | " + $"status={status} | " +
$"erro={erro}" $"erro={erro}"
); );
return false;
} }
return sucesso; // Segunda confirmacao, diretamente no estado transacional usado
// pelo Manager. O ACK so vale se a MESMA revisao e a MESMA
// quantidade de pontos realmente chegaram ao MPC.
long revisaoProcessada = RedisService.GetField<long>(
CtxKey.DadosOperacao,
"mapa_revisao_processada",
0L
);
int qtdProcessada = RedisService.GetField<int>(
CtxKey.DadosOperacao,
"mapa_quantidade_pontos_processada",
0
);
long revisaoMpc = RedisService.GetField<long>(
CtxKey.DadosOperacao,
"mapa_revisao_mpc_aplicada",
0L
);
int qtdMpc = RedisService.GetField<int>(
CtxKey.DadosOperacao,
"mapa_quantidade_pontos_mpc_aplicada",
0
);
string statusMpc = RedisService.GetField(
CtxKey.DadosOperacao,
"mapa_status_mpc",
""
);
bool confirmacaoExata =
revisaoProcessada == mapRevision &&
qtdProcessada == quantidadeEsperada &&
revisaoMpc == mapRevision &&
qtdMpc == quantidadeEsperada &&
statusMpc == "aplicado";
if (!confirmacaoExata)
{
Variaveis.MostrarLog(
$"[MAPSYNC] ACK rejeitado por divergencia de snapshot | " +
$"revEsperada={mapRevision} | revProcessada={revisaoProcessada} | revMpc={revisaoMpc} | " +
$"qtdEsperada={quantidadeEsperada} | qtdProcessada={qtdProcessada} | qtdMpc={qtdMpc} | " +
$"statusMpc={statusMpc}"
);
return false;
}
return true;
} }
private static async Task<bool> AguardarMapaAplicadoAsync(string commandId, long revision, TimeSpan timeout) private static async Task<bool> AguardarMapaAplicadoAsync(string commandId, long revision, TimeSpan timeout)
@ -696,16 +903,12 @@ namespace AgroBase.Services.Operadores
string status = RedisService.GetField(CtxKey.DadosHealthWorker, "mapa_status", ""); string status = RedisService.GetField(CtxKey.DadosHealthWorker, "mapa_status", "");
if (ultimoCommandId == commandId && revisaoAplicada == revision && status == "aplicado") if (ultimoCommandId == commandId && revisaoAplicada == revision && status == "aplicado")
{
return true; return true;
}
if (ultimoCommandId == commandId && status == "erro") if (ultimoCommandId == commandId && status == "erro")
{
return false; return false;
}
await Task.Delay(100); await Task.Delay(100).ConfigureAwait(false);
} }
return false; return false;

View File

@ -95,6 +95,19 @@ def main():
"map_revision" "map_revision"
) )
expected_point_count = _p.get(
"expected_point_count"
)
if expected_point_count is not None:
expected_point_count = int(expected_point_count)
quantidade_recebida = len(mapa or [])
if quantidade_recebida != expected_point_count:
raise ValueError(
f"Snapshot de mapa divergente no Manager: "
f"esperado={expected_point_count}, recebido={quantidade_recebida}"
)
mostrar_log( mostrar_log(
f"[MAPSYNC/MANAGER] aplicando mapa | " f"[MAPSYNC/MANAGER] aplicando mapa | "
f"cmd={command_id} | " f"cmd={command_id} | "
@ -122,6 +135,7 @@ def main():
CtxKey.DadosOperacao, CtxKey.DadosOperacao,
mapa_command_id_mpc_aplicado=str(command_id), mapa_command_id_mpc_aplicado=str(command_id),
mapa_revisao_mpc_aplicada=map_revision, mapa_revisao_mpc_aplicada=map_revision,
mapa_quantidade_pontos_mpc_aplicada=len(mapa or []),
mapa_status_mpc="aplicado", mapa_status_mpc="aplicado",
mapa_erro_mpc="", mapa_erro_mpc="",
mapa_mpc_aplicado_em=time.time(), mapa_mpc_aplicado_em=time.time(),
@ -150,6 +164,7 @@ def main():
CtxKey.DadosOperacao, CtxKey.DadosOperacao,
mapa_command_id_mpc_aplicado=str(command_id), mapa_command_id_mpc_aplicado=str(command_id),
mapa_revisao_mpc_aplicada=map_revision, mapa_revisao_mpc_aplicada=map_revision,
mapa_quantidade_pontos_mpc_aplicada=0,
mapa_status_mpc="erro", mapa_status_mpc="erro",
mapa_erro_mpc=str(e), mapa_erro_mpc=str(e),
) )

View File

@ -250,6 +250,12 @@ def _normalizar_comando_mpc(comando: dict, contexto: dict):
comando.get("tipo", TipoMovimentoDirecional.RodasDianteiras.value) comando.get("tipo", TipoMovimentoDirecional.RodasDianteiras.value)
) )
parada_necessaria = _bool(comando.get("parada_necessaria", False))
if parada_necessaria:
# A direção deve ficar neutra no mesmo ciclo em que o MPC pede stop.
angulo = 0.0
tipo = TipoMovimentoDirecional.RodasDianteiras.value
debug_custo = comando.get("debug_custo", {}) debug_custo = comando.get("debug_custo", {})
if not isinstance(debug_custo, dict): if not isinstance(debug_custo, dict):
debug_custo = {} debug_custo = {}
@ -261,7 +267,7 @@ def _normalizar_comando_mpc(comando: dict, contexto: dict):
return { return {
"comando_definido": True, "comando_definido": True,
"enviar_comando": _bool(comando.get("enviar_comando", True)), "enviar_comando": _bool(comando.get("enviar_comando", True)),
"parada_necessaria": _bool(comando.get("parada_necessaria", False)), "parada_necessaria": parada_necessaria,
"erro": _bool(comando.get("erro", False)), "erro": _bool(comando.get("erro", False)),
"latencia": _float(comando.get("latencia", 0.0), 0.0), "latencia": _float(comando.get("latencia", 0.0), 0.0),
"angulo": round(angulo, 2), "angulo": round(angulo, 2),
@ -1083,6 +1089,25 @@ def _montar_comando_retorno(comando, latencia=-1.0):
comando["erro_lateral"] = _float(comando.get("erro_lateral", 0.0), 0.0) comando["erro_lateral"] = _float(comando.get("erro_lateral", 0.0), 0.0)
comando["erro_orientacao"] = _float(comando.get("erro_orientacao", 0.0), 0.0) comando["erro_orientacao"] = _float(comando.get("erro_orientacao", 0.0), 0.0)
# V17 SAFETY:
# "parada_necessaria" é um contrato forte. Nenhum chamador pode receber
# esse flag junto com um esterçamento residual e continuar aplicando-o.
if comando["parada_necessaria"]:
comando["enviar_comando"] = True
comando["angulo"] = 0.0
comando["tipo"] = TipoMovimentoDirecional.RodasDianteiras.value
comando["simulacao"] = []
debug_stop = comando.get("debug_custo", {})
if not isinstance(debug_stop, dict):
debug_stop = {}
debug_stop["parada_necessaria"] = {
"ativo": True,
"acao_direcional": "angulo_zero_rodas_dianteiras",
}
comando["debug_custo"] = debug_stop
if not isinstance(comando.get("simulacao", []), list): if not isinstance(comando.get("simulacao", []), list):
comando["simulacao"] = [] comando["simulacao"] = []

View File

@ -892,6 +892,11 @@ class ControladorMPC:
# Índices globais são opcionais para manter compatibilidade. Quando # Índices globais são opcionais para manter compatibilidade. Quando
# presentes, eliminam a ambiguidade entre o índice da trajetória C# # presentes, eliminam a ambiguidade entre o índice da trajetória C#
# e o índice local de uma lista dinâmica enviada ao MPC. # e o índice local de uma lista dinâmica enviada ao MPC.
self._idx_global_explicito = bool(self.pontos_info) and all(
isinstance(p, dict)
and ("idxPonto" in p or "IdxPonto" in p)
for p in self.pontos_info
)
self._idx_global_pontos = np.array([ self._idx_global_pontos = np.array([
_safe_int( _safe_int(
p.get("idxPonto", p.get("IdxPonto", i)), p.get("idxPonto", p.get("IdxPonto", i)),
@ -3616,7 +3621,13 @@ class ControladorMPC:
try: try:
if not self.pontos_info: if not self.pontos_info:
return None motivo = "MPC sem pontos carregados"
_log(f"[MPC/V17 SAFETY] HARD STOP | {motivo}")
return self._comando_parada_segura_mpc(
motivo=motivo,
latencia=0.0,
erro=True,
)
# -------------------- Estado atual (real) -------------------- # -------------------- Estado atual (real) --------------------
GPS = contexto.get("GPS", {}) GPS = contexto.get("GPS", {})
@ -3645,6 +3656,83 @@ class ControladorMPC:
idx_ponto_alvo = contexto.get("Carro", {}).get("IdxPontoAlvo", 0) idx_ponto_alvo = contexto.get("Carro", {}).get("IdxPontoAlvo", 0)
Nmax = 30 if status_carro == StatusCarroMapa.CaminhandoRua.value else 60 Nmax = 30 if status_carro == StatusCarroMapa.CaminhandoRua.value else 60
# ============================================================
# V17 SAFETY - o índice autoritativo do C# NÃO pode ultrapassar
# a fotografia de mapa carregada neste MPC.
#
# Esta barreira vem ANTES de:
# - sincronização de visitados;
# - construção do alvo;
# - Pure Pursuit Dubins;
# - árvore MPC.
#
# Assim, se o C# pedir um ponto que não existe localmente, o
# controlador nunca faz clamp silencioso para o último ponto e
# nunca transforma "fim de mapa local" em um comando de reta.
# ============================================================
idx_proximo_ponto_real = _safe_int(idx_proximo_ponto_real, 0)
qtd_pontos_mpc = len(self.pontos_info)
if idx_proximo_ponto_real < 0:
motivo = (
f"Índice autoritativo inválido: "
f"idx_csharp={idx_proximo_ponto_real} | "
f"pontos_mpc={qtd_pontos_mpc}"
)
_log(f"[MPC/V17 SAFETY] HARD STOP | {motivo}")
return self._comando_parada_segura_mpc(
motivo=motivo,
latencia=pos_latencia,
erro=True,
)
identidade_global = bool(
getattr(self, "_idx_global_explicito", False)
)
if identidade_global:
# Com identidade global explícita não usamos o tamanho do array
# para validar um índice ainda existente. Isso mantém o código
# correto mesmo se no futuro houver snapshots com ids globais
# não iniciados em zero.
if idx_proximo_ponto_real not in self._idx_global_para_local:
max_idx_global = (
int(np.max(self._idx_global_pontos))
if len(self._idx_global_pontos)
else -1
)
fim_normal_global = (
idx_proximo_ponto_real == max_idx_global + 1
)
motivo = (
f"Mapa MPC sem ponto futuro autoritativo: "
f"idx_csharp={idx_proximo_ponto_real} | "
f"idx_max_mpc={max_idx_global} | "
f"pontos_mpc={qtd_pontos_mpc} | "
f"status={_enum_name(status_carro, StatusCarroMapa)}"
)
_log(f"[MPC/V17 SAFETY] HARD STOP | {motivo}")
return self._comando_parada_segura_mpc(
motivo=motivo,
latencia=pos_latencia,
erro=not fim_normal_global,
)
elif idx_proximo_ponto_real >= qtd_pontos_mpc:
# Contrato legado: índice local == posição no array.
excedeu = idx_proximo_ponto_real > qtd_pontos_mpc
motivo = (
f"Mapa MPC sem ponto futuro autoritativo: "
f"idx_csharp={idx_proximo_ponto_real} | "
f"pontos_mpc={qtd_pontos_mpc} | "
f"status={_enum_name(status_carro, StatusCarroMapa)}"
)
_log(f"[MPC/V17 SAFETY] HARD STOP | {motivo}")
return self._comando_parada_segura_mpc(
motivo=motivo,
latencia=pos_latencia,
erro=bool(excedeu),
)
self.tempo_execucao_local = contexto.get("Carro", {}).get("TempoEntreComandos", 0.5) self.tempo_execucao_local = contexto.get("Carro", {}).get("TempoEntreComandos", 0.5)
#self.passos_horizonte_local = max(1, math.floor(1 / self.tempo_execucao_local)) #self.passos_horizonte_local = max(1, math.floor(1 / self.tempo_execucao_local))
@ -4123,25 +4211,40 @@ class ControladorMPC:
mostrar_log(f"🚨 Tempo maximo de execucao excedido: {delta}/{limite}, processo: {processo}") mostrar_log(f"🚨 Tempo maximo de execucao excedido: {delta}/{limite}, processo: {processo}")
return hot_stop return hot_stop
def _comando_fallback_hot_stop(self, comando_anterior, motivos, latencia=0): def _comando_parada_segura_mpc(self, motivo, latencia=0.0, erro=False):
comando_anterior = _as_dict(comando_anterior) """Comando fail-stop canônico do MPC.
tipo_seguro = _movimento_from_value(
comando_anterior.get("tipo", TipoMovimentoDirecional.RodasDianteiras.value) Uma parada necessária deve SEMPRE zerar o direcional na origem.
).value O ProcessadorEmAndamento converte este contrato também em MOV=0.
"""
motivos = _safe_motivos(motivo)
return { return {
"enviar_comando": True, "enviar_comando": True,
"parada_necessaria": True, "parada_necessaria": True,
"erro": True, "erro": bool(erro),
"latencia": _safe_float(latencia, 0.0, min_value=0.0), "latencia": _safe_float(latencia, 0.0, min_value=0.0),
"angulo": _safe_float(comando_anterior.get("angulo", 0.0), 0.0), "angulo": 0.0,
"tipo": tipo_seguro, "tipo": TipoMovimentoDirecional.RodasDianteiras.value,
"simulacao": [], "simulacao": [],
"erro_lateral": 0.0, "erro_lateral": 0.0,
"erro_orientacao": 0.0, "erro_orientacao": 0.0,
"debug_custo": {}, "debug_custo": {
"candidatos_testados": 0, "safety_stop": {
"motivos": _safe_motivos(motivos), "ativo": True,
"origem": "MPC",
"motivo": " | ".join(map(str, motivos)),
} }
},
"candidatos_testados": 0,
"motivos": motivos,
}
def _comando_fallback_hot_stop(self, comando_anterior, motivos, latencia=0):
return self._comando_parada_segura_mpc(
motivo=motivos,
latencia=latencia,
erro=True,
)
def _gerar_angulos_candidatos_receding( def _gerar_angulos_candidatos_receding(
self, self,

View File

@ -256,8 +256,121 @@ class ProcessadorEmAndamento(ProcessadorBase):
erro_dir = bool( erro_dir = bool(
comando_dir.get("erro", False) comando_dir.get("erro", False)
) )
parada_dir = bool(
comando_dir.get("parada_necessaria", False)
)
if erro_dir: # ========================================================
# V17 SAFETY - parada necessária do direcional é GLOBAL.
#
# Antes desta versão o flag chegava corretamente do MPC,
# porém só `erro=True` zerava o MOV. Isso permitia o caso:
# MPC sem pontos -> parada_necessaria=True, erro=False
# DIR ia para 0°, mas MOV continuava com a velocidade
# calculada alguns microssegundos antes.
#
# Agora qualquer parada_necessaria força no MESMO envelope:
# MOV = 0
# freio = true
# DIR = 0°
# tipo = RodasDianteiras
# ========================================================
if parada_dir:
motivos_stop = (
comando_dir.get("motivos", []) or []
)
if isinstance(motivos_stop, str):
motivos_stop = [motivos_stop]
elif not isinstance(motivos_stop, list):
try:
motivos_stop = list(motivos_stop)
except Exception:
motivos_stop = [str(motivos_stop)]
if not motivos_stop:
motivos_stop = [
"Controle direcional solicitou parada necessária"
]
motivos.extend(motivos_stop)
self._logar_bloqueio(motivos_stop)
erro_geral = bool(erro_geral or erro_dir)
if movimento_automatico:
velocidade = 0.0
frear = True
atualizar_movimento = (
self._movimento_mudou(
velocidade,
frear
)
or self._venceu_intervalo_movimento(agora)
)
angulo = 0.0
tipo_movimento = (
TipoMovimentoDirecional
.RodasDianteiras
.value
)
simulacao = []
latencia = self._to_float(
comando_dir.get(
"latencia",
0.0
),
0.0
)
erro_lateral = self._to_float(
comando_dir.get(
"erro_lateral",
0.0
),
0.0
)
debug_custo = (
comando_dir.get(
"debug_custo",
{}
)
or {}
)
if not isinstance(debug_custo, dict):
debug_custo = {}
debug_custo["parada_global"] = {
"ativa": True,
"origem": "direcional.parada_necessaria",
"movimento_percentual": 0.0,
"frear": True,
"angulo": 0.0,
"tipo": int(
TipoMovimentoDirecional
.RodasDianteiras
.value
),
}
atualizar_direcional = (
self._direcional_mudou(
angulo,
tipo_movimento
)
or self._venceu_intervalo_direcional(agora)
or bool(
comando_dir.get(
"enviar_comando",
True
)
)
)
elif erro_dir:
erro_geral = True erro_geral = True
motivos.extend( motivos.extend(

View File

@ -433,6 +433,19 @@ class ContextoGlobalRedis:
modulos_opcionais_configurados=[], modulos_opcionais_configurados=[],
status_anterior_parado=StatusOperacao.Parado.value, status_anterior_parado=StatusOperacao.Parado.value,
status_iniciado_anterior=False, status_iniciado_anterior=False,
mapa_status="nao_sincronizado",
mapa_barreira_critica=False,
mapa_confirmado=False,
mapa_revisao_solicitada=0,
mapa_revisao_processada=0,
mapa_revisao_mpc_aplicada=0,
mapa_quantidade_pontos_esperada=0,
mapa_quantidade_pontos_processada=0,
mapa_quantidade_pontos_mpc_aplicada=0,
mapa_primeiro_idx_esperado=None,
mapa_ultimo_idx_esperado=None,
mapa_status_mpc="",
mapa_command_id_mpc_aplicado="",
) )
# ============================================================ # ============================================================
@ -614,6 +627,110 @@ class ContextoGlobalRedis:
detalhes_regras["trajetoria"] = trj_debug detalhes_regras["trajetoria"] = trj_debug
# ------------------------------------------------------------
# 4.1) Confirmação transacional do mapa no MPC
# ------------------------------------------------------------
# Para qualquer operação realmente controlada por MPC, não basta o
# mapa existir no Redis. A MESMA revisão precisa ter sido convertida
# pelo HealthWorker e aplicada pelo Manager com a MESMA quantidade
# de pontos. Este gate não é ignorado nem em debug_mode.
dados_dir = cls._dict(operacao.get("Dir", {}))
tipo_controle_dir = cls._int(
dados_dir.get("tipo_controle", TiposControladorDirecional.Manual.value),
TiposControladorDirecional.Manual.value,
)
dados_mpc = cls._dict(dados_dir.get("mpc", {}))
pontos_publicados = dados_mpc.get("pontos", []) or []
requer_confirmacao_mapa_mpc = bool(
tipo_controle_dir == TiposControladorDirecional.MPC.value
)
revisao_solicitada = cls._int(operacao.get("mapa_revisao_solicitada", 0), 0)
revisao_processada = cls._int(operacao.get("mapa_revisao_processada", 0), 0)
revisao_mpc = cls._int(operacao.get("mapa_revisao_mpc_aplicada", 0), 0)
qtd_esperada = cls._int(operacao.get("mapa_quantidade_pontos_esperada", 0), 0)
qtd_processada = cls._int(operacao.get("mapa_quantidade_pontos_processada", 0), 0)
qtd_mpc = cls._int(operacao.get("mapa_quantidade_pontos_mpc_aplicada", 0), 0)
qtd_publicada = len(pontos_publicados)
status_mapa = str(operacao.get("mapa_status", "") or "")
status_mpc = str(operacao.get("mapa_status_mpc", "") or "")
mapa_confirmado_flag = cls._bool(operacao.get("mapa_confirmado", False))
mapa_barreira_critica = cls._bool(operacao.get("mapa_barreira_critica", False))
primeiro_esperado = operacao.get("mapa_primeiro_idx_esperado", None)
ultimo_esperado = operacao.get("mapa_ultimo_idx_esperado", None)
primeiro_publicado = None
ultimo_publicado = None
try:
if pontos_publicados:
primeiro_publicado = cls._int(
cls._dict(pontos_publicados[0]).get("idxPonto", -1), -1
)
ultimo_publicado = cls._int(
cls._dict(pontos_publicados[-1]).get("idxPonto", -1), -1
)
except Exception:
primeiro_publicado = None
ultimo_publicado = None
indices_extremos_ok = True
if requer_confirmacao_mapa_mpc:
if primeiro_esperado is not None:
indices_extremos_ok = indices_extremos_ok and (
primeiro_publicado == cls._int(primeiro_esperado, -999999)
)
if ultimo_esperado is not None:
indices_extremos_ok = indices_extremos_ok and (
ultimo_publicado == cls._int(ultimo_esperado, -999999)
)
mapa_mpc_ok = (
not requer_confirmacao_mapa_mpc
or (
not mapa_barreira_critica
and mapa_confirmado_flag
and status_mapa == "aplicado"
and status_mpc == "aplicado"
and revisao_solicitada > 0
and revisao_solicitada == revisao_processada == revisao_mpc
and qtd_esperada > 0
and qtd_publicada == qtd_esperada
and qtd_processada == qtd_esperada
and qtd_mpc == qtd_esperada
and indices_extremos_ok
)
)
if not mapa_mpc_ok:
motivos.append(
"Mapa/MPC ainda não confirmado: "
f"rev solicitada/processada/mpc={revisao_solicitada}/{revisao_processada}/{revisao_mpc}; "
f"pontos publicados/esperados/processados/mpc={qtd_publicada}/{qtd_esperada}/{qtd_processada}/{qtd_mpc}; "
f"status={status_mapa}/{status_mpc}"
)
detalhes_regras["mapa_mpc"] = {
"requer_confirmacao": requer_confirmacao_mapa_mpc,
"ok": mapa_mpc_ok,
"confirmado": mapa_confirmado_flag,
"barreira_critica": mapa_barreira_critica,
"status_mapa": status_mapa,
"status_mpc": status_mpc,
"revisao_solicitada": revisao_solicitada,
"revisao_processada": revisao_processada,
"revisao_mpc": revisao_mpc,
"pontos_publicados": qtd_publicada,
"pontos_esperados": qtd_esperada,
"pontos_processados": qtd_processada,
"pontos_mpc": qtd_mpc,
"primeiro_idx_publicado": primeiro_publicado,
"ultimo_idx_publicado": ultimo_publicado,
"primeiro_idx_esperado": primeiro_esperado,
"ultimo_idx_esperado": ultimo_esperado,
"indices_extremos_ok": indices_extremos_ok,
}
# ------------------------------------------------------------ # ------------------------------------------------------------
# 5) Motivos runtime do controle # 5) Motivos runtime do controle
# ------------------------------------------------------------ # ------------------------------------------------------------
@ -719,7 +836,7 @@ class ContextoGlobalRedis:
# ------------------------------------------------------------ # ------------------------------------------------------------
estrutural_ok = bool(modulos_ok and parametros_ok) estrutural_ok = bool(modulos_ok and parametros_ok)
base_ok = bool(emergencia_ok and pausa_ok and trajetoria_ok) base_ok = bool(emergencia_ok and pausa_ok and trajetoria_ok and mapa_mpc_ok)
if debug_mode: if debug_mode:
# Debug ignora módulos/parâmetros, mas NÃO ignora emergência, pausa ou trajetória. # Debug ignora módulos/parâmetros, mas NÃO ignora emergência, pausa ou trajetória.
@ -1399,6 +1516,7 @@ class ContextoGlobalRedis:
mapa=None, mapa=None,
map_revision=None, map_revision=None,
command_id=None, command_id=None,
expected_point_count=None,
): ):
try: try:
# Snapshot único. # Snapshot único.
@ -1469,6 +1587,7 @@ class ContextoGlobalRedis:
# Identidade da transação # Identidade da transação
"map_revision": map_revision, "map_revision": map_revision,
"command_id": command_id, "command_id": command_id,
"expected_point_count": expected_point_count,
}, },
}, },
) )
@ -1492,7 +1611,6 @@ class ContextoGlobalRedis:
# ========================================================== # ==========================================================
# 1. Snapshot único da operação # 1. Snapshot único da operação
# ========================================================== # ==========================================================
operacao = cls.get_operacao() operacao = cls.get_operacao()
operacao_atual = str( operacao_atual = str(
@ -1512,7 +1630,6 @@ class ContextoGlobalRedis:
# ========================================================== # ==========================================================
# 2. Garantir que o comando pertence à revisão atual # 2. Garantir que o comando pertence à revisão atual
# ========================================================== # ==========================================================
revisao_atual = operacao.get( revisao_atual = operacao.get(
"mapa_revisao_solicitada", "mapa_revisao_solicitada",
None None
@ -1532,10 +1649,72 @@ class ContextoGlobalRedis:
), ),
} }
# ==========================================================
# 2.1. Validar o snapshot BRUTO antes da conversão
# ==========================================================
dados_dir = cls._dict(operacao.get("Dir", {}))
dados_mpc = cls._dict(dados_dir.get("mpc", {}))
pontos_brutos = dados_mpc.get("pontos", []) or []
qtd_esperada = cls._int(
operacao.get("mapa_quantidade_pontos_esperada", 0),
0,
)
primeiro_esperado = operacao.get("mapa_primeiro_idx_esperado", None)
ultimo_esperado = operacao.get("mapa_ultimo_idx_esperado", None)
if qtd_esperada <= 0:
return {
"sucesso": False,
"erro": "Quantidade esperada de pontos ausente/inválida",
}
if len(pontos_brutos) != qtd_esperada:
return {
"sucesso": False,
"erro": (
f"Snapshot bruto divergente: "
f"esperado={qtd_esperada}, recebido={len(pontos_brutos)}"
),
}
if pontos_brutos:
primeiro_bruto = cls._int(
cls._dict(pontos_brutos[0]).get("idxPonto", -1),
-1,
)
ultimo_bruto = cls._int(
cls._dict(pontos_brutos[-1]).get("idxPonto", -1),
-1,
)
if (
primeiro_esperado is not None
and primeiro_bruto != cls._int(primeiro_esperado, -999999)
):
return {
"sucesso": False,
"erro": (
f"Primeiro idxPonto divergente: "
f"esperado={primeiro_esperado}, recebido={primeiro_bruto}"
),
}
if (
ultimo_esperado is not None
and ultimo_bruto != cls._int(ultimo_esperado, -999999)
):
return {
"sucesso": False,
"erro": (
f"Último idxPonto divergente: "
f"esperado={ultimo_esperado}, recebido={ultimo_bruto}"
),
}
# ========================================================== # ==========================================================
# 3. Converter exatamente este snapshot # 3. Converter exatamente este snapshot
# ========================================================== # ==========================================================
pontos_info = cls._converter_pontos_mapa( pontos_info = cls._converter_pontos_mapa(
operacao operacao
) )
@ -1546,6 +1725,17 @@ class ContextoGlobalRedis:
"erro": "Nenhum ponto válido foi produzido", "erro": "Nenhum ponto válido foi produzido",
} }
# Não aceitamos conversão parcial. Se um único ponto falhou,
# a transação inteira é rejeitada e será reenviada.
if len(pontos_info) != qtd_esperada:
return {
"sucesso": False,
"erro": (
f"Conversão parcial rejeitada: "
f"esperado={qtd_esperada}, convertido={len(pontos_info)}"
),
}
mostrar_log( mostrar_log(
f"[MAPSYNC] mapa convertido | " f"[MAPSYNC] mapa convertido | "
f"cmd={command_id} | " f"cmd={command_id} | "
@ -1556,11 +1746,11 @@ class ContextoGlobalRedis:
# ========================================================== # ==========================================================
# 4. Gravar pontos convertidos # 4. Gravar pontos convertidos
# ========================================================== # ==========================================================
atualizado = cls.atualizar_ctx_dict( atualizado = cls.atualizar_ctx_dict(
CtxKey.DadosOperacao, CtxKey.DadosOperacao,
pontos_mapa=pontos_info, pontos_mapa=pontos_info,
mapa_status_mpc="pendente", mapa_status_mpc="pendente",
mapa_quantidade_pontos_processada=0,
) )
if not atualizado: if not atualizado:
@ -1572,10 +1762,10 @@ class ContextoGlobalRedis:
# ========================================================== # ==========================================================
# 5. Enviar EXATAMENTE este mapa ao Manager # 5. Enviar EXATAMENTE este mapa ao Manager
# ========================================================== # ==========================================================
mostrar_log( mostrar_log(
f"[MAPSYNC] enviando mapa ao Manager | " f"[MAPSYNC] enviando mapa ao Manager | "
f"cmd={command_id} | rev={map_revision}" f"cmd={command_id} | rev={map_revision} | "
f"pontos={qtd_esperada}"
) )
mpc_enviado = cls._atualizar_mpc( mpc_enviado = cls._atualizar_mpc(
@ -1585,6 +1775,7 @@ class ContextoGlobalRedis:
mapa=pontos_info, mapa=pontos_info,
map_revision=map_revision, map_revision=map_revision,
command_id=command_id, command_id=command_id,
expected_point_count=qtd_esperada,
) )
if not mpc_enviado: if not mpc_enviado:
@ -1596,10 +1787,10 @@ class ContextoGlobalRedis:
# ========================================================== # ==========================================================
# 6. Esperar Manager confirmar que realmente carregou # 6. Esperar Manager confirmar que realmente carregou
# ========================================================== # ==========================================================
aplicado_mpc = cls._aguardar_mapa_mpc_aplicado( aplicado_mpc = cls._aguardar_mapa_mpc_aplicado(
command_id=command_id, command_id=command_id,
map_revision=map_revision, map_revision=map_revision,
expected_point_count=qtd_esperada,
timeout_s=2.5, timeout_s=2.5,
) )
@ -1607,18 +1798,19 @@ class ContextoGlobalRedis:
return { return {
"sucesso": False, "sucesso": False,
"erro": ( "erro": (
f"Manager não confirmou aplicação do mapa | " f"Manager não confirmou aplicação exata do mapa | "
f"cmd={command_id} | rev={map_revision}" f"cmd={command_id} | rev={map_revision} | "
f"pontos={qtd_esperada}"
), ),
} }
# ========================================================== # ==========================================================
# 7. Só agora considerar revisão processada # 7. Só agora considerar revisão processada
# ========================================================== # ==========================================================
cls.atualizar_ctx_dict( cls.atualizar_ctx_dict(
CtxKey.DadosOperacao, CtxKey.DadosOperacao,
mapa_revisao_processada=map_revision, mapa_revisao_processada=map_revision,
mapa_quantidade_pontos_processada=len(pontos_info),
) )
return { return {
@ -1725,6 +1917,7 @@ class ContextoGlobalRedis:
cls, cls,
command_id, command_id,
map_revision, map_revision,
expected_point_count=None,
timeout_s=2.5, timeout_s=2.5,
): ):
limite = time.time() + max(0.1, float(timeout_s)) limite = time.time() + max(0.1, float(timeout_s))
@ -1744,6 +1937,11 @@ class ContextoGlobalRedis:
None None
) )
qtd_aplicada = cls._int(
operacao.get("mapa_quantidade_pontos_mpc_aplicada", 0),
0,
)
status = str( status = str(
operacao.get( operacao.get(
"mapa_status_mpc", "mapa_status_mpc",
@ -1751,11 +1949,17 @@ class ContextoGlobalRedis:
) )
) )
quantidade_ok = (
expected_point_count is None
or qtd_aplicada == int(expected_point_count)
)
if ( if (
cmd_aplicado == str(command_id) cmd_aplicado == str(command_id)
and revisao_aplicada is not None and revisao_aplicada is not None
and int(revisao_aplicada) == int(map_revision) and int(revisao_aplicada) == int(map_revision)
and status == "aplicado" and status == "aplicado"
and quantidade_ok
): ):
return True return True