ajustes calculo vazao por fluxo, saude do mov por frequencia por parametro

This commit is contained in:
Diego Freitas 2026-06-22 20:33:50 -03:00
parent 9b5fdf267f
commit bbd5f4bfec
5 changed files with 401 additions and 134 deletions

View File

@ -129,16 +129,21 @@ namespace AgroBase.Models.Modules
{
get
{
int pulsos = (int)(SensorFluxoLinha?.ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.QtdPulsos)?.atual?.valor ?? 0.0);
int pulsos = (int)(SensorFluxoLinha?.ValoresLeituras?
.FirstOrDefault(x => x.funcao == FuncoesPinout.QtdPulsos)?
.atual?.valor ?? 0.0);
int periodo = (int)(SensorFluxoLinha?.ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.Periodo)?.atual?.valor ?? 0.0);
int periodo = (int)(SensorFluxoLinha?.ValoresLeituras?
.FirstOrDefault(x => x.funcao == FuncoesPinout.Periodo)?
.atual?.valor ?? 0.0);
if (pulsos <= 0 || periodo <= 0)
return 0.0;
const double FatorCalibracaoFluxo = 0.776;
double frequencia = (double)pulsos / periodo * 1000.0;
// Fabricante: F = 8.1*q - 3, q em L/min
double q_L_min = (frequencia + 3.0) / 8.1;
if (q_L_min < 0)
@ -147,7 +152,7 @@ namespace AgroBase.Models.Modules
if (q_L_min > VazaoMaximaSensor_LMin)
q_L_min = VazaoMaximaSensor_LMin;
return q_L_min * 1000.0 / 60.0;
return q_L_min * 1000.0 / 60.0 * FatorCalibracaoFluxo;
}
}

View File

@ -235,44 +235,107 @@ namespace AgroBase.Models.Modules
}
}
private DateTime _ultimoEnvioControleMov = DateTime.MinValue;
private DateTime _ultimoRegistroLogMov = DateTime.MinValue;
private static readonly TimeSpan IntervaloControleMov = TimeSpan.FromSeconds(1);
private static readonly TimeSpan IntervaloLogMov = TimeSpan.FromSeconds(1);
private async Task tmrRegistradorMOV_Tick()
{
if (Variaveis.Fechando) return;
if (Variaveis.Fechando)
return;
if (!OIDCanService.Iniciado && SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Mov))
DateTime agora = DateTime.Now;
if (!OIDCanService.Iniciado &&
SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Mov))
{
foreach (var Leitura in OIDCanService.DadosLeitura.Where(x => x.UltimoComandoRecebido.AddSeconds(10) < DateTime.Now))
foreach (var leitura in OIDCanService.DadosLeitura
.Where(x => x.UltimoComandoRecebido.AddSeconds(10) < agora))
{
OIDCanService.RequisitarDado(_EnderecoCAN_Tx, _EnderecoCAN_Rx, OIDCanService.OidHandler.OidParametros.CodErro);
OIDCanService.RequisitarDado(
leitura.EnderecoTx,
leitura.EnderecoRx,
OIDCanService.OidHandler.OidParametros.CodErro
);
}
tmrRegistradorMOV.SetInterval(3000);
return;
}
foreach (var Modulo in Modulos)
bool deveEnviarControle =
_ultimoEnvioControleMov == DateTime.MinValue ||
agora - _ultimoEnvioControleMov >= IntervaloControleMov;
bool deveRegistrarLog =
_ultimoRegistroLogMov == DateTime.MinValue ||
agora - _ultimoRegistroLogMov >= IntervaloLogMov;
bool enviouControle = false;
bool registrouLog = false;
var controleMov =
deveEnviarControle
? Variaveis.OperacaoEmAndamento.Controle.TiposControle?
.FirstOrDefault(x => x.Tipo == T_Code.Mov)
: null;
foreach (var modulo in Modulos)
{
if (Modulo.MovMotor.Inicializado)
if (!modulo.MovMotor.Inicializado)
continue;
byte addrTx = modulo.MovMotor._EnderecoCAN_Tx;
byte addrRx = modulo.MovMotor._EnderecoCAN_Rx;
/*
* Esse continua em todo tick.
* O OIDCanService segura throttle, pendência e rodízio.
*/
OIDCanService.RequisitarProximoDado(addrTx, addrRx);
/*
* Desligado por enquanto para aliviar CAN.
*/
// modulo.MovMotor.RequisitarDadosSensoresExternos();
/*
* Controle a cada 1s.
* Se o EnviarComandoControle tiver dedupe interno, melhor ainda.
*/
if (deveEnviarControle && controleMov != null)
{
byte addrTx = Modulo.MovMotor._EnderecoCAN_Tx;
byte addrRx = Modulo.MovMotor._EnderecoCAN_Rx;
OIDCanService.RequisitarProximoDado(addrTx, addrRx);
Modulo.MovMotor.RequisitarDadosSensoresExternos();
modulo.MovMotor.EnviarComandoControle(controleMov.UltimaDirecao);
enviouControle = true;
}
var Controle = Variaveis.OperacaoEmAndamento.Controle.TiposControle?.FirstOrDefault(x => x.Tipo == T_Code.Mov);
if (Controle != null)
{
Modulo.MovMotor.EnviarComandoControle(Controle.UltimaDirecao);
}
/*
* Essa leitura é local, pode deixar no tick.
*/
modulo.MovMotor.ConfigFreio.Leitura =
modulo.MovMotor.Leitura?.Freado ?? false;
Modulo.MovMotor.ConfigFreio.Leitura = Modulo.MovMotor.Leitura?.Freado ?? false;
Variaveis.OperacaoEmAndamento.DispMvd.Dados.RegistrarLogMov(Modulo);
/*
* Log a cada 1s para não virar serragem de JSON.
*/
if (deveRegistrarLog)
{
Variaveis.OperacaoEmAndamento.DispMvd.Dados.RegistrarLogMov(modulo);
registrouLog = true;
}
}
if (enviouControle)
_ultimoEnvioControleMov = agora;
if (registrouLog)
_ultimoRegistroLogMov = agora;
tmrRegistradorMOV.SetInterval(OIDCanService.TaxaAmostragem);
await Task.CompletedTask;
}
private async Task tmrRegistradorDIR_Tick()

View File

@ -11,8 +11,7 @@ namespace AgroBase.Services
{
public class OIDCanService
{
public static List<OIDModel> DadosLeitura =
new List<OIDModel>();
public static List<OIDModel> DadosLeitura = new List<OIDModel>();
private static T_Code Dispositivo { get; set; } =
T_Code.Oid;
@ -22,72 +21,33 @@ namespace AgroBase.Services
private static AsyncTaskTimerModel tmrHeartbeat;
private static readonly object _sync =
new object();
private static readonly HashSet<byte> _handlersRegistrados =
new HashSet<byte>();
private static readonly Dictionary<byte, DateTime>
_ultimoEnvioConsultaPorDriver =
new Dictionary<byte, DateTime>();
private static readonly Dictionary<string, DateTime>
_ultimoEnvioParametro =
new Dictionary<string, DateTime>();
private static readonly Dictionary<byte, OidParametros>
_ultimaConsultaPendente =
new Dictionary<byte, OidParametros>();
private static readonly Dictionary<byte, int>
_cursorPollingPorDriver =
new Dictionary<byte, int>();
private static readonly Dictionary<string, string>
_ultimoComandoControleAssinatura =
new Dictionary<string, string>();
private static readonly Dictionary<string, DateTime>
_ultimoComandoControleQuando =
new Dictionary<string, DateTime>();
private static readonly object _sync = new object();
private static readonly HashSet<byte> _handlersRegistrados = new HashSet<byte>();
private static readonly Dictionary<byte, DateTime> _ultimoEnvioConsultaPorDriver = new Dictionary<byte, DateTime>();
private static readonly Dictionary<string, DateTime> _ultimoEnvioParametro = new Dictionary<string, DateTime>();
private static readonly Dictionary<byte, OidParametros> _ultimaConsultaPendente = new Dictionary<byte, OidParametros>();
private static readonly Dictionary<byte, int> _cursorPollingPorDriver = new Dictionary<byte, int>();
private static readonly Dictionary<string, string> _ultimoComandoControleAssinatura = new Dictionary<string, string>();
private static readonly Dictionary<string, DateTime> _ultimoComandoControleQuando = new Dictionary<string, DateTime>();
/*
* Consultas síncronas do OID respondem sempre com DATA[0]=0x0F.
* A diferença fica em DATA[1]. Para evitar confusão no CanService,
* enviaremos no máximo uma consulta pendente por driver.
*/
private static readonly TimeSpan IntervaloMinimoConsultaPorDriver =
TimeSpan.FromMilliseconds(120);
private static readonly TimeSpan IntervaloMinimoMesmoParametro =
TimeSpan.FromMilliseconds(300);
private static readonly TimeSpan JanelaConsultaPendente =
TimeSpan.FromMilliseconds(450);
private static readonly TimeSpan JanelaDedupeComandoControle =
TimeSpan.FromMilliseconds(80);
private static readonly TimeSpan TempoRespostaRecente =
TimeSpan.FromSeconds(3);
private static readonly TimeSpan TempoRemoverDriver =
TimeSpan.FromSeconds(15);
/*
* O manual sugere heartbeat com período metade do timeout.
* O timer continua em 500ms, mas o payload passa a seguir o manual:
* DATA[0] = 0x00.
*/
private static readonly TimeSpan PeriodoHeartbeat =
TimeSpan.FromMilliseconds(500);
private static readonly TimeSpan IntervaloMinimoConsultaPorDriver = TimeSpan.FromMilliseconds(120);
private static readonly TimeSpan IntervaloMinimoMesmoParametro = TimeSpan.FromMilliseconds(300);
private static readonly TimeSpan JanelaConsultaPendente = TimeSpan.FromMilliseconds(450);
private static readonly TimeSpan JanelaDedupeComandoControle = TimeSpan.FromMilliseconds(80);
private static readonly TimeSpan TempoRespostaRecente = TimeSpan.FromSeconds(3);
private static readonly TimeSpan TempoRemoverDriver = TimeSpan.FromSeconds(15);
private static readonly TimeSpan PeriodoHeartbeat = TimeSpan.FromMilliseconds(500);
/*
* Polling recomendado. Se outro ponto do sistema ainda chamar
* RequisitarDado() em massa, o throttle interno também segura.
*/
private static readonly OidParametros[] OrdemPolling =
public static readonly OidParametros[] OrdemPolling =
{
OidParametros.VelocidadeAtual,
OidParametros.CorrenteMotor,
@ -101,24 +61,264 @@ namespace AgroBase.Services
OidParametros.IO_Status,
};
private static readonly Dictionary<OidParametros, TimeSpan>
IntervaloParametro =
new Dictionary<OidParametros, TimeSpan>
private static readonly Dictionary<OidParametros, TimeSpan> IntervaloParametro = new Dictionary<OidParametros, TimeSpan>
{
{ OidParametros.VelocidadeAtual, TimeSpan.FromMilliseconds(160) },
{ OidParametros.CorrenteMotor, TimeSpan.FromMilliseconds(300) },
{ OidParametros.CorrenteBarramento, TimeSpan.FromMilliseconds(350) },
{ OidParametros.CodErro, TimeSpan.FromMilliseconds(750) },
{ OidParametros.Tensao, TimeSpan.FromSeconds(1) },
{ OidParametros.Temperatura, TimeSpan.FromSeconds(1) },
{ OidParametros.CicloTrabalho, TimeSpan.FromMilliseconds(500) },
{ OidParametros.Potencia, TimeSpan.FromMilliseconds(500) },
{ OidParametros.PosicaoAbsoluta, TimeSpan.FromMilliseconds(500) },
{ OidParametros.PosicaoRelativa, TimeSpan.FromMilliseconds(500) },
{ OidParametros.ModoCodificador, TimeSpan.FromSeconds(2) },
{ OidParametros.ErroLerCache, TimeSpan.FromSeconds(5) },
{ OidParametros.IO_Status, TimeSpan.FromSeconds(2) },
};
public class OidParametroHealthConfig
{
public OidParametros Parametro { get; set; }
public string RedisKey { get; set; }
/*
* Frequência esperada para health.
* Não precisa ser a frequência máxima possível.
* É o que consideramos saudável para operação.
*/
public double FreqBaseHz { get; set; }
/*
* Abaixo disso começa alerta.
* Sugestão: 50% a 60% da base, dependendo da importância.
*/
public double FreqMinHz { get; set; }
public bool EmUsoHealth { get; set; } = true;
}
public static readonly Dictionary<OidParametros, OidParametroHealthConfig> HealthParametroConfig = new Dictionary<OidParametros, OidParametroHealthConfig>
{
/*
* Runtime importante.
*/
{
OidParametros.VelocidadeAtual,
new OidParametroHealthConfig
{
{ OidParametros.VelocidadeAtual, TimeSpan.FromMilliseconds(160) },
{ OidParametros.CorrenteMotor, TimeSpan.FromMilliseconds(300) },
{ OidParametros.CorrenteBarramento, TimeSpan.FromMilliseconds(350) },
{ OidParametros.CodErro, TimeSpan.FromMilliseconds(750) },
{ OidParametros.Tensao, TimeSpan.FromSeconds(1) },
{ OidParametros.Temperatura, TimeSpan.FromSeconds(1) },
{ OidParametros.CicloTrabalho, TimeSpan.FromMilliseconds(500) },
{ OidParametros.Potencia, TimeSpan.FromMilliseconds(500) },
{ OidParametros.PosicaoAbsoluta, TimeSpan.FromMilliseconds(500) },
{ OidParametros.PosicaoRelativa, TimeSpan.FromMilliseconds(500) },
{ OidParametros.ModoCodificador, TimeSpan.FromSeconds(2) },
{ OidParametros.ErroLerCache, TimeSpan.FromSeconds(5) },
{ OidParametros.IO_Status, TimeSpan.FromSeconds(2) },
};
Parametro = OidParametros.VelocidadeAtual,
RedisKey = "rpm",
FreqBaseHz = 1.0,
FreqMinHz = 0.45,
EmUsoHealth = true
}
},
{
OidParametros.CorrenteMotor,
new OidParametroHealthConfig
{
Parametro = OidParametros.CorrenteMotor,
RedisKey = "corrente_motor",
FreqBaseHz = 1.0,
FreqMinHz = 0.45,
EmUsoHealth = true
}
},
{
OidParametros.CorrenteBarramento,
new OidParametroHealthConfig
{
Parametro = OidParametros.CorrenteBarramento,
RedisKey = "corrente_barramento",
FreqBaseHz = 1.0,
FreqMinHz = 0.45,
EmUsoHealth = true
}
},
/*
* Estado elétrico/térmico. Pode ser mais lento.
*/
{
OidParametros.Tensao,
new OidParametroHealthConfig
{
Parametro = OidParametros.Tensao,
RedisKey = "tensao",
FreqBaseHz = 0.5,
FreqMinHz = 0.20,
EmUsoHealth = true
}
},
{
OidParametros.Temperatura,
new OidParametroHealthConfig
{
Parametro = OidParametros.Temperatura,
RedisKey = "temperatura",
FreqBaseHz = 0.5,
FreqMinHz = 0.20,
EmUsoHealth = true
}
},
/*
* Diagnóstico/telemetria secundária.
*/
{
OidParametros.CodErro,
new OidParametroHealthConfig
{
Parametro = OidParametros.CodErro,
RedisKey = "erro",
FreqBaseHz = 0.5,
FreqMinHz = 0.20,
EmUsoHealth = true
}
},
{
OidParametros.CicloTrabalho,
new OidParametroHealthConfig
{
Parametro = OidParametros.CicloTrabalho,
RedisKey = "ciclo_trabalho",
FreqBaseHz = 0.5,
FreqMinHz = 0.20,
EmUsoHealth = true
}
},
{
OidParametros.Potencia,
new OidParametroHealthConfig
{
Parametro = OidParametros.Potencia,
RedisKey = "potencia",
FreqBaseHz = 0.5,
FreqMinHz = 0.20,
EmUsoHealth = true
}
},
/*
* Por enquanto, não cobra no health se não for usado.
*/
{
OidParametros.PosicaoAbsoluta,
new OidParametroHealthConfig
{
Parametro = OidParametros.PosicaoAbsoluta,
RedisKey = "posicao_absoluta",
FreqBaseHz = 0.2,
FreqMinHz = 0.08,
EmUsoHealth = false
}
},
{
OidParametros.IO_Status,
new OidParametroHealthConfig
{
Parametro = OidParametros.IO_Status,
RedisKey = "io_status",
FreqBaseHz = 0.2,
FreqMinHz = 0.08,
EmUsoHealth = false
}
},
};
public static OidParametroHealthConfig ObterConfigHealth(OidParametros parametro)
{
if (HealthParametroConfig.TryGetValue(parametro, out var cfg))
{
return cfg;
}
return new OidParametroHealthConfig
{
Parametro = parametro,
RedisKey = parametro.ToString().ToLowerInvariant(),
FreqBaseHz = 0.5,
FreqMinHz = 0.2,
EmUsoHealth = false
};
}
public static double ObterFreqBaseHz(OidParametros parametro)
{
return ObterConfigHealth(parametro).FreqBaseHz;
}
public static double ObterFreqMinHz(OidParametros parametro)
{
return ObterConfigHealth(parametro).FreqMinHz;
}
public static bool ParametroEmUsoHealth(OidParametros parametro)
{
return ObterConfigHealth(parametro).EmUsoHealth;
}
public static double ObterIntervaloPollingParametroMs(OidParametros parametro)
{
if (IntervaloParametro.TryGetValue(parametro, out TimeSpan intervalo))
{
return intervalo.TotalMilliseconds;
}
return 500.0;
}
public static (string campo, object valor)[] GerarCamposRedisHealthBase(int enderecoMov)
{
var campos = new List<(string campo, object valor)>();
foreach (var kv in HealthParametroConfig)
{
var cfg = kv.Value;
string key = cfg.RedisKey;
campos.Add((
$"{enderecoMov}.freq_base.{key}",
cfg.FreqBaseHz
));
campos.Add((
$"{enderecoMov}.freq_min.{key}",
cfg.FreqMinHz
));
campos.Add((
$"{enderecoMov}.freq_uso.{key}",
cfg.EmUsoHealth
));
campos.Add((
$"{enderecoMov}.polling_ms.{key}",
ObterIntervaloPollingParametroMs(kv.Key)
));
}
campos.Add((
$"{enderecoMov}.polling.intervalo_timer_ms",
TaxaAmostragem
));
campos.Add((
$"{enderecoMov}.polling.qtd_parametros",
OrdemPolling.Length
));
campos.Add((
$"{enderecoMov}.polling.ordem",
OrdemPolling.Select(x => ObterConfigHealth(x).RedisKey).ToList()
));
return campos.ToArray();
}
public static bool Iniciado
{
@ -542,47 +742,25 @@ namespace AgroBase.Services
private static OidParametros? EscolherProximoParametro(byte idTx)
{
DateTime agora =
DateTime.Now;
DateTime agora = DateTime.Now;
int cursor =
_cursorPollingPorDriver.TryGetValue(
idTx,
out int c
)
? c
: 0;
int cursor = _cursorPollingPorDriver.TryGetValue(idTx, out int c) ? c : 0;
for (int tentativa = 0;
tentativa < OrdemPolling.Length;
tentativa++)
for (int tentativa = 0; tentativa < OrdemPolling.Length; tentativa++)
{
int index =
(cursor + tentativa) %
OrdemPolling.Length;
int index = (cursor + tentativa) % OrdemPolling.Length;
OidParametros parametro =
OrdemPolling[index];
OidParametros parametro = OrdemPolling[index];
string chave =
idTx.ToString("X2") +
":" +
((byte)parametro).ToString("X2");
string chave = idTx.ToString("X2") + ":" + ((byte)parametro).ToString("X2");
TimeSpan intervalo =
IntervaloParametro.TryGetValue(
parametro,
out TimeSpan valor
)
? valor
: TimeSpan.FromMilliseconds(500);
TimeSpan intervalo = IntervaloParametro.TryGetValue(parametro, out TimeSpan valor)
? valor
: TimeSpan.FromMilliseconds(500);
if (!_ultimoEnvioParametro.TryGetValue(chave, out DateTime ultimo) ||
agora - ultimo >= intervalo)
if (!_ultimoEnvioParametro.TryGetValue(chave, out DateTime ultimo) || agora - ultimo >= intervalo)
{
_cursorPollingPorDriver[idTx] =
(index + 1) %
OrdemPolling.Length;
_cursorPollingPorDriver[idTx] = (index + 1) % OrdemPolling.Length;
return parametro;
}

View File

@ -424,13 +424,20 @@ namespace AgroBase.Services.Operadores
);
int endereco_mov = mod.MovMotor._EnderecoCAN_Tx;
RedisService.AtualizarCampos(
RedisService.ModKey(Enums.T_Code.Mov),
var camposMov = new List<(string campo, object valor)>
{
($"{endereco_mov}.timestamp", agora),
($"{endereco_mov}.conectado", mod.MovMotor.Inicializado),
($"{endereco_mov}.freq_base", 1),
// legado
($"{endereco_mov}.freq_base",
OIDCanService.ObterFreqBaseHz(
OIDCanService.OidHandler.OidParametros.VelocidadeAtual
)),
($"{endereco_mov}.endereco", mod.MovMotor._EnderecoCAN_Tx),
($"{endereco_mov}.mod_id", mod.Modulo_ID),
($"{endereco_mov}.cod_erro", mod.MovMotor.CodigoAlarme),
($"{endereco_mov}.rpm", mod.MovMotor.RPM_Roda),
($"{endereco_mov}.rpm_sp", mod.MovMotor.RPM_SP_Leitura),
@ -440,12 +447,21 @@ namespace AgroBase.Services.Operadores
($"{endereco_mov}.temperatura_driver", mod.MovMotor.TemperaturaDriver),
($"{endereco_mov}.temperatura_motor", mod.MovMotor.Temperatura),
($"{endereco_mov}.freado", mod.MovMotor.Leitura?.Freado ?? false),
($"{endereco_mov}.freq.erro", mod.MovMotor.Leitura?.CodigoAlarme?.frequencia ?? 0),
($"{endereco_mov}.freq.rpm", mod.MovMotor.Leitura?.ERPM?.frequencia ?? 0),
($"{endereco_mov}.freq.tensao", mod.MovMotor.Leitura?.Tensao?.frequencia ?? 0),
($"{endereco_mov}.freq.corrente_motor", mod.MovMotor.Leitura?.CorrenteMotor?.frequencia ?? 0),
($"{endereco_mov}.freq.corrente_barramento", mod.MovMotor.Leitura?.CorrenteBarramento?.frequencia ?? 0),
($"{endereco_mov}.freq.temperatura", mod.MovMotor.Leitura?.Temperatura?.frequencia ?? 0)
($"{endereco_mov}.freq.temperatura", mod.MovMotor.Leitura?.Temperatura?.frequencia ?? 0),
($"{endereco_mov}.freq.ciclo_trabalho", mod.MovMotor.Leitura?.CicloTrabalho?.frequencia ?? 0),
($"{endereco_mov}.freq.potencia", mod.MovMotor.Leitura?.Potencia?.frequencia ?? 0),
};
camposMov.AddRange(OIDCanService.GerarCamposRedisHealthBase(endereco_mov));
RedisService.AtualizarCampos(
RedisService.ModKey(Enums.T_Code.Mov),
camposMov.ToArray()
);
dirAtivos.Add(endereco_dir);

View File

@ -152,6 +152,8 @@ class SensorFluxo_v2 : public ComponenteCAN {
float CalcularVazao(uint16_t pulsos, unsigned long periodoMs) {
if (periodoMs == 0) return 0.0f;
const float FATOR_CALIBRACAO_FLUXO = 0.776f;
float frequenciaHz = ((float)pulsos) * 1000.0f / (float)periodoMs;
float q_L_min = (frequenciaHz + 3.0f) / 8.1f;
@ -159,6 +161,9 @@ class SensorFluxo_v2 : public ComponenteCAN {
if (q_L_min > 10.0f) q_L_min = 10.0f;
float q_mL_s = q_L_min * (1000.0f / 60.0f);
q_mL_s *= FATOR_CALIBRACAO_FLUXO;
return q_mL_s;
}