ajustado mapsync e parada ao detectar fim dos pontos no MPC
This commit is contained in:
parent
82d19dc003
commit
933135d00a
|
|
@ -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
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
op,
|
|
||||||
aguardarGate: true
|
// Intertravamento publicado ANTES de esperar o gate. Mesmo que uma
|
||||||
).ConfigureAwait(false);
|
// 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,
|
||||||
|
aguardarGate: true
|
||||||
|
).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,19 +815,17 @@ 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",
|
""
|
||||||
""
|
);
|
||||||
);
|
|
||||||
|
|
||||||
Variaveis.MostrarLog(
|
Variaveis.MostrarLog(
|
||||||
$"[MAPSYNC] ACK não confirmado | " +
|
$"[MAPSYNC] ACK não confirmado | " +
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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"] = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,26 +4211,41 @@ 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": {
|
||||||
|
"safety_stop": {
|
||||||
|
"ativo": True,
|
||||||
|
"origem": "MPC",
|
||||||
|
"motivo": " | ".join(map(str, motivos)),
|
||||||
|
}
|
||||||
|
},
|
||||||
"candidatos_testados": 0,
|
"candidatos_testados": 0,
|
||||||
"motivos": _safe_motivos(motivos),
|
"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,
|
||||||
ponto_alvo,
|
ponto_alvo,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue