ajustado calculo de vazao por fluxo, protocolo daly bms, protocolo oid, can service robusto

This commit is contained in:
Diego Freitas 2026-06-22 16:09:41 -03:00
parent 824f2bad58
commit 92a845aec0
7 changed files with 2149 additions and 575 deletions

View File

@ -155,15 +155,13 @@ namespace AgroBase.Models.Modules
{
get
{
double direta = VazaoFluxoMLpSInstantanea;
if (direta > 0)
return direta;
return VazaoFluxoMLpSInstantaneaCalc;
return ObterVazaoAtualConfiavelMLs(DateTime.Now);
}
}
[JsonIgnore]
public double VazaoMaximaSensor_mLs => VazaoMaximaSensor_LMin * 1000.0 / 60.0;
public double VazaoMediaMLs { get; set; }
public double VolumeVazadoML { get; set; }
public double TempoAtuado { get; set; }
@ -226,25 +224,38 @@ namespace AgroBase.Models.Modules
{
var sensor = SensorFluxoLinha;
if (sensor == null || !sensor.Inicializado || sensor.ValoresLeituras == null)
if (sensor == null ||
!sensor.Inicializado ||
sensor.ValoresLeituras == null)
{
return 0;
}
var leituraFluxo = sensor.ValoresLeituras.FirstOrDefault(x => x.funcao == FuncoesPinout.Fluxo);
double maximaMLs =
VazaoMaximaSensor_LMin *
1000.0 /
60.0;
var leituraFluxo =
sensor.ValoresLeituras.FirstOrDefault(
x => x.funcao == FuncoesPinout.Fluxo
);
/*
* A leitura direta é prioritária, inclusive quando vale zero.
* Zero recente é uma medição válida, não motivo para usar
* pulsos antigos como fallback.
* IMPORTANTE:
* A leitura direta FuncoesPinout.Fluxo está em mL/s no modelo.
* Não converter novamente de L/min para mL/s.
*
* Zero recente é válido. Se a linha está sem fluxo, o sensor deve
* poder dizer zero sem cair no fallback por pulso antigo.
*/
if (LeituraSensorRecente(leituraFluxo, agora))
{
double vazaoLMin;
double vazaoMLs;
try
{
vazaoLMin =
vazaoMLs =
Convert.ToDouble(
leituraFluxo.atual.valor
);
@ -254,25 +265,23 @@ namespace AgroBase.Models.Modules
return 0;
}
if (double.IsNaN(vazaoLMin) ||
double.IsInfinity(vazaoLMin) ||
vazaoLMin < 0)
if (double.IsNaN(vazaoMLs) ||
double.IsInfinity(vazaoMLs) ||
vazaoMLs < 0)
{
return 0;
}
vazaoLMin = FuncoesMatematicas.Clamp(
vazaoLMin,
return FuncoesMatematicas.Clamp(
vazaoMLs,
0.0,
VazaoMaximaSensor_LMin
maximaMLs
);
return vazaoLMin * 1000.0 / 60.0;
}
/*
* Somente usa o cálculo por pulsos se os dois valores
* necessários também forem recentes.
* Fallback por pulso:
* aqui sim a fórmula calcula q em L/min e depois converte para mL/s.
*/
var leituraPulsos =
sensor.ValoresLeituras.FirstOrDefault(
@ -300,11 +309,6 @@ namespace AgroBase.Models.Modules
return 0;
}
double maximaMLs =
VazaoMaximaSensor_LMin *
1000.0 /
60.0;
return FuncoesMatematicas.Clamp(
calculadaMLs,
0.0,
@ -639,13 +643,10 @@ namespace AgroBase.Models.Modules
volumeIntervaloML /
bicosDoIntervalo.Count;
foreach (var bico in
bicosDoIntervalo)
foreach (var bico in bicosDoIntervalo)
{
bico.TempoComFluxo += dt;
bico.VolumeVazadoML +=
volumePorBicoML;
bico.VolumeVazadoML += volumePorBicoML;
}
}

View File

@ -258,7 +258,8 @@ namespace AgroBase.Models
{
public byte EnderecoTx { get; set; }
public byte EnderecoRx { get; set; }
public bool Iniciado
public bool Iniciado { get; set; }
public bool Iniciado_old
{
get
{

View File

@ -129,6 +129,61 @@ namespace AgroBase.Services
return $"{Id}_{data[0]}_{parametro}";
}
public static string GerarChaveCorrelacao(T_Code dispositivo, uint idTx, uint idRx, byte funcCodeTx, byte funcCodeRx, byte[] dataTx)
{
dataTx = dataTx ?? new byte[0];
switch (dispositivo)
{
case T_Code.Bat:
/*
* Daly usa DataID no CAN ID estendido.
* Então o idRx diferencia 0x90, 0x91, 0x95...
*/
return
$"{dispositivo}|tx:{IdKey(idTx)}|rx:{IdKey(idRx)}";
case T_Code.Oid:
/*
* OID consulta:
* TX/RX DATA[0] = 0x0F
* DATA[1] = parâmetro.
*/
return
$"{dispositivo}|tx:{IdKey(idTx)}|rx:{IdKey(idRx)}|fc:0x{funcCodeRx:X2}|param:{ByteKey(dataTx, 1)}";
case T_Code.Mks:
return
$"{dispositivo}|tx:{IdKey(idTx)}|rx:{IdKey(idRx)}|fc:0x{funcCodeRx:X2}";
case T_Code.Atu:
case T_Code.Sen:
/*
* Protocolo dos módulos usa posição/funcCode e normalmente
* ID_Num em DATA[1].
*/
return
$"{dispositivo}|tx:{IdKey(idTx)}|rx:{IdKey(idRx)}|fc:0x{funcCodeRx:X2}|id_num:{ByteKey(dataTx, 1)}";
default:
return
$"{dispositivo}|tx:{IdKey(idTx)}|rx:{IdKey(idRx)}|fc:0x{funcCodeRx:X2}|p1:{ByteKey(dataTx, 1)}";
}
}
private static string ByteKey(byte[] data, int index)
{
if (data == null || data.Length <= index)
return "--";
return "0x" + data[index].ToString("X2");
}
public static string IdKey(uint id)
{
return $"0x{id:X}";
}
}
public class CanMessage

View File

@ -107,7 +107,7 @@ namespace AgroBase.Services
{
return MensagensPendentes
.Where(x => x.ComResposta && x.Enviado && !x.Respondido)
.GroupBy(x => IdKey(x.IdRx))
.GroupBy(x => CanManager.IdKey(x.IdRx))
.ToDictionary(g => g.Key, g => g.Count());
}
}
@ -139,6 +139,33 @@ namespace AgroBase.Services
private DateTime _inicioJanelaLatencia = DateTime.UtcNow;
private double _latenciaMaxJanelaMs = 0;
private readonly ConcurrentDictionary<string, DateTime> _cooldownPorCanal =
new ConcurrentDictionary<string, DateTime>();
private readonly ConcurrentDictionary<string, DateTime> _ultimaFilaPorChave =
new ConcurrentDictionary<string, DateTime>();
private static readonly TimeSpan COOLDOWN_DEFAULT =
TimeSpan.FromMilliseconds(0);
private static readonly TimeSpan COOLDOWN_MKS =
TimeSpan.FromMilliseconds(20);
private static readonly TimeSpan COOLDOWN_OID =
TimeSpan.FromMilliseconds(8);
private static readonly TimeSpan COOLDOWN_BAT =
TimeSpan.FromMilliseconds(80);
private static readonly TimeSpan COOLDOWN_ATU_SEN =
TimeSpan.FromMilliseconds(4);
private static readonly TimeSpan DEDUPE_JANELA_GET =
TimeSpan.FromMilliseconds(250);
private static readonly TimeSpan DEDUPE_JANELA_SET =
TimeSpan.FromMilliseconds(60);
private static long NowUnixMs()
{
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
@ -340,6 +367,16 @@ namespace AgroBase.Services
if (dados.Count > 8)
throw new InvalidOperationException($"Payload CAN maior que 8 bytes. ID=0x{idTx:X}, Len={dados.Count}");
string chaveCorrelacao =
CanManager.GerarChaveCorrelacao(
Dispositivo,
idTx,
idRx,
funcCodeTx,
funcCodeRx,
dados.ToArray()
);
var novaMensagem = new CanMessage
{
Momento = DateTime.Now,
@ -350,25 +387,70 @@ namespace AgroBase.Services
funcCodeRx = funcCodeRx,
DataTx = dados.ToArray(),
ComResposta = get,
ChaveInterna = CanManager.GerarChaveMensagem(Dispositivo, idRx, dados.ToArray()),
ChaveInterna = chaveCorrelacao,
ContabilizarTimeoutHealth = contabilizarTimeoutHealth,
MensagemDescoberta = mensagemDescoberta
};
lock (_LockMensagens)
{
bool mensagemNaFila = MensagensPendentes.Any(x =>
(!x.Enviado || (x.ComResposta && x.Enviado && !x.Respondido)) &&
x.IdTx == idTx &&
x.IdRx == idRx &&
x.DataTx != null &&
x.DataTx.SequenceEqual(dados.ToArray()));
DateTime agora =
DateTime.Now;
if (!mensagemNaFila)
/*
* Para request com resposta, não deixa duas pendências iguais
* ficarem esperando a mesma resposta. Isso é vital para OID, porque
* todas as consultas respondem com funcCode 0x0F e diferem pelo param.
*/
bool mesmaPendenciaAberta =
MensagensPendentes.Any(x =>
x.ComResposta &&
!x.Respondido &&
x.ChaveInterna == chaveCorrelacao);
if (get && mesmaPendenciaAberta)
return;
/*
* Para comando sem resposta, evita empilhar comandos idênticos
* ainda não enviados. Depois de enviado, ele será limpo normalmente.
*/
bool comandoIdenticoNaoEnviado =
MensagensPendentes.Any(x =>
!x.ComResposta &&
!x.Enviado &&
x.IdTx == idTx &&
x.DataTx != null &&
x.DataTx.SequenceEqual(dados.ToArray()));
if (!get && comandoIdenticoNaoEnviado)
return;
/*
* Dedupe temporal extra, útil para comandos de controle que chegam
* repetidos por loop rápido.
*/
TimeSpan janela =
get
? DEDUPE_JANELA_GET
: DEDUPE_JANELA_SET;
if (_ultimaFilaPorChave.TryGetValue(
chaveCorrelacao,
out DateTime ultima) &&
agora - ultima < janela)
{
MensagensPendentes.Add(novaMensagem);
MostrarLog($"Mensagem adicionada. ID=0x{idTx:X}, Data={BitConverter.ToString(novaMensagem.DataTx)}", idTx);
return;
}
_ultimaFilaPorChave[chaveCorrelacao] = agora;
MensagensPendentes.Add(novaMensagem);
MostrarLog(
$"Mensagem adicionada. ID=0x{idTx:X}, Data={BitConverter.ToString(novaMensagem.DataTx)}",
idTx
);
}
}
@ -402,23 +484,62 @@ namespace AgroBase.Services
}
private readonly ConcurrentDictionary<uint, DateTime> _cooldownPorId = new ConcurrentDictionary<uint, DateTime>();
private static readonly TimeSpan COOLDOWN_MKS = TimeSpan.FromMilliseconds(20);
private bool PodeEnviarAgora(CanMessage m)
{
if (m.Dispositivo != T_Code.Mks)
if (m == null)
return false;
TimeSpan cooldown =
ObterCooldownDispositivo(m.Dispositivo);
if (cooldown <= TimeSpan.Zero)
return true;
var now = DateTime.UtcNow;
var due = _cooldownPorId.GetOrAdd(m.IdTx, DateTime.MinValue);
string chave =
$"{m.Dispositivo}|tx:{CanManager.IdKey(m.IdTx)}";
var now =
DateTime.UtcNow;
var due =
_cooldownPorCanal.GetOrAdd(
chave,
DateTime.MinValue
);
if (now < due)
return false;
_cooldownPorId[m.IdTx] = now + COOLDOWN_MKS;
_cooldownPorCanal[chave] =
now + cooldown;
return true;
}
private static TimeSpan ObterCooldownDispositivo(T_Code dispositivo)
{
switch (dispositivo)
{
case T_Code.Mks:
return COOLDOWN_MKS;
case T_Code.Oid:
return COOLDOWN_OID;
case T_Code.Bat:
return COOLDOWN_BAT;
case T_Code.Atu:
case T_Code.Sen:
return COOLDOWN_ATU_SEN;
default:
return COOLDOWN_DEFAULT;
}
}
private async Task EnviarCAN()
{
if (!IsConnected) return;
@ -498,7 +619,7 @@ namespace AgroBase.Services
}
_tcpStream.Write(packet, 0, packet.Length);
_tcpStream.Flush();
//_tcpStream.Flush();
}
else
{
@ -686,29 +807,17 @@ namespace AgroBase.Services
lock (_LockMensagens)
{
mensagem = MensagensPendentes.FirstOrDefault(x =>
{
if (!x.Enviado) return false;
if (x.Respondido) return false;
if (x.Dispositivo != dispositivo) return false;
if (x.IdRx != id) return false;
if (x.DataTx == null || x.DataTx.Length == 0) return false;
if (db)
return true;
// MKS: a resposta deve casar pelo function code.
// TX: [code, ..., crc]
// RX: [code, ..., crc]
if (x.Dispositivo == T_Code.Mks)
return data.Length > 0 && x.funcCodeRx == data[0];
// Demais protocolos antigos
if (data.Length < 2 || x.DataTx.Length < 2)
return true;
return x.DataTx[1] == data[1];
});
mensagem =
MensagensPendentes
.Where(x =>
MensagemCorrespondeResposta(
x,
dispositivo,
id,
data
))
.OrderBy(x => x.EnviadoEm)
.FirstOrDefault();
}
if (mensagem == null)
@ -745,6 +854,112 @@ namespace AgroBase.Services
}
}
private bool MensagemCorrespondeResposta(CanMessage pendente, T_Code dispositivoResposta, uint idResposta, byte[] dataResposta)
{
if (pendente == null)
return false;
if (!pendente.Enviado)
return false;
if (pendente.Respondido)
return false;
if (!pendente.ComResposta)
return false;
if (pendente.Dispositivo != dispositivoResposta)
return false;
if (pendente.IdRx != idResposta)
return false;
byte[] dataTx =
pendente.DataTx ?? new byte[0];
byte[] dataRx =
dataResposta ?? new byte[0];
switch (pendente.Dispositivo)
{
case T_Code.Bat:
/*
* Daly: DataID vem no CAN ID, então idRx fechou.
*/
return true;
case T_Code.Mks:
return
dataRx.Length > 0 &&
pendente.funcCodeRx == dataRx[0];
case T_Code.Oid:
/*
* OID:
* consulta retorna DATA[0]=0x0F e DATA[1]=param.
*/
return
dataRx.Length >= 2 &&
dataTx.Length >= 2 &&
dataRx[0] == pendente.funcCodeRx &&
dataRx[1] == dataTx[1];
case T_Code.Atu:
case T_Code.Sen:
/*
* ATU/SEN:
* casa pelo func/posição e, quando existir, pelo ID_Num.
*/
if (dataRx.Length <= 0)
return false;
if (dataRx[0] != pendente.funcCodeRx)
return false;
if (dataRx.Length >= 2 && dataTx.Length >= 2)
return dataRx[1] == dataTx[1];
return true;
default:
if (dataRx.Length <= 0)
return false;
if (dataRx[0] != pendente.funcCodeRx)
return false;
if (dataRx.Length >= 2 && dataTx.Length >= 2)
return dataRx[1] == dataTx[1];
return true;
}
}
private static TimeSpan TimeoutResposta(CanMessage msg)
{
if (msg == null)
return TimeSpan.FromSeconds(2);
switch (msg.Dispositivo)
{
case T_Code.Oid:
return TimeSpan.FromMilliseconds(900);
case T_Code.Bat:
return TimeSpan.FromMilliseconds(1500);
case T_Code.Mks:
return TimeSpan.FromMilliseconds(900);
case T_Code.Atu:
case T_Code.Sen:
return TimeSpan.FromMilliseconds(1200);
default:
return TimeSpan.FromSeconds(2);
}
}
private void LimparMensagensAntigas()
{
var agora = DateTime.Now;
@ -756,7 +971,8 @@ namespace AgroBase.Services
x.ComResposta &&
x.Enviado &&
!x.Respondido &&
x.EnviadoEm < agora.AddSeconds(-2))
x.EnviadoEm != DateTime.MinValue &&
agora - x.EnviadoEm > TimeoutResposta(x))
.ToList();
foreach (var item in timeouts)
@ -765,12 +981,14 @@ namespace AgroBase.Services
MensagensPendentes.Remove(item);
}
var remover = MensagensPendentes
.Where(x =>
(!x.ComResposta && x.Enviado) ||
(x.ComResposta && x.Respondido) ||
(x.Momento < agora.AddSeconds(-30)))
.ToList();
var remover =
MensagensPendentes
.Where(x =>
(!x.ComResposta && x.Enviado) ||
(x.ComResposta && x.Respondido) ||
(x.Momento < agora.AddSeconds(-30)) ||
(!IsConnected && x.Enviado))
.ToList();
foreach (var item in remover)
MensagensPendentes.Remove(item);
@ -836,6 +1054,8 @@ namespace AgroBase.Services
{
try
{
LimparPendenciasEnviadasPorReconexao();
FecharConexaoSemLimparHandlers();
if (TransportMode == CanEthernetTransportMode.Tcp)
@ -863,6 +1083,22 @@ namespace AgroBase.Services
}
}
private void LimparPendenciasEnviadasPorReconexao()
{
lock (_LockMensagens)
{
var remover =
MensagensPendentes
.Where(x =>
x.Enviado ||
x.Momento < DateTime.Now.AddSeconds(-2))
.ToList();
foreach (var item in remover)
MensagensPendentes.Remove(item);
}
}
private void FecharConexaoSemLimparHandlers()
{
lock (_socketLock)
@ -1032,17 +1268,12 @@ namespace AgroBase.Services
}
}
private static string IdKey(uint id)
{
return $"0x{id:X}";
}
private static string TimeoutChave(CanMessage msg)
{
if (msg == null)
return "desconhecido";
string chave = $"{msg.Dispositivo}|tx:{IdKey(msg.IdTx)}|rx:{IdKey(msg.IdRx)}|fc_rx:0x{msg.funcCodeRx:X2}";
string chave = $"{msg.Dispositivo}|tx:{CanManager.IdKey(msg.IdTx)}|rx:{CanManager.IdKey(msg.IdRx)}|fc_rx:0x{msg.funcCodeRx:X2}";
if (new List<T_Code>() { T_Code.Atu, T_Code.Sen }.Contains(msg.Dispositivo) && msg.DataTx != null && msg.DataTx.Length > 1)
{
@ -1054,6 +1285,11 @@ namespace AgroBase.Services
chave += $"|fc_tx:0x{msg.DataTx[0]:X2}";
}
if (!string.IsNullOrWhiteSpace(msg.ChaveInterna))
{
chave += $"|key:{msg.ChaveInterna}";
}
return chave;
}
@ -1075,7 +1311,7 @@ namespace AgroBase.Services
TimeoutsRespostaTotal++;
string idRx = IdKey(msg.IdRx);
string idRx = CanManager.IdKey(msg.IdRx);
string dispositivo = msg.Dispositivo.ToString();
string chave = TimeoutChave(msg);

File diff suppressed because it is too large Load Diff

View File

@ -144,14 +144,6 @@ namespace AgroBase.Services
public void IniciarOperacao(double distanciaTotalMetrosAtual, double tempoTotalSegundosAtual)
{
/*
* Antes, se o contador ainda não tivesse recebido uma leitura
* válida do reservatório, o início era ignorado.
*
* Isso fazia VolumeConsumidoOperacao_L ficar zero durante toda
* a operação quando a primeira leitura válida chegava depois
* do comando de iniciar.
*/
if (!_inicializado)
{
OperacaoAtiva = true;
@ -229,19 +221,24 @@ namespace AgroBase.Services
if (!_inicializado)
{
Inicializar(distanciaTotalMetrosAtual, tempoTotalSegundosAtual);
Inicializar(
distanciaTotalMetrosAtual,
tempoTotalSegundosAtual
);
if (OperacaoAtiva || _inicioOperacaoPendente)
{
/*
* Primeira leitura válida depois do comando de iniciar.
* A operação passa a usar o volume atual como referência
* inicial, sem perder o estado de OperacaoAtiva.
* Primeira leitura válida depois do início da operação.
* Usa o volume atual como baseline da operação para começar
* PercentualConsumidoOperacao em zero.
*/
_volumeInicialOperacao_L = VolumeAtual_L;
_distanciaTotalAnterior_m = distanciaTotalMetrosAtual;
_tempoTotalAnterior_s = tempoTotalSegundosAtual;
_distanciaTotalAnterior_m =
distanciaTotalMetrosAtual;
_tempoTotalAnterior_s =
tempoTotalSegundosAtual;
OperacaoAtiva = true;
OperacaoFinalizada = false;
@ -306,15 +303,24 @@ namespace AgroBase.Services
private double ObterVazaoFluxo_mLs()
{
var dados = Variaveis.OperacaoEmAndamento.DispAtu?.Dados;
var dados =
Variaveis
.OperacaoEmAndamento
.DispAtu?
.Dados;
if (dados == null)
return 0;
double vazao = dados.VazaoFluxoMLpSReferencia;
double vazao =
dados.VazaoFluxoMLpSReferencia;
if (vazao < 0)
if (double.IsNaN(vazao) ||
double.IsInfinity(vazao) ||
vazao < 0)
{
return 0;
}
return vazao;
}
@ -399,11 +405,15 @@ namespace AgroBase.Services
?? 0;
if (qtdAtuacoes > 0)
{
HerbicidaPorAtuacao_L =
VolumeConsumidoOperacao_L /
qtdAtuacoes;
}
else
{
HerbicidaPorAtuacao_L = 0;
}
}
private void AtualizarAutonomia(double velocidadeAtual_m_s)

File diff suppressed because it is too large Load Diff