centralizacao no corredor e resync de mapa
This commit is contained in:
parent
8af660c9b5
commit
5fb8caa88c
|
|
@ -98,3 +98,4 @@ Python/OAK/datasets/oak-fcc-3/audit/
|
||||||
/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/AgroBase/.vs/
|
||||||
|
/.vs
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,40 @@ namespace AgroBase.Services.Operadores
|
||||||
case HealthWorkerCommandType.ScriptCarregado:
|
case HealthWorkerCommandType.ScriptCarregado:
|
||||||
DadosLeitura.Pronto = true;
|
DadosLeitura.Pronto = true;
|
||||||
DadosLeitura.ProntoEm = DateTime.Now;
|
DadosLeitura.ProntoEm = DateTime.Now;
|
||||||
MostrarLog($"Comando recebido: cmd = {cmd.ToString()}");
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var op = Variaveis.OperacaoEmAndamento;
|
||||||
|
|
||||||
|
if (
|
||||||
|
op != null &&
|
||||||
|
(op.Parametros?.ControleAutomatico ?? false) &&
|
||||||
|
(op.Trajetoria?._TrajetoriaFixa?.Any() ?? false)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Variaveis.MostrarLog(
|
||||||
|
"[MAPSYNC/RECONNECT] Python reconectado; " +
|
||||||
|
"ressincronizando mapa/MPC atual."
|
||||||
|
);
|
||||||
|
|
||||||
|
bool ok = await SincronizarMapaCriticoAsync(op)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
Variaveis.MostrarLog(
|
||||||
|
$"[MAPSYNC/RECONNECT] Resync concluído | sucesso={ok}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Variaveis.MostrarLog(
|
||||||
|
$"[MAPSYNC/RECONNECT] Erro: {ex.Message}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
@ -285,7 +318,7 @@ namespace AgroBase.Services.Operadores
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AtualizarDadosOperacao(bool forcar = false, bool sincronizarMapa = true, bool configurado = true)
|
public static void AtualizarDadosOperacao(bool forcar = false, bool sincronizarMapa = false, bool configurado = true)
|
||||||
{
|
{
|
||||||
var op = Variaveis.OperacaoEmAndamento;
|
var op = Variaveis.OperacaoEmAndamento;
|
||||||
|
|
||||||
|
|
@ -391,6 +424,9 @@ namespace AgroBase.Services.Operadores
|
||||||
horizonte = pControle.MpcHorizonte,
|
horizonte = pControle.MpcHorizonte,
|
||||||
passos_atraso = 1,
|
passos_atraso = 1,
|
||||||
ku_direcional = VariaveisEquipamento.KuDirecional,
|
ku_direcional = VariaveisEquipamento.KuDirecional,
|
||||||
|
tracking_row_hybrid_diagonal_enabled = true,
|
||||||
|
entry_capture_enabled = false,
|
||||||
|
tracking_axis_debug = false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
@ -649,12 +685,8 @@ namespace AgroBase.Services.Operadores
|
||||||
("mapa_quantidade_pontos_esperada", quantidadeEsperada),
|
("mapa_quantidade_pontos_esperada", quantidadeEsperada),
|
||||||
("mapa_primeiro_idx_esperado", primeiroIdxEsperado),
|
("mapa_primeiro_idx_esperado", primeiroIdxEsperado),
|
||||||
("mapa_ultimo_idx_esperado", ultimoIdxEsperado),
|
("mapa_ultimo_idx_esperado", ultimoIdxEsperado),
|
||||||
("mapa_revisao_processada", 0L),
|
// ACKs do Health e do Manager vivem em chaves próprias.
|
||||||
("mapa_quantidade_pontos_processada", 0),
|
// DadosOperacao guarda apenas a intenção/snapshot desejado.
|
||||||
("mapa_revisao_mpc_aplicada", 0L),
|
|
||||||
("mapa_quantidade_pontos_mpc_aplicada", 0),
|
|
||||||
("mapa_command_id_mpc_aplicado", ""),
|
|
||||||
("mapa_status_mpc", "pendente"),
|
|
||||||
("mapa_confirmado", false),
|
("mapa_confirmado", false),
|
||||||
("mapa_status", "pendente")
|
("mapa_status", "pendente")
|
||||||
);
|
);
|
||||||
|
|
@ -744,7 +776,7 @@ namespace AgroBase.Services.Operadores
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tentativa < 3)
|
if (tentativa < 3)
|
||||||
await Task.Delay(250).ConfigureAwait(false);
|
await Task.Delay(500).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
RedisService.AtualizarCampos(
|
RedisService.AtualizarCampos(
|
||||||
|
|
@ -812,7 +844,7 @@ namespace AgroBase.Services.Operadores
|
||||||
bool sucesso = await AguardarMapaAplicadoAsync(
|
bool sucesso = await AguardarMapaAplicadoAsync(
|
||||||
commandId,
|
commandId,
|
||||||
mapRevision,
|
mapRevision,
|
||||||
TimeSpan.FromSeconds(4)
|
TimeSpan.FromSeconds(6)
|
||||||
).ConfigureAwait(false);
|
).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!sucesso)
|
if (!sucesso)
|
||||||
|
|
@ -840,53 +872,64 @@ namespace AgroBase.Services.Operadores
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Segunda confirmacao, diretamente no estado transacional usado
|
// Segunda confirmação por ownership:
|
||||||
// pelo Manager. O ACK so vale se a MESMA revisao e a MESMA
|
// - HealthWorker confirma conversão/transporte em DadosHealthWorker;
|
||||||
// quantidade de pontos realmente chegaram ao MPC.
|
// - Manager confirma o MPC em DadosManagerWorker;
|
||||||
long revisaoProcessada = RedisService.GetField<long>(
|
// - DadosOperacao NÃO recebe ACK de outros processos.
|
||||||
CtxKey.DadosOperacao,
|
string healthCmd = RedisService.GetField(
|
||||||
"mapa_revisao_processada",
|
CtxKey.DadosHealthWorker,
|
||||||
|
"ultimo_comando_mapa",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
long healthRev = RedisService.GetField<long>(
|
||||||
|
CtxKey.DadosHealthWorker,
|
||||||
|
"mapa_revisao_aplicada",
|
||||||
0L
|
0L
|
||||||
);
|
);
|
||||||
|
string healthStatus = RedisService.GetField(
|
||||||
int qtdProcessada = RedisService.GetField<int>(
|
CtxKey.DadosHealthWorker,
|
||||||
CtxKey.DadosOperacao,
|
"mapa_status",
|
||||||
"mapa_quantidade_pontos_processada",
|
""
|
||||||
0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
long revisaoMpc = RedisService.GetField<long>(
|
string managerCmd = RedisService.GetField(
|
||||||
CtxKey.DadosOperacao,
|
CtxKey.DadosManagerWorker,
|
||||||
"mapa_revisao_mpc_aplicada",
|
"mapa_command_id_aplicado",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
long managerRev = RedisService.GetField<long>(
|
||||||
|
CtxKey.DadosManagerWorker,
|
||||||
|
"mapa_revisao_aplicada",
|
||||||
0L
|
0L
|
||||||
);
|
);
|
||||||
|
int managerQtd = RedisService.GetField<int>(
|
||||||
int qtdMpc = RedisService.GetField<int>(
|
CtxKey.DadosManagerWorker,
|
||||||
CtxKey.DadosOperacao,
|
"mapa_quantidade_pontos_aplicada",
|
||||||
"mapa_quantidade_pontos_mpc_aplicada",
|
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
string managerStatus = RedisService.GetField(
|
||||||
string statusMpc = RedisService.GetField(
|
CtxKey.DadosManagerWorker,
|
||||||
CtxKey.DadosOperacao,
|
"mapa_status",
|
||||||
"mapa_status_mpc",
|
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
bool confirmacaoExata =
|
bool confirmacaoExata =
|
||||||
revisaoProcessada == mapRevision &&
|
healthCmd == commandId &&
|
||||||
qtdProcessada == quantidadeEsperada &&
|
healthRev == mapRevision &&
|
||||||
revisaoMpc == mapRevision &&
|
healthStatus == "aplicado" &&
|
||||||
qtdMpc == quantidadeEsperada &&
|
managerCmd == commandId &&
|
||||||
statusMpc == "aplicado";
|
managerRev == mapRevision &&
|
||||||
|
managerQtd == quantidadeEsperada &&
|
||||||
|
managerStatus == "aplicado";
|
||||||
|
|
||||||
if (!confirmacaoExata)
|
if (!confirmacaoExata)
|
||||||
{
|
{
|
||||||
Variaveis.MostrarLog(
|
Variaveis.MostrarLog(
|
||||||
$"[MAPSYNC] ACK rejeitado por divergencia de snapshot | " +
|
$"[MAPSYNC] ACK rejeitado por divergencia de ownership | " +
|
||||||
$"revEsperada={mapRevision} | revProcessada={revisaoProcessada} | revMpc={revisaoMpc} | " +
|
$"cmdEsperado={commandId} | healthCmd={healthCmd} | managerCmd={managerCmd} | " +
|
||||||
$"qtdEsperada={quantidadeEsperada} | qtdProcessada={qtdProcessada} | qtdMpc={qtdMpc} | " +
|
$"revEsperada={mapRevision} | healthRev={healthRev} | managerRev={managerRev} | " +
|
||||||
$"statusMpc={statusMpc}"
|
$"qtdEsperada={quantidadeEsperada} | managerQtd={managerQtd} | " +
|
||||||
|
$"statusHealth={healthStatus} | statusManager={managerStatus}"
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
|
|
@ -25,6 +26,15 @@ def main():
|
||||||
loop_fixo = True
|
loop_fixo = True
|
||||||
frequencia_loop = 15 # hz
|
frequencia_loop = 15 # hz
|
||||||
|
|
||||||
|
# Serializa somente as aplicações MAPSYNC. Redis Pub/Sub pode entregar
|
||||||
|
# replays do mesmo command_id enquanto a primeira aplicação ainda termina.
|
||||||
|
_mapsync_apply_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Enquanto uma aplicação de mapa está em andamento, suspendemos apenas o
|
||||||
|
# heartbeat do DadosManagerWorker. Assim o ACK transacional não compete
|
||||||
|
# com escritas de telemetria de 15 Hz no MESMO JSON Redis.
|
||||||
|
_mapsync_em_andamento = threading.Event()
|
||||||
|
|
||||||
def loop_ativo():
|
def loop_ativo():
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -43,6 +53,10 @@ def main():
|
||||||
mostrar_log(f"❌ Erro no loop do Manager: {e}")
|
mostrar_log(f"❌ Erro no loop do Manager: {e}")
|
||||||
|
|
||||||
latencia = time.time() - t0
|
latencia = time.time() - t0
|
||||||
|
|
||||||
|
# MAPSYNC tem prioridade sobre heartbeat/telemetria nesta chave.
|
||||||
|
# O heartbeat volta automaticamente assim que o ACK for gravado.
|
||||||
|
if not _mapsync_em_andamento.is_set():
|
||||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
CtxKey.DadosManagerWorker,
|
CtxKey.DadosManagerWorker,
|
||||||
momento=agora,
|
momento=agora,
|
||||||
|
|
@ -73,10 +87,15 @@ def main():
|
||||||
_mpc_iniciado = get_iniciado()
|
_mpc_iniciado = get_iniciado()
|
||||||
|
|
||||||
if _mpc_iniciado and _p is None:
|
if _mpc_iniciado and _p is None:
|
||||||
ContextoGlobalRedis()._atualizar_mpc(
|
# O mapa já foi carregado e confirmado pelo MAPSYNC. O
|
||||||
do_manager=True,
|
# antigo fluxo chamava _atualizar_mpc(..., True), mas esse
|
||||||
iniciar_operacao=True
|
# quarto argumento chega em mpc.inicializar() como
|
||||||
|
# forcar=True e recriava a instância sem necessidade.
|
||||||
|
mostrar_log(
|
||||||
|
"[MAPSYNC/MANAGER] início confirmado; "
|
||||||
|
"instância MPC preservada"
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
elif _p is not None:
|
elif _p is not None:
|
||||||
parametros = _p.get("parametros")
|
parametros = _p.get("parametros")
|
||||||
|
|
@ -87,17 +106,9 @@ def main():
|
||||||
)
|
)
|
||||||
p_ref = _p.get("p_ref", [])
|
p_ref = _p.get("p_ref", [])
|
||||||
|
|
||||||
command_id = _p.get(
|
command_id = _p.get("command_id")
|
||||||
"command_id"
|
map_revision = _p.get("map_revision")
|
||||||
)
|
expected_point_count = _p.get("expected_point_count")
|
||||||
|
|
||||||
map_revision = _p.get(
|
|
||||||
"map_revision"
|
|
||||||
)
|
|
||||||
|
|
||||||
expected_point_count = _p.get(
|
|
||||||
"expected_point_count"
|
|
||||||
)
|
|
||||||
|
|
||||||
if expected_point_count is not None:
|
if expected_point_count is not None:
|
||||||
expected_point_count = int(expected_point_count)
|
expected_point_count = int(expected_point_count)
|
||||||
|
|
@ -108,6 +119,101 @@ def main():
|
||||||
f"esperado={expected_point_count}, recebido={quantidade_recebida}"
|
f"esperado={expected_point_count}, recebido={quantidade_recebida}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# MAPSYNC transacional deve sempre trazer identidade.
|
||||||
|
# Comandos legados sem identidade continuam possíveis, mas
|
||||||
|
# nunca contam como ACK da barreira crítica.
|
||||||
|
eh_mapsync = (
|
||||||
|
command_id is not None
|
||||||
|
and map_revision is not None
|
||||||
|
and expected_point_count is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
lock_ctx = _mapsync_apply_lock if eh_mapsync else threading.Lock()
|
||||||
|
|
||||||
|
# Pare imediatamente o heartbeat concorrente antes de
|
||||||
|
# tocar no estado transacional do Manager.
|
||||||
|
if eh_mapsync:
|
||||||
|
_mapsync_em_andamento.set()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with lock_ctx:
|
||||||
|
if eh_mapsync:
|
||||||
|
estado_mgr = ContextoGlobalRedis.get(
|
||||||
|
CtxKey.DadosManagerWorker, {}
|
||||||
|
)
|
||||||
|
if not isinstance(estado_mgr, dict):
|
||||||
|
estado_mgr = {}
|
||||||
|
|
||||||
|
ack_cmd = str(
|
||||||
|
estado_mgr.get("mapa_command_id_aplicado", "") or ""
|
||||||
|
)
|
||||||
|
ack_rev = int(
|
||||||
|
estado_mgr.get("mapa_revisao_aplicada", 0) or 0
|
||||||
|
)
|
||||||
|
ack_qtd = int(
|
||||||
|
estado_mgr.get("mapa_quantidade_pontos_aplicada", 0) or 0
|
||||||
|
)
|
||||||
|
ack_status = str(
|
||||||
|
estado_mgr.get("mapa_status", "") or ""
|
||||||
|
)
|
||||||
|
|
||||||
|
incoming_rev = int(map_revision)
|
||||||
|
incoming_qtd = int(expected_point_count)
|
||||||
|
|
||||||
|
# Uma revisão mais nova já aplicada nunca pode ser
|
||||||
|
# rebaixada por uma mensagem atrasada do Pub/Sub.
|
||||||
|
if ack_status == "aplicado" and ack_rev > incoming_rev:
|
||||||
|
mostrar_log(
|
||||||
|
f"[MAPSYNC/MANAGER] replay obsoleto ignorado | "
|
||||||
|
f"cmd={command_id} | rev={incoming_rev} | "
|
||||||
|
f"rev_atual={ack_rev}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Idempotência por revisão: command_id é identidade
|
||||||
|
# de transporte; map_revision identifica o snapshot.
|
||||||
|
# Se esta MESMA revisão/quantidade já está aplicada,
|
||||||
|
# apenas reafirmamos o ACK para o command_id recebido.
|
||||||
|
if (
|
||||||
|
ack_status == "aplicado"
|
||||||
|
and ack_rev == incoming_rev
|
||||||
|
and ack_qtd == incoming_qtd
|
||||||
|
):
|
||||||
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
|
CtxKey.DadosManagerWorker,
|
||||||
|
mapa_command_id_aplicado=str(command_id),
|
||||||
|
mapa_revisao_aplicada=incoming_rev,
|
||||||
|
mapa_quantidade_pontos_aplicada=incoming_qtd,
|
||||||
|
mapa_status="aplicado",
|
||||||
|
mapa_erro="",
|
||||||
|
mapa_aplicado_em=time.time(),
|
||||||
|
)
|
||||||
|
|
||||||
|
tipo_replay = (
|
||||||
|
"duplicado" if ack_cmd == str(command_id)
|
||||||
|
else "rebind"
|
||||||
|
)
|
||||||
|
mostrar_log(
|
||||||
|
f"[MAPSYNC/MANAGER] ACK reafirmado ({tipo_replay}) | "
|
||||||
|
f"cmd={command_id} | rev={incoming_rev} | "
|
||||||
|
f"pontos={incoming_qtd}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sinaliza recepção/posse da transação ANTES de
|
||||||
|
# construir o MPC. O Health vê "aplicando", para de
|
||||||
|
# retransmitir e concede uma janela de conclusão.
|
||||||
|
if eh_mapsync:
|
||||||
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
|
CtxKey.DadosManagerWorker,
|
||||||
|
mapa_command_id_aplicado=str(command_id),
|
||||||
|
mapa_revisao_aplicada=int(map_revision),
|
||||||
|
mapa_quantidade_pontos_aplicada=int(expected_point_count),
|
||||||
|
mapa_status="aplicando",
|
||||||
|
mapa_erro="",
|
||||||
|
mapa_aplicado_em=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
mostrar_log(
|
mostrar_log(
|
||||||
f"[MAPSYNC/MANAGER] aplicando mapa | "
|
f"[MAPSYNC/MANAGER] aplicando mapa | "
|
||||||
f"cmd={command_id} | "
|
f"cmd={command_id} | "
|
||||||
|
|
@ -115,30 +221,30 @@ def main():
|
||||||
f"pontos={len(mapa or [])}"
|
f"pontos={len(mapa or [])}"
|
||||||
)
|
)
|
||||||
|
|
||||||
iniciar_mpc(
|
resultado_mpc = iniciar_mpc(
|
||||||
parametros,
|
parametros,
|
||||||
mapa,
|
mapa,
|
||||||
p_ref,
|
p_ref,
|
||||||
iniciar_operacao
|
iniciar_operacao
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------
|
if resultado_mpc is False:
|
||||||
# ACK somente depois de inicializar()
|
raise RuntimeError(
|
||||||
# retornar sem exception.
|
"MPC recusou a inicialização do snapshot recebido"
|
||||||
# ---------------------------------------------
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
command_id is not None
|
command_id is not None
|
||||||
and map_revision is not None
|
and map_revision is not None
|
||||||
):
|
):
|
||||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
CtxKey.DadosOperacao,
|
CtxKey.DadosManagerWorker,
|
||||||
mapa_command_id_mpc_aplicado=str(command_id),
|
mapa_command_id_aplicado=str(command_id),
|
||||||
mapa_revisao_mpc_aplicada=map_revision,
|
mapa_revisao_aplicada=map_revision,
|
||||||
mapa_quantidade_pontos_mpc_aplicada=len(mapa or []),
|
mapa_quantidade_pontos_aplicada=len(mapa or []),
|
||||||
mapa_status_mpc="aplicado",
|
mapa_status="aplicado",
|
||||||
mapa_erro_mpc="",
|
mapa_erro="",
|
||||||
mapa_mpc_aplicado_em=time.time(),
|
mapa_aplicado_em=time.time(),
|
||||||
)
|
)
|
||||||
|
|
||||||
mostrar_log(
|
mostrar_log(
|
||||||
|
|
@ -148,6 +254,10 @@ def main():
|
||||||
f"pontos={len(mapa or [])}"
|
f"pontos={len(mapa or [])}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if eh_mapsync:
|
||||||
|
_mapsync_em_andamento.clear()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ContextoGlobalRedis()._atualizar_mpc(
|
ContextoGlobalRedis()._atualizar_mpc(
|
||||||
do_manager=True,
|
do_manager=True,
|
||||||
|
|
@ -161,12 +271,13 @@ def main():
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
CtxKey.DadosOperacao,
|
CtxKey.DadosManagerWorker,
|
||||||
mapa_command_id_mpc_aplicado=str(command_id),
|
mapa_command_id_aplicado=str(command_id),
|
||||||
mapa_revisao_mpc_aplicada=map_revision,
|
mapa_revisao_aplicada=map_revision,
|
||||||
mapa_quantidade_pontos_mpc_aplicada=0,
|
mapa_quantidade_pontos_aplicada=0,
|
||||||
mapa_status_mpc="erro",
|
mapa_status="erro",
|
||||||
mapa_erro_mpc=str(e),
|
mapa_erro=str(e),
|
||||||
|
mapa_aplicado_em=time.time(),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -194,6 +305,20 @@ def main():
|
||||||
|
|
||||||
def inicializar():
|
def inicializar():
|
||||||
mostrar_log("🚀 Iniciando Manager Worker...")
|
mostrar_log("🚀 Iniciando Manager Worker...")
|
||||||
|
|
||||||
|
# ACK pertence à memória viva deste processo. Se o Manager reiniciou,
|
||||||
|
# uma confirmação antiga no Redis não pode liberar a operação como se
|
||||||
|
# o MPC ainda estivesse carregado nesta nova instância.
|
||||||
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
|
CtxKey.DadosManagerWorker,
|
||||||
|
mapa_command_id_aplicado="",
|
||||||
|
mapa_revisao_aplicada=0,
|
||||||
|
mapa_quantidade_pontos_aplicada=0,
|
||||||
|
mapa_status="",
|
||||||
|
mapa_erro="worker reiniciado; aguardando MAPSYNC",
|
||||||
|
mapa_aplicado_em=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
ContextoGlobalRedis.ouvir_comandos(CmdKey.ManagerWorkerRx, redis_callback)
|
ContextoGlobalRedis.ouvir_comandos(CmdKey.ManagerWorkerRx, redis_callback)
|
||||||
ContextoGlobalRedis.publicar_comando(CmdKey.ManagerWorkerTx, { "cmd": ManagerWorkerCommandType.ScriptCarregado.value })
|
ContextoGlobalRedis.publicar_comando(CmdKey.ManagerWorkerTx, { "cmd": ManagerWorkerCommandType.ScriptCarregado.value })
|
||||||
loop_ativo()
|
loop_ativo()
|
||||||
|
|
|
||||||
|
|
@ -547,17 +547,18 @@ class ControladorMPC:
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# O campo mostrou que, ao terminar a ferradura, o rover pode entrar na
|
# O campo mostrou que, ao terminar a ferradura, o rover pode entrar na
|
||||||
# nova passada com 20-70 cm de cross-track e ainda 5-12 graus de yaw.
|
# nova passada com 20-70 cm de cross-track e ainda 5-12 graus de yaw.
|
||||||
# A política antiga priorizava yaw e só depois liberava o crab, fazendo
|
# V21.1 - ENTRY CAPTURE legado fica DESLIGADO por padrão.
|
||||||
# o rover consumir alguns metros dentro da rua antes de centralizar.
|
|
||||||
#
|
#
|
||||||
# Agora o fim do Pure Pursuit arma uma janela curta "centro primeiro":
|
# Nos testes de entrada de corredor, o crab pós-Dubins podia assumir o
|
||||||
# - se o erro lateral ainda é relevante e |heading| <= 15 graus,
|
# volante ainda com heading moderado (até 15 graus) e preservar uma pose
|
||||||
# MovimentoDiagonal recebe prioridade mesmo sem heading perfeito;
|
# ruim logo após a ferradura. A V20/V21 já consegue capturar a centerline
|
||||||
# - heading muito grande continua com Arco/Dianteira normalmente;
|
# com Dianteira/Traseira de forma contínua, portanto a transição normal
|
||||||
# - assim que |e_lat| <= 10 cm, a política normal volta a mandar;
|
# passa a ser a política preferida.
|
||||||
# - existe limite de distância para o latch nunca permanecer indefinido.
|
#
|
||||||
|
# O código permanece disponível apenas para A/B/rollback explícito:
|
||||||
|
# entry_capture_enabled = True
|
||||||
self._entry_capture_enabled = bool(
|
self._entry_capture_enabled = bool(
|
||||||
parametros_mpc.get("entry_capture_enabled", True)
|
parametros_mpc.get("entry_capture_enabled", False)
|
||||||
)
|
)
|
||||||
self._entry_capture_heading_max_graus = _safe_float(
|
self._entry_capture_heading_max_graus = _safe_float(
|
||||||
parametros_mpc.get("entry_capture_heading_max_graus", 15.0),
|
parametros_mpc.get("entry_capture_heading_max_graus", 15.0),
|
||||||
|
|
@ -731,6 +732,18 @@ class ControladorMPC:
|
||||||
)
|
)
|
||||||
self._tracking_eixo_yaw_ultimo_log = None
|
self._tracking_eixo_yaw_ultimo_log = None
|
||||||
|
|
||||||
|
# V21.1 - diagnóstico opcional do seletor Dianteira x Traseira.
|
||||||
|
# Não altera a escolha; apenas mostra periodicamente os dois custos para
|
||||||
|
# comprovar que RodasTraseiras continua participando da competição.
|
||||||
|
self._tracking_axis_debug = bool(
|
||||||
|
parametros_mpc.get("tracking_axis_debug", False)
|
||||||
|
)
|
||||||
|
self._tracking_axis_debug_period_s = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_axis_debug_period_s", 2.0),
|
||||||
|
2.0, min_value=0.25, max_value=30.0,
|
||||||
|
)
|
||||||
|
self._tracking_axis_debug_last_t = 0.0
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# V16 - CINEMATICA 4WS UNIFICADA
|
# V16 - CINEMATICA 4WS UNIFICADA
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
@ -791,15 +804,106 @@ class ControladorMPC:
|
||||||
9.0, min_value=0.0, max_value=self.angulo_max_graus,
|
9.0, min_value=0.0, max_value=self.angulo_max_graus,
|
||||||
)
|
)
|
||||||
self._tracking_curve_diag_emergency_m = _safe_float(
|
self._tracking_curve_diag_emergency_m = _safe_float(
|
||||||
parametros_mpc.get("tracking_curve_diag_emergency_m", 0.24),
|
parametros_mpc.get("tracking_curve_diag_emergency_m", 0.12),
|
||||||
0.24, min_value=self._tracking_center_recenter_m, max_value=0.80,
|
0.12, min_value=self._tracking_center_recenter_m, max_value=0.80,
|
||||||
)
|
)
|
||||||
self._tracking_curve_diag_release_m = _safe_float(
|
self._tracking_curve_diag_release_m = _safe_float(
|
||||||
parametros_mpc.get("tracking_curve_diag_release_m", 0.14),
|
parametros_mpc.get("tracking_curve_diag_release_m", 0.06),
|
||||||
0.14, min_value=self._tracking_center_hold_m,
|
0.06, min_value=self._tracking_center_hold_m,
|
||||||
max_value=self._tracking_curve_diag_emergency_m,
|
max_value=self._tracking_curve_diag_emergency_m,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# V20 - TRACKING CONTINUO DE CORREDOR
|
||||||
|
#
|
||||||
|
# Em CaminhandoRua não alternamos mais entre "corrigir lateral com
|
||||||
|
# Diagonal" e "corrigir yaw com Dianteira/Traseira". Esse chaveamento
|
||||||
|
# desacopla duas grandezas que, geometricamente, precisam ser resolvidas
|
||||||
|
# juntas. O controlador nominal da passada passa a ser contínuo:
|
||||||
|
#
|
||||||
|
# delta = feed-forward(curvatura)
|
||||||
|
# + feedback(heading)
|
||||||
|
# + feedback(cross-track)
|
||||||
|
#
|
||||||
|
# Dianteira/Traseira continuam competindo no seletor axle-aware.
|
||||||
|
# MovimentoDiagonal fica preservado para Entry Capture e fluxos legados,
|
||||||
|
# mas não é o HOLD nominal de CaminhandoRua quando esta opção está ativa.
|
||||||
|
self._tracking_row_continuous_enabled = bool(
|
||||||
|
parametros_mpc.get("tracking_row_continuous_enabled", True)
|
||||||
|
)
|
||||||
|
self._tracking_row_lateral_gain = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_lateral_gain", 0.85),
|
||||||
|
0.85, min_value=0.0, max_value=3.0,
|
||||||
|
)
|
||||||
|
self._tracking_row_lateral_max_graus = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_lateral_max_graus", 12.0),
|
||||||
|
12.0, min_value=1.0, max_value=self.angulo_max_graus,
|
||||||
|
)
|
||||||
|
|
||||||
|
# O PP da cabeceira responde imediatamente porque MovimentoArco não
|
||||||
|
# passa pelo filtro de tracking. Em passada mantemos amortecimento, mas
|
||||||
|
# com autoridade suficiente para não gastar metros "acordando" a roda.
|
||||||
|
self._tracking_row_filter_tau_s = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_filter_tau_s", 0.12),
|
||||||
|
0.12, min_value=0.0, max_value=1.0,
|
||||||
|
)
|
||||||
|
self._tracking_row_rate_graus_s = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_rate_graus_s", 35.0),
|
||||||
|
35.0, min_value=5.0, max_value=120.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# V21 - RECOVERY HIBRIDO POR MOVIMENTO DIAGONAL
|
||||||
|
#
|
||||||
|
# A V20 continua sendo a lei NOMINAL da passada. O crab/Diagonal só
|
||||||
|
# recebe o volante quando existe assinatura de derrapagem lateral:
|
||||||
|
# cross-track grande, corpo ainda alinhado e centerline reta ou apenas
|
||||||
|
# suavemente curva. Assim preservamos o tracking continuo da V20 e
|
||||||
|
# recuperamos a principal vantagem pratica do 4WS em terreno inclinado.
|
||||||
|
#
|
||||||
|
# Flag desligada => comportamento V20 puro, bit-a-bit na politica.
|
||||||
|
self._tracking_row_hybrid_diagonal_enabled = bool(
|
||||||
|
parametros_mpc.get("tracking_row_hybrid_diagonal_enabled", False)
|
||||||
|
)
|
||||||
|
self._tracking_row_hybrid_enter_m = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_hybrid_enter_m", 0.15),
|
||||||
|
0.15, min_value=0.08, max_value=0.80,
|
||||||
|
)
|
||||||
|
self._tracking_row_hybrid_exit_m = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_hybrid_exit_m", 0.07),
|
||||||
|
0.07, min_value=self._tracking_center_hold_m,
|
||||||
|
max_value=self._tracking_row_hybrid_enter_m,
|
||||||
|
)
|
||||||
|
self._tracking_row_hybrid_heading_enter_graus = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_hybrid_heading_enter_graus", 3.0),
|
||||||
|
3.0, min_value=0.5, max_value=10.0,
|
||||||
|
)
|
||||||
|
self._tracking_row_hybrid_heading_abort_graus = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_hybrid_heading_abort_graus", 6.0),
|
||||||
|
6.0, min_value=self._tracking_row_hybrid_heading_enter_graus,
|
||||||
|
max_value=15.0,
|
||||||
|
)
|
||||||
|
self._tracking_row_hybrid_curve_enter_max_graus = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_hybrid_curve_enter_max_graus", 1.5),
|
||||||
|
1.5, min_value=0.0, max_value=8.0,
|
||||||
|
)
|
||||||
|
self._tracking_row_hybrid_curve_abort_graus = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_hybrid_curve_abort_graus", 2.5),
|
||||||
|
2.5, min_value=self._tracking_row_hybrid_curve_enter_max_graus,
|
||||||
|
max_value=12.0,
|
||||||
|
)
|
||||||
|
self._tracking_row_hybrid_max_dist_m = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_hybrid_max_dist_m", 2.50),
|
||||||
|
2.50, min_value=0.50, max_value=10.0,
|
||||||
|
)
|
||||||
|
self._tracking_row_hybrid_cooldown_m = _safe_float(
|
||||||
|
parametros_mpc.get("tracking_row_hybrid_cooldown_m", 0.75),
|
||||||
|
0.75, min_value=0.0, max_value=5.0,
|
||||||
|
)
|
||||||
|
self._tracking_row_hybrid_active = False
|
||||||
|
self._tracking_row_hybrid_dist_m = 0.0
|
||||||
|
self._tracking_row_hybrid_cooldown_restante_m = 0.0
|
||||||
|
|
||||||
# Estado interno apenas para histerese do recovery lateral em curva.
|
# Estado interno apenas para histerese do recovery lateral em curva.
|
||||||
# Não sai do MPC e não altera o contrato C#/Redis.
|
# Não sai do MPC e não altera o contrato C#/Redis.
|
||||||
self._tracking_curve_diag_recovery_active = False
|
self._tracking_curve_diag_recovery_active = False
|
||||||
|
|
@ -1892,6 +1996,33 @@ class ControladorMPC:
|
||||||
)
|
)
|
||||||
return float(np.clip(delta, -lim, lim))
|
return float(np.clip(delta, -lim, lim))
|
||||||
|
|
||||||
|
def _angulo_lateral_row_tracking(self, e_lat, velocidade):
|
||||||
|
"""V20: parcela lateral contínua da passada para Dianteira/Traseira.
|
||||||
|
|
||||||
|
É uma forma Stanley-like: quanto maior o cross-track, maior o heading
|
||||||
|
corretivo pedido; a velocidade reduz a agressividade naturalmente.
|
||||||
|
Diferente do crab, esta parcela NÃO desliga a autoridade de yaw.
|
||||||
|
"""
|
||||||
|
e_lat = float(e_lat)
|
||||||
|
v = max(0.0, float(velocidade))
|
||||||
|
|
||||||
|
e_eff_abs = max(0.0, abs(e_lat) - self._tracking_center_hold_m)
|
||||||
|
if e_eff_abs <= 0.0 or self._tracking_row_lateral_gain <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
e_eff = math.copysign(e_eff_abs, e_lat)
|
||||||
|
delta = math.atan2(
|
||||||
|
self._tracking_row_lateral_gain * e_eff,
|
||||||
|
v + self._velocidade_offset_lateral,
|
||||||
|
)
|
||||||
|
lim = math.radians(
|
||||||
|
min(
|
||||||
|
self._tracking_row_lateral_max_graus,
|
||||||
|
self.angulo_max_graus,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return float(np.clip(delta, -lim, lim))
|
||||||
|
|
||||||
def _angulo_diagonal_tracking(self, e_lat, velocidade):
|
def _angulo_diagonal_tracking(self, e_lat, velocidade):
|
||||||
"""Comando crab para centralizar sem alterar o heading do corpo."""
|
"""Comando crab para centralizar sem alterar o heading do corpo."""
|
||||||
e_lat = float(e_lat)
|
e_lat = float(e_lat)
|
||||||
|
|
@ -1912,6 +2043,114 @@ class ControladorMPC:
|
||||||
)
|
)
|
||||||
return float(np.clip(delta, -lim, lim))
|
return float(np.clip(delta, -lim, lim))
|
||||||
|
|
||||||
|
def _atualizar_tracking_row_hybrid_real(
|
||||||
|
self, x, y, theta, velocidade, idx_base, status_carro, contexto
|
||||||
|
):
|
||||||
|
"""V21: atualiza o latch do recovery Diagonal usando apenas a pose REAL.
|
||||||
|
|
||||||
|
A decisao nao e feita dentro das simulacoes do beam search. Isso evita
|
||||||
|
que candidatos hipoteticos armem/desarmem o estado do controlador.
|
||||||
|
O Diagonal entra somente com assinatura de deriva lateral: erro lateral
|
||||||
|
grande, heading pequeno e baixa curvatura da centerline.
|
||||||
|
|
||||||
|
Histerese:
|
||||||
|
enter_m > exit_m;
|
||||||
|
heading_enter < heading_abort;
|
||||||
|
curve_enter < curve_abort.
|
||||||
|
|
||||||
|
Existe ainda um limite de distancia e cooldown para impedir que o crab
|
||||||
|
fique preso indefinidamente caso a derrapagem supere sua autoridade.
|
||||||
|
"""
|
||||||
|
enabled = bool(self._tracking_row_hybrid_diagonal_enabled)
|
||||||
|
em_passada = _status_in(status_carro, [StatusCarroMapa.CaminhandoRua])
|
||||||
|
entry_ativo = bool(getattr(self, "_entry_capture_active", False))
|
||||||
|
|
||||||
|
dt = _safe_float(
|
||||||
|
getattr(self, "tempo_execucao_local", 0.5),
|
||||||
|
0.5, min_value=0.05, max_value=1.5,
|
||||||
|
)
|
||||||
|
ds = abs(float(velocidade)) * dt
|
||||||
|
|
||||||
|
# Cooldown e medido em metros, portanto permanece coerente em baixa e
|
||||||
|
# alta velocidade.
|
||||||
|
self._tracking_row_hybrid_cooldown_restante_m = max(
|
||||||
|
0.0,
|
||||||
|
float(getattr(self, "_tracking_row_hybrid_cooldown_restante_m", 0.0)) - ds,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not enabled or not em_passada or entry_ativo:
|
||||||
|
self._tracking_row_hybrid_active = False
|
||||||
|
self._tracking_row_hybrid_dist_m = 0.0
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
ref = self._referencia_caminho_lookahead(
|
||||||
|
float(x), float(y), int(idx_base), float(velocidade), status_carro
|
||||||
|
)
|
||||||
|
e_lat_abs = abs(float(ref["e_lat"]))
|
||||||
|
theta_ref = float(ref["theta_ref"])
|
||||||
|
e_head_deg = abs(math.degrees(
|
||||||
|
self._erro_heading_caminho(float(theta), theta_ref)
|
||||||
|
))
|
||||||
|
_, delta_ff = self._feedforward_curva_tracking(
|
||||||
|
ref.get("s_proj"), int(idx_base), contexto
|
||||||
|
)
|
||||||
|
ff_deg_abs = abs(math.degrees(float(delta_ff)))
|
||||||
|
except Exception as e:
|
||||||
|
# Qualquer duvida geometrica devolve o volante a V20.
|
||||||
|
if bool(getattr(self, "_tracking_row_hybrid_active", False)):
|
||||||
|
_log(f"[MPC/V21 HYBRID] OFF por referencia invalida: {e}")
|
||||||
|
self._tracking_row_hybrid_active = False
|
||||||
|
self._tracking_row_hybrid_dist_m = 0.0
|
||||||
|
return
|
||||||
|
|
||||||
|
ativo = bool(getattr(self, "_tracking_row_hybrid_active", False))
|
||||||
|
|
||||||
|
if ativo:
|
||||||
|
self._tracking_row_hybrid_dist_m += ds
|
||||||
|
motivo_off = None
|
||||||
|
|
||||||
|
if e_lat_abs <= float(self._tracking_row_hybrid_exit_m):
|
||||||
|
motivo_off = "centralizado"
|
||||||
|
elif e_head_deg > float(self._tracking_row_hybrid_heading_abort_graus):
|
||||||
|
motivo_off = "heading"
|
||||||
|
elif ff_deg_abs > float(self._tracking_row_hybrid_curve_abort_graus):
|
||||||
|
motivo_off = "curvatura"
|
||||||
|
elif self._tracking_row_hybrid_dist_m >= float(self._tracking_row_hybrid_max_dist_m):
|
||||||
|
motivo_off = "distancia_max"
|
||||||
|
self._tracking_row_hybrid_cooldown_restante_m = float(
|
||||||
|
self._tracking_row_hybrid_cooldown_m
|
||||||
|
)
|
||||||
|
|
||||||
|
if motivo_off is not None:
|
||||||
|
_log(
|
||||||
|
"[MPC/V21 HYBRID] Diagonal OFF | "
|
||||||
|
f"motivo={motivo_off} | e_lat={e_lat_abs:.3f}m | "
|
||||||
|
f"e_head={e_head_deg:.2f}deg | ff={ff_deg_abs:.2f}deg | "
|
||||||
|
f"dist={self._tracking_row_hybrid_dist_m:.2f}m"
|
||||||
|
)
|
||||||
|
self._tracking_row_hybrid_active = False
|
||||||
|
self._tracking_row_hybrid_dist_m = 0.0
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._tracking_row_hybrid_cooldown_restante_m > 1e-6:
|
||||||
|
return
|
||||||
|
|
||||||
|
pode_entrar = bool(
|
||||||
|
e_lat_abs >= float(self._tracking_row_hybrid_enter_m)
|
||||||
|
and e_head_deg <= float(self._tracking_row_hybrid_heading_enter_graus)
|
||||||
|
and ff_deg_abs <= float(self._tracking_row_hybrid_curve_enter_max_graus)
|
||||||
|
)
|
||||||
|
|
||||||
|
if pode_entrar:
|
||||||
|
self._tracking_row_hybrid_active = True
|
||||||
|
self._tracking_row_hybrid_dist_m = 0.0
|
||||||
|
_log(
|
||||||
|
"[MPC/V21 HYBRID] Diagonal ON | "
|
||||||
|
f"e_lat={e_lat_abs:.3f}m | e_head={e_head_deg:.2f}deg | "
|
||||||
|
f"ff={ff_deg_abs:.2f}deg"
|
||||||
|
)
|
||||||
|
|
||||||
def _entre_eixos_tracking(self, contexto):
|
def _entre_eixos_tracking(self, contexto):
|
||||||
equipamento = _as_dict(_as_dict(contexto or {}).get("Equipamento", {}))
|
equipamento = _as_dict(_as_dict(contexto or {}).get("Equipamento", {}))
|
||||||
fallback = float(getattr(self, "_cinematica_entre_eixos_m", 0.94))
|
fallback = float(getattr(self, "_cinematica_entre_eixos_m", 0.94))
|
||||||
|
|
@ -2233,28 +2472,38 @@ class ControladorMPC:
|
||||||
}
|
}
|
||||||
|
|
||||||
def _log_escolha_eixo_v15(self, escolhido, frente_dbg, traseira_dbg):
|
def _log_escolha_eixo_v15(self, escolhido, frente_dbg, traseira_dbg):
|
||||||
"""Loga somente quando o eixo escolhido muda, evitando spam."""
|
"""V21.1: diagnóstico opcional do seletor de eixo, sem alterar controle."""
|
||||||
try:
|
try:
|
||||||
nome = _enum_name(
|
nome = _enum_name(
|
||||||
escolhido, TipoMovimentoDirecional, default=str(escolhido)
|
escolhido, TipoMovimentoDirecional, default=str(escolhido)
|
||||||
)
|
)
|
||||||
if self._tracking_eixo_yaw_ultimo_log == nome:
|
mudou = self._tracking_eixo_yaw_ultimo_log != nome
|
||||||
return
|
|
||||||
self._tracking_eixo_yaw_ultimo_log = nome
|
self._tracking_eixo_yaw_ultimo_log = nome
|
||||||
|
|
||||||
#_log(
|
if not bool(getattr(self, "_tracking_axis_debug", False)):
|
||||||
# "[MPC/V15 AXIS] "
|
return
|
||||||
# f"eixo={nome} | "
|
|
||||||
# f"Jf={frente_dbg['custo']:.2f} "
|
agora = time.monotonic()
|
||||||
# f"Jt={traseira_dbg['custo']:.2f} | "
|
periodo = float(getattr(self, "_tracking_axis_debug_period_s", 2.0))
|
||||||
# f"F0={frente_dbg['e_front_0']:+.3f} "
|
ultimo = float(getattr(self, "_tracking_axis_debug_last_t", 0.0))
|
||||||
# f"T0={frente_dbg['e_rear_0']:+.3f} | "
|
if not mudou and (agora - ultimo) < periodo:
|
||||||
# f"Ff={frente_dbg['e_front']:+.3f} "
|
return
|
||||||
# f"Tf={frente_dbg['e_rear']:+.3f} | "
|
self._tracking_axis_debug_last_t = agora
|
||||||
# f"Ft={traseira_dbg['e_front']:+.3f} "
|
|
||||||
# f"Tt={traseira_dbg['e_rear']:+.3f} | "
|
_log(
|
||||||
# f"preserva={frente_dbg['melhor_eixo_0']}"
|
"[MPC/V21 AXIS] "
|
||||||
#)
|
f"eixo={nome} | "
|
||||||
|
f"Jf={frente_dbg['custo']:.3f} "
|
||||||
|
f"Jt={traseira_dbg['custo']:.3f} | "
|
||||||
|
f"F0={frente_dbg['e_front_0']:+.3f} "
|
||||||
|
f"T0={frente_dbg['e_rear_0']:+.3f} | "
|
||||||
|
f"Ff={frente_dbg['e_front']:+.3f} "
|
||||||
|
f"Tf={frente_dbg['e_rear']:+.3f} | "
|
||||||
|
f"Ft={traseira_dbg['e_front']:+.3f} "
|
||||||
|
f"Tt={traseira_dbg['e_rear']:+.3f} | "
|
||||||
|
f"preservaF={frente_dbg['melhor_eixo_0']} "
|
||||||
|
f"preservaT={traseira_dbg['melhor_eixo_0']}"
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -2339,21 +2588,16 @@ class ControladorMPC:
|
||||||
s_proj=None,
|
s_proj=None,
|
||||||
contexto=None,
|
contexto=None,
|
||||||
):
|
):
|
||||||
"""V15: supervisor de tracking da passada com CURVE_TRACK axle-aware.
|
"""V20: supervisor contínuo de tracking da passada, axle-aware.
|
||||||
|
|
||||||
RETA:
|
Com tracking_row_continuous_enabled=True:
|
||||||
- Diagonal é HOLD nominal e corrige cross-track sem criar yaw;
|
- Arco fica reservado para recuperação grosseira de heading;
|
||||||
- Dianteira/Traseira entram quando heading realmente sai da banda;
|
- Dianteira/Traseira controlam continuamente a centerline;
|
||||||
- Arco continua sendo recuperação grosseira.
|
- comando = feed-forward(curvatura) + heading + cross-track;
|
||||||
|
- não existe chaveamento RETA/CURVA para decidir entre yaw e crab;
|
||||||
|
- Diagonal deixa de ser o HOLD nominal de CaminhandoRua.
|
||||||
|
|
||||||
CURVA:
|
O bloco V15 antigo permanece abaixo para rollback/A-B.
|
||||||
- Dianteira/Traseira acompanham continuamente a curvatura da rua;
|
|
||||||
- comando = feed-forward(curvatura) + heading + lateral;
|
|
||||||
- não esperamos acumular heading/cross-track para começar a virar;
|
|
||||||
- Diagonal só interrompe CURVE_TRACK em erro lateral de emergência.
|
|
||||||
|
|
||||||
Isso evita o deadlock da V13 em que a diagonal tentava centralizar
|
|
||||||
enquanto a própria centerline continuava girando.
|
|
||||||
"""
|
"""
|
||||||
e_lat = float(e_lat)
|
e_lat = float(e_lat)
|
||||||
e_lat_abs = abs(e_lat)
|
e_lat_abs = abs(e_lat)
|
||||||
|
|
@ -2391,6 +2635,77 @@ class ControladorMPC:
|
||||||
kappa, delta_ff = self._feedforward_curva_tracking(
|
kappa, delta_ff = self._feedforward_curva_tracking(
|
||||||
s_proj, idx_base, contexto
|
s_proj, idx_base, contexto
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --------------------------------------------------------------
|
||||||
|
# V21) Recovery hibrido: crab curto para derrapagem lateral pura.
|
||||||
|
#
|
||||||
|
# O latch e atualizado somente no ciclo REAL. Aqui apenas respeitamos
|
||||||
|
# sua decisao. Se por algum motivo a geometria ja mudou entre os dois
|
||||||
|
# pontos, as guardas abaixo devolvem imediatamente o volante a V20.
|
||||||
|
# --------------------------------------------------------------
|
||||||
|
if (
|
||||||
|
bool(self._tracking_row_hybrid_diagonal_enabled)
|
||||||
|
and bool(getattr(self, "_tracking_row_hybrid_active", False))
|
||||||
|
):
|
||||||
|
ff_deg_abs = abs(math.degrees(float(delta_ff)))
|
||||||
|
if (
|
||||||
|
e_head_deg <= float(self._tracking_row_hybrid_heading_abort_graus)
|
||||||
|
and ff_deg_abs <= float(self._tracking_row_hybrid_curve_abort_graus)
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
TipoMovimentoDirecional.MovimentoDiagonal,
|
||||||
|
self._angulo_diagonal_tracking(e_lat, v),
|
||||||
|
)
|
||||||
|
self._tracking_row_hybrid_active = False
|
||||||
|
self._tracking_row_hybrid_dist_m = 0.0
|
||||||
|
|
||||||
|
# --------------------------------------------------------------
|
||||||
|
# V20) CaminhandoRua: tracking contínuo e acoplado.
|
||||||
|
#
|
||||||
|
# Não existe mais a situação "heading pequeno => crab puro". Mesmo em
|
||||||
|
# reta, um cross-track gera uma curvatura de convergência. Conforme o
|
||||||
|
# rover cria heading para voltar ao centro, a parcela de heading se
|
||||||
|
# opõe naturalmente à lateral e amortece a chegada.
|
||||||
|
#
|
||||||
|
# Em curva, o feed-forward continua girando junto com a centerline
|
||||||
|
# enquanto a mesma lei corrige o deslocamento lateral. Assim não
|
||||||
|
# congelamos yaw justamente quando mais precisamos acompanhar a curva.
|
||||||
|
# --------------------------------------------------------------
|
||||||
|
if bool(self._tracking_row_continuous_enabled):
|
||||||
|
self._tracking_curve_diag_recovery_active = False
|
||||||
|
|
||||||
|
delta_heading = self._ganho_heading_reta * erro_heading_rad
|
||||||
|
delta_lateral = self._angulo_lateral_row_tracking(e_lat, v)
|
||||||
|
|
||||||
|
delta_base = delta_ff + delta_heading + delta_lateral
|
||||||
|
lim = math.radians(self.angulo_max_graus)
|
||||||
|
delta_base = float(np.clip(delta_base, -lim, lim))
|
||||||
|
|
||||||
|
if (
|
||||||
|
x is not None
|
||||||
|
and y is not None
|
||||||
|
and theta is not None
|
||||||
|
and idx_base is not None
|
||||||
|
):
|
||||||
|
return self._escolher_eixo_yaw_tracking(
|
||||||
|
delta_base,
|
||||||
|
float(x), float(y), float(theta),
|
||||||
|
v,
|
||||||
|
int(idx_base),
|
||||||
|
status_carro,
|
||||||
|
tipo_prev,
|
||||||
|
contexto=contexto,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
TipoMovimentoDirecional.RodasDianteiras,
|
||||||
|
delta_base,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --------------------------------------------------------------
|
||||||
|
# V15 legado abaixo: mantido para rollback/A-B com
|
||||||
|
# tracking_row_continuous_enabled=False.
|
||||||
|
# --------------------------------------------------------------
|
||||||
ff_deg_abs = abs(math.degrees(delta_ff))
|
ff_deg_abs = abs(math.degrees(delta_ff))
|
||||||
|
|
||||||
curva_ativa = bool(
|
curva_ativa = bool(
|
||||||
|
|
@ -2791,12 +3106,30 @@ class ControladorMPC:
|
||||||
|
|
||||||
if inversao_significativa:
|
if inversao_significativa:
|
||||||
filtrado = 0.0
|
filtrado = 0.0
|
||||||
|
else:
|
||||||
|
# V20: Dianteira/Traseira em CaminhandoRua precisam responder mais
|
||||||
|
# cedo que o filtro legado, mas continuam amortecidas. MovimentoArco
|
||||||
|
# permanece sem filtro, como antes.
|
||||||
|
usar_filtro_row = bool(
|
||||||
|
self._tracking_row_continuous_enabled
|
||||||
|
and _status_in(status_carro, [StatusCarroMapa.CaminhandoRua])
|
||||||
|
and tipo in [
|
||||||
|
TipoMovimentoDirecional.RodasDianteiras,
|
||||||
|
TipoMovimentoDirecional.RodasTraseiras,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if usar_filtro_row:
|
||||||
|
tau = float(self._tracking_row_filter_tau_s)
|
||||||
|
taxa_graus_s = float(self._tracking_row_rate_graus_s)
|
||||||
else:
|
else:
|
||||||
tau = float(self._filtro_angulo_reta_tau_s)
|
tau = float(self._filtro_angulo_reta_tau_s)
|
||||||
|
taxa_graus_s = float(self._taxa_angulo_reta_graus_s)
|
||||||
|
|
||||||
alpha = 1.0 if tau <= 1e-6 else dt / (tau + dt)
|
alpha = 1.0 if tau <= 1e-6 else dt / (tau + dt)
|
||||||
filtrado = anterior + alpha * (desejado - anterior)
|
filtrado = anterior + alpha * (desejado - anterior)
|
||||||
|
|
||||||
max_delta = np.radians(self._taxa_angulo_reta_graus_s) * dt
|
max_delta = np.radians(taxa_graus_s) * dt
|
||||||
filtrado = anterior + float(np.clip(
|
filtrado = anterior + float(np.clip(
|
||||||
filtrado - anterior,
|
filtrado - anterior,
|
||||||
-max_delta,
|
-max_delta,
|
||||||
|
|
@ -3267,11 +3600,11 @@ class ControladorMPC:
|
||||||
if idx_local > 0:
|
if idx_local > 0:
|
||||||
pontos_visitados[:idx_local] = True
|
pontos_visitados[:idx_local] = True
|
||||||
|
|
||||||
if havia_avanco_especulativo and not self._ultima_divergencia_visitados:
|
#if havia_avanco_especulativo and not self._ultima_divergencia_visitados:
|
||||||
_log(
|
# _log(
|
||||||
f"[MPC] Progresso especulativo descartado; "
|
# f"[MPC] Progresso especulativo descartado; "
|
||||||
f"C# autoritativo no idx_local={idx_local}."
|
# f"C# autoritativo no idx_local={idx_local}."
|
||||||
)
|
# )
|
||||||
|
|
||||||
self._ultima_divergencia_visitados = havia_avanco_especulativo
|
self._ultima_divergencia_visitados = havia_avanco_especulativo
|
||||||
|
|
||||||
|
|
@ -3906,6 +4239,8 @@ class ControladorMPC:
|
||||||
self._dubins_pp_ativo_ciclo_anterior = True
|
self._dubins_pp_ativo_ciclo_anterior = True
|
||||||
self._entry_capture_active = False
|
self._entry_capture_active = False
|
||||||
self._entry_capture_dist_m = 0.0
|
self._entry_capture_dist_m = 0.0
|
||||||
|
self._tracking_row_hybrid_active = False
|
||||||
|
self._tracking_row_hybrid_dist_m = 0.0
|
||||||
|
|
||||||
return self._comando_seguidor_dubins(
|
return self._comando_seguidor_dubins(
|
||||||
contexto=contexto,
|
contexto=contexto,
|
||||||
|
|
@ -3990,6 +4325,16 @@ class ControladorMPC:
|
||||||
f"dist={self._entry_capture_dist_m:.2f}m"
|
f"dist={self._entry_capture_dist_m:.2f}m"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# V21 - atualiza o latch hibrido uma unica vez com a pose REAL.
|
||||||
|
# A flag desligada preserva integralmente a V20 pura.
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
self._atualizar_tracking_row_hybrid_real(
|
||||||
|
x=x, y=y, theta=theta, velocidade=velocidade,
|
||||||
|
idx_base=idx_alvo_correcao, status_carro=status_carro,
|
||||||
|
contexto=contexto,
|
||||||
|
)
|
||||||
|
|
||||||
# -------------------- Planejamento: horizonte ESPACIAL fixo --------------------
|
# -------------------- Planejamento: horizonte ESPACIAL fixo --------------------
|
||||||
S_ALVO = float(self.horizonte)
|
S_ALVO = float(self.horizonte)
|
||||||
V_FLOOR = float(getattr(self, "_vmin_planejamento", 0.30))
|
V_FLOOR = float(getattr(self, "_vmin_planejamento", 0.30))
|
||||||
|
|
|
||||||
|
|
@ -236,54 +236,71 @@ class ContextoGlobalRedis:
|
||||||
@classmethod
|
@classmethod
|
||||||
def atualizar_ctx_dict(cls, chave: CtxKey, **novos_campos):
|
def atualizar_ctx_dict(cls, chave: CtxKey, **novos_campos):
|
||||||
"""
|
"""
|
||||||
Atualiza campos de um JSON salvo no Redis.
|
Atualiza campos de um JSON salvo no Redis sem perder writes concorrentes.
|
||||||
|
|
||||||
Suporta caminho aninhado:
|
O lock Python é apenas local ao processo. Como ManagerWorker,
|
||||||
atualizar_ctx_dict(CtxKey.DadosControle, "a__b"=1)
|
HealthWorker e os demais workers são processos separados, um simples
|
||||||
vira:
|
GET -> modifica -> SET pode apagar campos escritos por outro processo
|
||||||
{"a": {"b": 1}}
|
(ou pelo C#). WATCH/MULTI transforma a atualização em CAS otimista.
|
||||||
|
|
||||||
Também aceita caminho com ponto:
|
|
||||||
atualizar_ctx_dict(..., a__b=1)
|
|
||||||
ou internamente "a.b".
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import MutableMapping
|
from collections.abc import MutableMapping
|
||||||
|
|
||||||
def _set_nested(d: MutableMapping, path: str, value):
|
def _set_nested(d: MutableMapping, path: str, value):
|
||||||
keys = str(path).split(".")
|
keys = str(path).split(".")
|
||||||
atual = d
|
atual = d
|
||||||
|
|
||||||
for k in keys[:-1]:
|
for k in keys[:-1]:
|
||||||
if k not in atual or not isinstance(atual[k], dict):
|
if k not in atual or not isinstance(atual[k], dict):
|
||||||
atual[k] = {}
|
atual[k] = {}
|
||||||
atual = atual[k]
|
atual = atual[k]
|
||||||
|
|
||||||
atual[keys[-1]] = value
|
atual[keys[-1]] = value
|
||||||
|
|
||||||
try:
|
|
||||||
chave_str = cls._key(chave)
|
chave_str = cls._key(chave)
|
||||||
|
|
||||||
novos_campos = {
|
novos_campos = {
|
||||||
str(k).replace("__", "."): v
|
str(k).replace("__", "."): v
|
||||||
for k, v in novos_campos.items()
|
for k, v in novos_campos.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ultimo_erro = None
|
||||||
|
for tentativa in range(8):
|
||||||
|
try:
|
||||||
|
# Serializa writers do mesmo processo; WATCH protege contra
|
||||||
|
# writers de outros processos e clientes externos (C#).
|
||||||
with cls._update_lock:
|
with cls._update_lock:
|
||||||
dados = cls.get(chave_str, {})
|
with cls._redis.pipeline() as pipe:
|
||||||
|
pipe.watch(chave_str)
|
||||||
|
bruto = pipe.get(chave_str)
|
||||||
|
dados = cls._json_loads(bruto, default={})
|
||||||
if not isinstance(dados, dict):
|
if not isinstance(dados, dict):
|
||||||
dados = {}
|
dados = {}
|
||||||
|
|
||||||
for campo, valor in novos_campos.items():
|
for campo, valor in novos_campos.items():
|
||||||
_set_nested(dados, campo, valor)
|
_set_nested(dados, campo, valor)
|
||||||
|
|
||||||
return cls.set(chave_str, dados)
|
payload = cls._json_dumps(dados)
|
||||||
|
pipe.multi()
|
||||||
|
pipe.set(chave_str, payload)
|
||||||
|
pipe.execute()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except redis.WatchError as e:
|
||||||
|
ultimo_erro = e
|
||||||
|
time.sleep(min(0.025, 0.002 * (tentativa + 1)))
|
||||||
|
continue
|
||||||
|
except (redis.ConnectionError, redis.TimeoutError) as e:
|
||||||
|
ultimo_erro = e
|
||||||
|
cls._reconectar()
|
||||||
|
time.sleep(0.02 * (tentativa + 1))
|
||||||
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
mostrar_log(f"Erro ao atualizar_ctx_dict: {e} - Chave: {chave}")
|
mostrar_log(f"Erro ao atualizar_ctx_dict: {e} - Chave: {chave}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
mostrar_log(
|
||||||
|
f"Erro ao atualizar_ctx_dict após conflitos concorrentes: "
|
||||||
|
f"{ultimo_erro} - Chave: {chave}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def publicar_comando(cls, canal: CmdKey, dados):
|
def publicar_comando(cls, canal: CmdKey, dados):
|
||||||
try:
|
try:
|
||||||
|
|
@ -647,16 +664,27 @@ class ContextoGlobalRedis:
|
||||||
)
|
)
|
||||||
|
|
||||||
revisao_solicitada = cls._int(operacao.get("mapa_revisao_solicitada", 0), 0)
|
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_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)
|
qtd_publicada = len(pontos_publicados)
|
||||||
status_mapa = str(operacao.get("mapa_status", "") or "")
|
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_confirmado_flag = cls._bool(operacao.get("mapa_confirmado", False))
|
||||||
mapa_barreira_critica = cls._bool(operacao.get("mapa_barreira_critica", False))
|
mapa_barreira_critica = cls._bool(operacao.get("mapa_barreira_critica", False))
|
||||||
|
command_solicitado = str(operacao.get("mapa_command_id", "") or "")
|
||||||
|
|
||||||
|
# MAPSYNC V2: ACKs possuem dono único.
|
||||||
|
# HealthWorker nunca mais disputa DadosOperacao com o Manager;
|
||||||
|
# ManagerWorker nunca mais escreve ACK no JSON central da operação.
|
||||||
|
health_map = cls._dict(cls.get(CtxKey.DadosHealthWorker, {}))
|
||||||
|
manager_map = cls._dict(cls.get(CtxKey.DadosManagerWorker, {}))
|
||||||
|
|
||||||
|
health_cmd = str(health_map.get("ultimo_comando_mapa", "") or "")
|
||||||
|
health_rev = cls._int(health_map.get("mapa_revisao_aplicada", 0), 0)
|
||||||
|
health_status = str(health_map.get("mapa_status", "") or "")
|
||||||
|
|
||||||
|
manager_cmd = str(manager_map.get("mapa_command_id_aplicado", "") or "")
|
||||||
|
manager_rev = cls._int(manager_map.get("mapa_revisao_aplicada", 0), 0)
|
||||||
|
manager_qtd = cls._int(manager_map.get("mapa_quantidade_pontos_aplicada", 0), 0)
|
||||||
|
manager_status = str(manager_map.get("mapa_status", "") or "")
|
||||||
|
|
||||||
primeiro_esperado = operacao.get("mapa_primeiro_idx_esperado", None)
|
primeiro_esperado = operacao.get("mapa_primeiro_idx_esperado", None)
|
||||||
ultimo_esperado = operacao.get("mapa_ultimo_idx_esperado", None)
|
ultimo_esperado = operacao.get("mapa_ultimo_idx_esperado", None)
|
||||||
|
|
@ -691,13 +719,17 @@ class ContextoGlobalRedis:
|
||||||
not mapa_barreira_critica
|
not mapa_barreira_critica
|
||||||
and mapa_confirmado_flag
|
and mapa_confirmado_flag
|
||||||
and status_mapa == "aplicado"
|
and status_mapa == "aplicado"
|
||||||
and status_mpc == "aplicado"
|
|
||||||
and revisao_solicitada > 0
|
and revisao_solicitada > 0
|
||||||
and revisao_solicitada == revisao_processada == revisao_mpc
|
and health_rev == revisao_solicitada
|
||||||
|
and manager_rev == revisao_solicitada
|
||||||
|
and command_solicitado != ""
|
||||||
|
and health_cmd == command_solicitado
|
||||||
|
and manager_cmd == command_solicitado
|
||||||
|
and health_status == "aplicado"
|
||||||
|
and manager_status == "aplicado"
|
||||||
and qtd_esperada > 0
|
and qtd_esperada > 0
|
||||||
and qtd_publicada == qtd_esperada
|
and qtd_publicada == qtd_esperada
|
||||||
and qtd_processada == qtd_esperada
|
and manager_qtd == qtd_esperada
|
||||||
and qtd_mpc == qtd_esperada
|
|
||||||
and indices_extremos_ok
|
and indices_extremos_ok
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -705,9 +737,10 @@ class ContextoGlobalRedis:
|
||||||
if not mapa_mpc_ok:
|
if not mapa_mpc_ok:
|
||||||
motivos.append(
|
motivos.append(
|
||||||
"Mapa/MPC ainda não confirmado: "
|
"Mapa/MPC ainda não confirmado: "
|
||||||
f"rev solicitada/processada/mpc={revisao_solicitada}/{revisao_processada}/{revisao_mpc}; "
|
f"rev solicitada/health/manager={revisao_solicitada}/{health_rev}/{manager_rev}; "
|
||||||
f"pontos publicados/esperados/processados/mpc={qtd_publicada}/{qtd_esperada}/{qtd_processada}/{qtd_mpc}; "
|
f"cmd solicitado/health/manager={command_solicitado}/{health_cmd}/{manager_cmd}; "
|
||||||
f"status={status_mapa}/{status_mpc}"
|
f"pontos publicados/esperados/manager={qtd_publicada}/{qtd_esperada}/{manager_qtd}; "
|
||||||
|
f"status={status_mapa}/{health_status}/{manager_status}"
|
||||||
)
|
)
|
||||||
|
|
||||||
detalhes_regras["mapa_mpc"] = {
|
detalhes_regras["mapa_mpc"] = {
|
||||||
|
|
@ -716,14 +749,17 @@ class ContextoGlobalRedis:
|
||||||
"confirmado": mapa_confirmado_flag,
|
"confirmado": mapa_confirmado_flag,
|
||||||
"barreira_critica": mapa_barreira_critica,
|
"barreira_critica": mapa_barreira_critica,
|
||||||
"status_mapa": status_mapa,
|
"status_mapa": status_mapa,
|
||||||
"status_mpc": status_mpc,
|
"status_health": health_status,
|
||||||
|
"status_manager": manager_status,
|
||||||
"revisao_solicitada": revisao_solicitada,
|
"revisao_solicitada": revisao_solicitada,
|
||||||
"revisao_processada": revisao_processada,
|
"revisao_health": health_rev,
|
||||||
"revisao_mpc": revisao_mpc,
|
"revisao_manager": manager_rev,
|
||||||
|
"command_solicitado": command_solicitado,
|
||||||
|
"command_health": health_cmd,
|
||||||
|
"command_manager": manager_cmd,
|
||||||
"pontos_publicados": qtd_publicada,
|
"pontos_publicados": qtd_publicada,
|
||||||
"pontos_esperados": qtd_esperada,
|
"pontos_esperados": qtd_esperada,
|
||||||
"pontos_processados": qtd_processada,
|
"pontos_manager": manager_qtd,
|
||||||
"pontos_mpc": qtd_mpc,
|
|
||||||
"primeiro_idx_publicado": primeiro_publicado,
|
"primeiro_idx_publicado": primeiro_publicado,
|
||||||
"ultimo_idx_publicado": ultimo_publicado,
|
"ultimo_idx_publicado": ultimo_publicado,
|
||||||
"primeiro_idx_esperado": primeiro_esperado,
|
"primeiro_idx_esperado": primeiro_esperado,
|
||||||
|
|
@ -1293,7 +1329,8 @@ class ContextoGlobalRedis:
|
||||||
operacao.get("inicializacao_operacao_executada", False)
|
operacao.get("inicializacao_operacao_executada", False)
|
||||||
)
|
)
|
||||||
if not inicializacao_executada:
|
if not inicializacao_executada:
|
||||||
cls._iniciar_operacao()
|
inicio_emitido = cls._iniciar_operacao()
|
||||||
|
if inicio_emitido:
|
||||||
updates_base["tempo_aguardando"] = agora
|
updates_base["tempo_aguardando"] = agora
|
||||||
updates_base["ja_entrou_em_andamento"] = False
|
updates_base["ja_entrou_em_andamento"] = False
|
||||||
updates_base["inicializacao_operacao_executada"] = True
|
updates_base["inicializacao_operacao_executada"] = True
|
||||||
|
|
@ -1488,17 +1525,84 @@ class ContextoGlobalRedis:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _iniciar_operacao(cls):
|
def _iniciar_operacao(cls):
|
||||||
|
"""
|
||||||
|
Inicia a execução somente depois de o mapa MPC estar confirmado.
|
||||||
|
|
||||||
|
MAPSYNC V3:
|
||||||
|
- evita o antigo envio de um mapa com cmd/rev=None antes da barreira;
|
||||||
|
- o mapa já foi carregado pela transação crítica;
|
||||||
|
- aqui enviamos apenas o comando de INÍCIO para a instância confirmada.
|
||||||
|
|
||||||
|
Retorna True quando o comando de início foi efetivamente emitido.
|
||||||
|
"""
|
||||||
|
operacao = cls._dict(cls.get_operacao())
|
||||||
|
dados_dir = cls._dict(operacao.get("Dir", {}))
|
||||||
|
tipo_controle = cls._int(
|
||||||
|
dados_dir.get(
|
||||||
|
"tipo_controle",
|
||||||
|
TiposControladorDirecional.Manual.value,
|
||||||
|
),
|
||||||
|
TiposControladorDirecional.Manual.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
if tipo_controle == TiposControladorDirecional.MPC.value:
|
||||||
|
revisao = cls._int(operacao.get("mapa_revisao_solicitada", 0), 0)
|
||||||
|
command_id = str(operacao.get("mapa_command_id", "") or "")
|
||||||
|
qtd_esperada = cls._int(
|
||||||
|
operacao.get("mapa_quantidade_pontos_esperada", 0), 0
|
||||||
|
)
|
||||||
|
|
||||||
|
health = cls._dict(cls.get(CtxKey.DadosHealthWorker, {}))
|
||||||
|
manager = cls._dict(cls.get(CtxKey.DadosManagerWorker, {}))
|
||||||
|
|
||||||
|
health_ok = (
|
||||||
|
command_id != ""
|
||||||
|
and str(health.get("ultimo_comando_mapa", "") or "") == command_id
|
||||||
|
and cls._int(health.get("mapa_revisao_aplicada", 0), 0) == revisao
|
||||||
|
and str(health.get("mapa_status", "") or "") == "aplicado"
|
||||||
|
)
|
||||||
|
|
||||||
|
manager_ok = (
|
||||||
|
command_id != ""
|
||||||
|
and str(manager.get("mapa_command_id_aplicado", "") or "") == command_id
|
||||||
|
and cls._int(manager.get("mapa_revisao_aplicada", 0), 0) == revisao
|
||||||
|
and cls._int(manager.get("mapa_quantidade_pontos_aplicada", 0), 0) == qtd_esperada
|
||||||
|
and str(manager.get("mapa_status", "") or "") == "aplicado"
|
||||||
|
)
|
||||||
|
|
||||||
|
mapa_ok = (
|
||||||
|
revisao > 0
|
||||||
|
and qtd_esperada > 0
|
||||||
|
and cls._bool(operacao.get("mapa_confirmado", False))
|
||||||
|
and not cls._bool(operacao.get("mapa_barreira_critica", False))
|
||||||
|
and str(operacao.get("mapa_status", "") or "") == "aplicado"
|
||||||
|
and health_ok
|
||||||
|
and manager_ok
|
||||||
|
)
|
||||||
|
|
||||||
|
if not mapa_ok:
|
||||||
|
return False
|
||||||
|
|
||||||
cls.publicar_comando(
|
cls.publicar_comando(
|
||||||
CmdKey.WeedWorkerRx,
|
CmdKey.WeedWorkerRx,
|
||||||
{"cmd": WeedWorkerCommandType.ReiniciarDeteccoes.value},
|
{"cmd": WeedWorkerCommandType.ReiniciarDeteccoes.value},
|
||||||
)
|
)
|
||||||
|
|
||||||
cls._atualizar_mpc(do_manager=False, iniciar_operacao=True)
|
# O mapa já está confirmado no Manager. Não reenviamos params/mapa aqui.
|
||||||
|
# O comando sem params apenas inicia a instância existente.
|
||||||
|
iniciado_enviado = cls.publicar_comando(
|
||||||
|
CmdKey.ManagerWorkerRx,
|
||||||
|
{"cmd": ManagerWorkerCommandType.IniciarMPC.value},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not iniciado_enviado:
|
||||||
|
return False
|
||||||
|
|
||||||
cls.atualizar_ctx_dict(
|
cls.atualizar_ctx_dict(
|
||||||
CtxKey.DadosOperacao,
|
CtxKey.DadosOperacao,
|
||||||
tempo_aguardando=time.time(),
|
tempo_aguardando=time.time(),
|
||||||
)
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _finalizar_operacao(cls):
|
def _finalizar_operacao(cls):
|
||||||
|
|
@ -1749,8 +1853,6 @@ class ContextoGlobalRedis:
|
||||||
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_quantidade_pontos_processada=0,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not atualizado:
|
if not atualizado:
|
||||||
|
|
@ -1760,10 +1862,125 @@ class ContextoGlobalRedis:
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# 5. Enviar EXATAMENTE este mapa ao Manager
|
# 5/6. Entrega confiável Health -> Manager
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
# Redis Pub/Sub é efêmero. Se uma publicação cair exatamente
|
||||||
|
# durante uma janela ruim do listener, ela não fica enfileirada.
|
||||||
|
#
|
||||||
|
# Em vez de esperar 4 s e pedir ao C# um NOVO command_id, o Health
|
||||||
|
# retransmite o MESMO snapshot com o MESMO command_id/revisão.
|
||||||
|
# O Manager V3 é idempotente: replays da mesma revisão não
|
||||||
|
# reinicializam o MPC desnecessariamente e apenas reafirmam o ACK.
|
||||||
|
intervalo_reenvio_s = 0.35
|
||||||
|
timeout_total_s = 5.00
|
||||||
|
timeout_aplicando_s = 3.00
|
||||||
|
poll_s = 0.05
|
||||||
|
|
||||||
|
inicio_entrega = time.monotonic()
|
||||||
|
limite_total = inicio_entrega + timeout_total_s
|
||||||
|
proximo_envio_em = inicio_entrega
|
||||||
|
numero_envio = 0
|
||||||
|
aplicado_mpc = False
|
||||||
|
aplicando_desde = None
|
||||||
|
|
||||||
|
def _estado_manager_mapsync():
|
||||||
|
estado = cls._dict(
|
||||||
|
cls.get(CtxKey.DadosManagerWorker, {})
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"cmd": str(
|
||||||
|
estado.get("mapa_command_id_aplicado", "") or ""
|
||||||
|
),
|
||||||
|
"rev": cls._int(
|
||||||
|
estado.get("mapa_revisao_aplicada", 0), 0
|
||||||
|
),
|
||||||
|
"qtd": cls._int(
|
||||||
|
estado.get("mapa_quantidade_pontos_aplicada", 0), 0
|
||||||
|
),
|
||||||
|
"status": str(
|
||||||
|
estado.get("mapa_status", "") or ""
|
||||||
|
),
|
||||||
|
"erro": str(
|
||||||
|
estado.get("mapa_erro", "") or ""
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
while time.monotonic() < limite_total:
|
||||||
|
agora_mono = time.monotonic()
|
||||||
|
mgr = _estado_manager_mapsync()
|
||||||
|
|
||||||
|
mesma_revisao = (
|
||||||
|
mgr["rev"] == cls._int(map_revision, 0)
|
||||||
|
and mgr["qtd"] == qtd_esperada
|
||||||
|
)
|
||||||
|
mesmo_comando = (
|
||||||
|
mgr["cmd"] == str(command_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# Manager recebeu a revisão e já está construindo o MPC.
|
||||||
|
# Nesse estado NÃO retransmitimos. Damos uma janela curta
|
||||||
|
# para finalizar e escrever o ACK, evitando fila de replays.
|
||||||
|
# Aceitamos "aplicando" da mesma revisão mesmo se o cmd for
|
||||||
|
# de uma tentativa anterior; ao concluir, o replay corrente
|
||||||
|
# fará rebind idempotente do command_id.
|
||||||
|
# ------------------------------------------------------
|
||||||
|
if mesma_revisao and mgr["status"] == "aplicando":
|
||||||
|
if aplicando_desde is None:
|
||||||
|
aplicando_desde = agora_mono
|
||||||
mostrar_log(
|
mostrar_log(
|
||||||
f"[MAPSYNC] enviando mapa ao Manager | "
|
f"[MAPSYNC/TX-MANAGER] Manager aplicando; "
|
||||||
|
f"retransmissão pausada | cmd={command_id} | "
|
||||||
|
f"cmd_manager={mgr['cmd']} | rev={map_revision} | "
|
||||||
|
f"pontos={qtd_esperada}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if agora_mono - aplicando_desde > timeout_aplicando_s:
|
||||||
|
return {
|
||||||
|
"sucesso": False,
|
||||||
|
"erro": (
|
||||||
|
f"Manager permaneceu em 'aplicando' por mais de "
|
||||||
|
f"{timeout_aplicando_s:.1f}s | "
|
||||||
|
f"cmd={command_id} | rev={map_revision} | "
|
||||||
|
f"pontos={qtd_esperada}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
time.sleep(poll_s)
|
||||||
|
continue
|
||||||
|
|
||||||
|
aplicando_desde = None
|
||||||
|
|
||||||
|
if mesma_revisao and mgr["status"] == "erro" and mesmo_comando:
|
||||||
|
return {
|
||||||
|
"sucesso": False,
|
||||||
|
"erro": (
|
||||||
|
"Manager rejeitou explicitamente o snapshot | "
|
||||||
|
f"cmd={command_id} | rev={map_revision} | "
|
||||||
|
f"erro={mgr['erro']}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if mesma_revisao and mgr["status"] == "aplicado":
|
||||||
|
if mesmo_comando:
|
||||||
|
aplicado_mpc = True
|
||||||
|
mostrar_log(
|
||||||
|
f"[MAPSYNC/TX-MANAGER] ACK confirmado | "
|
||||||
|
f"envios={numero_envio} | cmd={command_id} | "
|
||||||
|
f"rev={map_revision} | pontos={qtd_esperada}"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Mesma revisão já aplicada sob command_id anterior.
|
||||||
|
# Enviamos o command atual imediatamente para o Manager
|
||||||
|
# executar apenas o rebind idempotente do ACK.
|
||||||
|
proximo_envio_em = min(proximo_envio_em, agora_mono)
|
||||||
|
|
||||||
|
if agora_mono >= proximo_envio_em:
|
||||||
|
numero_envio += 1
|
||||||
|
|
||||||
|
mostrar_log(
|
||||||
|
f"[MAPSYNC/TX-MANAGER] envio={numero_envio} | "
|
||||||
f"cmd={command_id} | rev={map_revision} | "
|
f"cmd={command_id} | rev={map_revision} | "
|
||||||
f"pontos={qtd_esperada}"
|
f"pontos={qtd_esperada}"
|
||||||
)
|
)
|
||||||
|
|
@ -1784,35 +2001,28 @@ class ContextoGlobalRedis:
|
||||||
"erro": "Falha ao enviar mapa ao Manager/MPC",
|
"erro": "Falha ao enviar mapa ao Manager/MPC",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==========================================================
|
proximo_envio_em = agora_mono + intervalo_reenvio_s
|
||||||
# 6. Esperar Manager confirmar que realmente carregou
|
|
||||||
# ==========================================================
|
time.sleep(poll_s)
|
||||||
aplicado_mpc = cls._aguardar_mapa_mpc_aplicado(
|
|
||||||
command_id=command_id,
|
|
||||||
map_revision=map_revision,
|
|
||||||
expected_point_count=qtd_esperada,
|
|
||||||
timeout_s=2.5,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not aplicado_mpc:
|
if not aplicado_mpc:
|
||||||
|
mgr = _estado_manager_mapsync()
|
||||||
return {
|
return {
|
||||||
"sucesso": False,
|
"sucesso": False,
|
||||||
"erro": (
|
"erro": (
|
||||||
f"Manager não confirmou aplicação exata do mapa | "
|
f"Manager não confirmou aplicação exata do mapa após "
|
||||||
|
f"{numero_envio} envio(s) e {timeout_total_s:.1f}s | "
|
||||||
f"cmd={command_id} | rev={map_revision} | "
|
f"cmd={command_id} | rev={map_revision} | "
|
||||||
f"pontos={qtd_esperada}"
|
f"pontos={qtd_esperada} | "
|
||||||
|
f"manager_status={mgr['status']} | "
|
||||||
|
f"manager_cmd={mgr['cmd']} | manager_rev={mgr['rev']}"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# 7. Só agora considerar revisão processada
|
# 7. ACK do Health será publicado pelo próprio HealthWorker.
|
||||||
|
# Não espelhamos estado transacional em DadosOperacao.
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
cls.atualizar_ctx_dict(
|
|
||||||
CtxKey.DadosOperacao,
|
|
||||||
mapa_revisao_processada=map_revision,
|
|
||||||
mapa_quantidade_pontos_processada=len(pontos_info),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"sucesso": True,
|
"sucesso": True,
|
||||||
"quantidade": len(pontos_info),
|
"quantidade": len(pontos_info),
|
||||||
|
|
@ -1918,36 +2128,20 @@ class ContextoGlobalRedis:
|
||||||
command_id,
|
command_id,
|
||||||
map_revision,
|
map_revision,
|
||||||
expected_point_count=None,
|
expected_point_count=None,
|
||||||
timeout_s=2.5,
|
timeout_s=4.0,
|
||||||
):
|
):
|
||||||
|
"""Espera o ACK de propriedade exclusiva do ManagerWorker."""
|
||||||
limite = time.time() + max(0.1, float(timeout_s))
|
limite = time.time() + max(0.1, float(timeout_s))
|
||||||
|
|
||||||
while time.time() < limite:
|
while time.time() < limite:
|
||||||
operacao = cls.get_operacao()
|
manager = cls._dict(cls.get(CtxKey.DadosManagerWorker, {}))
|
||||||
|
|
||||||
cmd_aplicado = str(
|
|
||||||
operacao.get(
|
|
||||||
"mapa_command_id_mpc_aplicado",
|
|
||||||
""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
revisao_aplicada = operacao.get(
|
|
||||||
"mapa_revisao_mpc_aplicada",
|
|
||||||
None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
cmd_aplicado = str(manager.get("mapa_command_id_aplicado", "") or "")
|
||||||
|
revisao_aplicada = manager.get("mapa_revisao_aplicada", None)
|
||||||
qtd_aplicada = cls._int(
|
qtd_aplicada = cls._int(
|
||||||
operacao.get("mapa_quantidade_pontos_mpc_aplicada", 0),
|
manager.get("mapa_quantidade_pontos_aplicada", 0), 0
|
||||||
0,
|
|
||||||
)
|
|
||||||
|
|
||||||
status = str(
|
|
||||||
operacao.get(
|
|
||||||
"mapa_status_mpc",
|
|
||||||
""
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
status = str(manager.get("mapa_status", "") or "")
|
||||||
|
|
||||||
quantidade_ok = (
|
quantidade_ok = (
|
||||||
expected_point_count is None
|
expected_point_count is None
|
||||||
|
|
@ -1963,10 +2157,7 @@ class ContextoGlobalRedis:
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if (
|
if cmd_aplicado == str(command_id) and status == "erro":
|
||||||
cmd_aplicado == str(command_id)
|
|
||||||
and status == "erro"
|
|
||||||
):
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue