Implementado bateria Daly, integrado IA segformer ao visual worker, treinamento raw
This commit is contained in:
parent
c805f6103b
commit
5e9155c3ed
|
|
@ -519,6 +519,7 @@
|
||||||
<Compile Include="Models\Components\CustomDialog.cs">
|
<Compile Include="Models\Components\CustomDialog.cs">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="Models\DalyBMSModel.cs" />
|
||||||
<Compile Include="Models\DispositivosModel.cs" />
|
<Compile Include="Models\DispositivosModel.cs" />
|
||||||
<Compile Include="Models\GPSModel.cs" />
|
<Compile Include="Models\GPSModel.cs" />
|
||||||
<Compile Include="Models\GraficoInterativoModel.cs" />
|
<Compile Include="Models\GraficoInterativoModel.cs" />
|
||||||
|
|
@ -576,6 +577,7 @@
|
||||||
<Compile Include="Services\CANServiceWaveshare.cs" />
|
<Compile Include="Services\CANServiceWaveshare.cs" />
|
||||||
<Compile Include="Services\CoolerControlService.cs" />
|
<Compile Include="Services\CoolerControlService.cs" />
|
||||||
<Compile Include="Services\CoulombCounterService.cs" />
|
<Compile Include="Services\CoulombCounterService.cs" />
|
||||||
|
<Compile Include="Services\DalyBMSService.cs" />
|
||||||
<Compile Include="Services\DispositivosService.cs" />
|
<Compile Include="Services\DispositivosService.cs" />
|
||||||
<Compile Include="Forms\frmPrincipal.cs">
|
<Compile Include="Forms\frmPrincipal.cs">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ namespace AgroBase.Forms
|
||||||
System.Windows.Forms.DataVisualization.Charting.Series series4 = new System.Windows.Forms.DataVisualization.Charting.Series();
|
System.Windows.Forms.DataVisualization.Charting.Series series4 = new System.Windows.Forms.DataVisualization.Charting.Series();
|
||||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frmAcompanhamento));
|
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frmAcompanhamento));
|
||||||
this.pnlFundo = new System.Windows.Forms.Panel();
|
this.pnlFundo = new System.Windows.Forms.Panel();
|
||||||
|
this.lblSen_Latencia = new System.Windows.Forms.Label();
|
||||||
this.lblB19V = new System.Windows.Forms.Label();
|
this.lblB19V = new System.Windows.Forms.Label();
|
||||||
this.lblB24V = new System.Windows.Forms.Label();
|
this.lblB24V = new System.Windows.Forms.Label();
|
||||||
this.lblGPSDetalhes = new System.Windows.Forms.Label();
|
this.lblGPSDetalhes = new System.Windows.Forms.Label();
|
||||||
|
|
@ -126,7 +127,7 @@ namespace AgroBase.Forms
|
||||||
this.lblEFA_SP = new System.Windows.Forms.Label();
|
this.lblEFA_SP = new System.Windows.Forms.Label();
|
||||||
this.lblM_EF = new System.Windows.Forms.Label();
|
this.lblM_EF = new System.Windows.Forms.Label();
|
||||||
this.picEF = new System.Windows.Forms.PictureBox();
|
this.picEF = new System.Windows.Forms.PictureBox();
|
||||||
this.lblSen_Latencia = new System.Windows.Forms.Label();
|
this.lblVBat = new System.Windows.Forms.Label();
|
||||||
this.pnlFundo.SuspendLayout();
|
this.pnlFundo.SuspendLayout();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.chartET)).BeginInit();
|
((System.ComponentModel.ISupportInitialize)(this.chartET)).BeginInit();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.chartDT)).BeginInit();
|
((System.ComponentModel.ISupportInitialize)(this.chartDT)).BeginInit();
|
||||||
|
|
@ -143,6 +144,7 @@ namespace AgroBase.Forms
|
||||||
//
|
//
|
||||||
this.pnlFundo.AutoScroll = true;
|
this.pnlFundo.AutoScroll = true;
|
||||||
this.pnlFundo.BackColor = System.Drawing.Color.White;
|
this.pnlFundo.BackColor = System.Drawing.Color.White;
|
||||||
|
this.pnlFundo.Controls.Add(this.lblVBat);
|
||||||
this.pnlFundo.Controls.Add(this.lblSen_Latencia);
|
this.pnlFundo.Controls.Add(this.lblSen_Latencia);
|
||||||
this.pnlFundo.Controls.Add(this.lblB19V);
|
this.pnlFundo.Controls.Add(this.lblB19V);
|
||||||
this.pnlFundo.Controls.Add(this.lblB24V);
|
this.pnlFundo.Controls.Add(this.lblB24V);
|
||||||
|
|
@ -234,6 +236,17 @@ namespace AgroBase.Forms
|
||||||
this.pnlFundo.Size = new System.Drawing.Size(1473, 811);
|
this.pnlFundo.Size = new System.Drawing.Size(1473, 811);
|
||||||
this.pnlFundo.TabIndex = 0;
|
this.pnlFundo.TabIndex = 0;
|
||||||
//
|
//
|
||||||
|
// lblSen_Latencia
|
||||||
|
//
|
||||||
|
this.lblSen_Latencia.AutoSize = true;
|
||||||
|
this.lblSen_Latencia.Font = new System.Drawing.Font("Microsoft Sans Serif", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
|
||||||
|
this.lblSen_Latencia.Location = new System.Drawing.Point(656, 608);
|
||||||
|
this.lblSen_Latencia.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||||
|
this.lblSen_Latencia.Name = "lblSen_Latencia";
|
||||||
|
this.lblSen_Latencia.Size = new System.Drawing.Size(87, 12);
|
||||||
|
this.lblSen_Latencia.TabIndex = 193;
|
||||||
|
this.lblSen_Latencia.Text = "Latencia Loop: 0 ms";
|
||||||
|
//
|
||||||
// lblB19V
|
// lblB19V
|
||||||
//
|
//
|
||||||
this.lblB19V.AutoSize = true;
|
this.lblB19V.AutoSize = true;
|
||||||
|
|
@ -1108,16 +1121,16 @@ namespace AgroBase.Forms
|
||||||
this.picEF.TabIndex = 90;
|
this.picEF.TabIndex = 90;
|
||||||
this.picEF.TabStop = false;
|
this.picEF.TabStop = false;
|
||||||
//
|
//
|
||||||
// lblSen_Latencia
|
// lblVBat
|
||||||
//
|
//
|
||||||
this.lblSen_Latencia.AutoSize = true;
|
this.lblVBat.AutoSize = true;
|
||||||
this.lblSen_Latencia.Font = new System.Drawing.Font("Microsoft Sans Serif", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
|
this.lblVBat.Font = new System.Drawing.Font("Microsoft Sans Serif", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
|
||||||
this.lblSen_Latencia.Location = new System.Drawing.Point(656, 608);
|
this.lblVBat.Location = new System.Drawing.Point(656, 739);
|
||||||
this.lblSen_Latencia.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
this.lblVBat.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||||
this.lblSen_Latencia.Name = "lblSen_Latencia";
|
this.lblVBat.Name = "lblVBat";
|
||||||
this.lblSen_Latencia.Size = new System.Drawing.Size(87, 12);
|
this.lblVBat.Size = new System.Drawing.Size(149, 12);
|
||||||
this.lblSen_Latencia.TabIndex = 193;
|
this.lblVBat.TabIndex = 194;
|
||||||
this.lblSen_Latencia.Text = "Latencia Loop: 0 ms";
|
this.lblVBat.Text = "Bateria: 0,00V 0,00A 0,00% 0,00ºC";
|
||||||
//
|
//
|
||||||
// frmAcompanhamento
|
// frmAcompanhamento
|
||||||
//
|
//
|
||||||
|
|
@ -1234,5 +1247,6 @@ namespace AgroBase.Forms
|
||||||
public System.Windows.Forms.Label lblB24V;
|
public System.Windows.Forms.Label lblB24V;
|
||||||
public System.Windows.Forms.Label lblB19V;
|
public System.Windows.Forms.Label lblB19V;
|
||||||
public System.Windows.Forms.Label lblSen_Latencia;
|
public System.Windows.Forms.Label lblSen_Latencia;
|
||||||
|
public System.Windows.Forms.Label lblVBat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -83,6 +83,12 @@ namespace AgroBase.Forms
|
||||||
lblSen_Latencia.Text = $"Latencia de Loop: {Variaveis.OperacaoEmAndamento.DispSen.Dados?.DadosLeitura?.LatenciaLoop ?? 0} ms";
|
lblSen_Latencia.Text = $"Latencia de Loop: {Variaveis.OperacaoEmAndamento.DispSen.Dados?.DadosLeitura?.LatenciaLoop ?? 0} ms";
|
||||||
|
|
||||||
tmrAtualizacao.SetInterval(500);
|
tmrAtualizacao.SetInterval(500);
|
||||||
|
|
||||||
|
if (DalyBMSService.Iniciado)
|
||||||
|
{
|
||||||
|
var Bat = DalyBMSService.DadosLeitura;
|
||||||
|
lblVBat.Text = $"Bateria: {Bat.Pack.TensaoTotal_V:0.00}V {Bat.Pack.Corrente_A:0.00}A {Bat.Pack.SOC_percent:0.00}% {Bat.TemperaturaMax:0.00}ºC";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AtualizarDadosSensorCorrente(string id, Label lbl)
|
private void AtualizarDadosSensorCorrente(string id, Label lbl)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
using AgroBase.Services;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AgroBase.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Snapshot do estado atual do Daly BMS, conforme leituras via CAN.
|
||||||
|
/// </summary>
|
||||||
|
public class DalyBmsModel
|
||||||
|
{
|
||||||
|
// ===== Meta / estado interno =====
|
||||||
|
public DateTime Momento { get; set; } = DateTime.UtcNow;
|
||||||
|
public bool Iniciado { get; set; }
|
||||||
|
public DateTime? UltimoComandoRecebido { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Frames brutos por DataID (0x90..0x98). Útil para debug / logs.
|
||||||
|
/// Key = DataID (ex: 0x90), Value = payload de 8 bytes.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<byte, byte[]> LeiturasBrutas { get; set; } = new Dictionary<byte, byte[]>();
|
||||||
|
|
||||||
|
public double TensaoMax { get; set; }
|
||||||
|
public double TensaoMin { get; set; }
|
||||||
|
public double TemperaturaMax { get; set; }
|
||||||
|
public double TemperaturaMin { get; set; }
|
||||||
|
|
||||||
|
// ===== Dados principais =====
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resumo do pack: tensão total, corrente, SOC, MOS, contagem de células, etc.
|
||||||
|
/// (DataIDs: 0x90, 0x93, 0x94, e futuramente outros)
|
||||||
|
/// </summary>
|
||||||
|
public DalyPackStatus Pack { get; set; } = new DalyPackStatus();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tensões de cada célula, além de info de Vmax / Vmin.
|
||||||
|
/// (DataIDs: 0x91 e 0x95)
|
||||||
|
/// </summary>
|
||||||
|
public List<DalyCellVoltage> Celulas { get; set; } = new List<DalyCellVoltage>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Temperaturas individuais de NTCs / sensores, mais T_max / T_min.
|
||||||
|
/// (DataIDs: 0x92 e 0x96)
|
||||||
|
/// </summary>
|
||||||
|
public List<DalyTemperatureSensor> Temperaturas { get; set; } = new List<DalyTemperatureSensor>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estado de balanceamento de cada célula.
|
||||||
|
/// (DataID: 0x97)
|
||||||
|
/// </summary>
|
||||||
|
public DalyBalanceStatus Balanceamento { get; set; } = new DalyBalanceStatus();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Alarmes / falhas ativos no momento.
|
||||||
|
/// (DataID: 0x98)
|
||||||
|
/// </summary>
|
||||||
|
public DalyAlarms Alarmes { get; set; } = new DalyAlarms();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== PACK (0x90, 0x93, 0x94, futuros) =====================
|
||||||
|
|
||||||
|
public class DalyPackStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tensão total do pack (V) – cumulative voltage (0x90).
|
||||||
|
/// </summary>
|
||||||
|
public double TensaoTotal_V { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tensão medida / de amostragem (V) – gather voltage (0x90).
|
||||||
|
/// </summary>
|
||||||
|
public double TensaoMedida_V { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Corrente do pack (A). Positivo = carga, negativo = descarga. (0x90)
|
||||||
|
/// </summary>
|
||||||
|
public double Corrente_A { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// State of Charge (%), com resolução de 0,1% (0x90).
|
||||||
|
/// </summary>
|
||||||
|
public double SOC_percent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Número de células em série (0x94).
|
||||||
|
/// </summary>
|
||||||
|
public int NumeroCelulasSerie { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Número de sensores de temperatura reportados (0x94).
|
||||||
|
/// </summary>
|
||||||
|
public int NumeroSensoresTemperatura { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Carregador conectado (0x94).
|
||||||
|
/// </summary>
|
||||||
|
public bool CarregadorConectado { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Carga / load conectado (0x94).
|
||||||
|
/// </summary>
|
||||||
|
public bool LoadConectado { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estado geral do BMS: 0=parado, 1=carga, 2=descarga (0x93).
|
||||||
|
/// </summary>
|
||||||
|
public int EstadoGeral { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MOS de carga ON/OFF (0x93).
|
||||||
|
/// </summary>
|
||||||
|
public bool ChargeMosOn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MOS de descarga ON/OFF (0x93).
|
||||||
|
/// </summary>
|
||||||
|
public bool DischargeMosOn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vida / ciclos (Life). Valor bruto 0–255 (0x93).
|
||||||
|
/// </summary>
|
||||||
|
public int CiclosAproximados { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Capacidade restante estimada (Ah) (0x93 – valor em mAh / 1000).
|
||||||
|
/// </summary>
|
||||||
|
public double CapacidadeRestante_Ah { get; set; }
|
||||||
|
|
||||||
|
// Campos futuros (caso você leia via outro DataID / RS485):
|
||||||
|
public double? CapacidadeNominal_Ah { get; set; }
|
||||||
|
public double? CapacidadeReal_Ah { get; set; }
|
||||||
|
public double? SOH_percent { get; set; } // state of health
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IO digitais brutos (bit0-3 DI, bit4-7 DO) caso queira exibir depois.
|
||||||
|
/// </summary>
|
||||||
|
public byte IoStatusRaw { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== CÉLULAS (0x91, 0x95) =====================
|
||||||
|
|
||||||
|
public class DalyCellVoltage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Índice da célula (1-based, igual ao protocolo).
|
||||||
|
/// </summary>
|
||||||
|
public int Index { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tensão da célula (V).
|
||||||
|
/// </summary>
|
||||||
|
public double Tensao_V { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flag opcional se esta é a célula de maior tensão no pack.
|
||||||
|
/// (derivado de 0x91)
|
||||||
|
/// </summary>
|
||||||
|
public bool EhMaxima { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flag opcional se esta é a célula de menor tensão no pack.
|
||||||
|
/// (derivado de 0x91)
|
||||||
|
/// </summary>
|
||||||
|
public bool EhMinima { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indica se está em balanceamento no momento (derivado de 0x97).
|
||||||
|
/// </summary>
|
||||||
|
public bool EmBalanceamento { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== TEMPERATURAS (0x92, 0x96) =====================
|
||||||
|
|
||||||
|
public class DalyTemperatureSensor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Índice do sensor (1-based, igual ao protocolo 0x96).
|
||||||
|
/// </summary>
|
||||||
|
public int Index { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Temperatura em graus Celsius.
|
||||||
|
/// </summary>
|
||||||
|
public double Temperatura_C { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flag opcional se este é o sensor de maior temperatura.
|
||||||
|
/// (derivado de 0x92)
|
||||||
|
/// </summary>
|
||||||
|
public bool EhMaxima { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flag opcional se este é o sensor de menor temperatura.
|
||||||
|
/// (derivado de 0x92)
|
||||||
|
/// </summary>
|
||||||
|
public bool EhMinima { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== BALANCEAMENTO (0x97) =====================
|
||||||
|
|
||||||
|
public class DalyBalanceStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Mapa de células em balanceamento:
|
||||||
|
/// key = índice da célula (1-based), value = true se bit ligado.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<int, bool> CelulasEmBalanceamento { get; set; } = new Dictionary<int, bool>();
|
||||||
|
|
||||||
|
public int QtdeCelulasEmBalanceamento => CelulasEmBalanceamento.Count(kv => kv.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== ALARMES / FALHAS (0x98) =====================
|
||||||
|
|
||||||
|
public class DalyAlarms
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lista de descrições de alarmes ativos (já decodificados).
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Mensagens { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Código de falha bruto (byte 7 do frame 0x98).
|
||||||
|
/// </summary>
|
||||||
|
public byte FaultCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bytes brutos 0..6 de bits de alarme, caso queira debugar depois.
|
||||||
|
/// </summary>
|
||||||
|
public byte[] AlarmBytesRaw { get; set; } = Array.Empty<byte>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True se existe qualquer alarme ou fault code != 0.
|
||||||
|
/// </summary>
|
||||||
|
public bool TemAlarme => (Mensagens?.Count > 0) || FaultCode != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2307,6 +2307,7 @@ namespace AgroBase.Models
|
||||||
GPSService.AtualizaDadosRedis();
|
GPSService.AtualizaDadosRedis();
|
||||||
LivoxManagerProcess.AtualizarLeitura();
|
LivoxManagerProcess.AtualizarLeitura();
|
||||||
MovimentacaoMovimentoCompensadoModel.AtualizarDados();
|
MovimentacaoMovimentoCompensadoModel.AtualizarDados();
|
||||||
|
DalyBMSService.AtualizarDados();
|
||||||
|
|
||||||
var _Trajetoria = Variaveis.OperacaoEmAndamento.Trajetoria;
|
var _Trajetoria = Variaveis.OperacaoEmAndamento.Trajetoria;
|
||||||
var _DispMvd = Variaveis.OperacaoEmAndamento.DispMvd;
|
var _DispMvd = Variaveis.OperacaoEmAndamento.DispMvd;
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,96 @@ namespace AgroBase.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static uint ConverterIdCan(byte[] idBytes)
|
||||||
|
{
|
||||||
|
if (idBytes == null || idBytes.Length == 0)
|
||||||
|
throw new ArgumentException("ID CAN inválido.");
|
||||||
|
|
||||||
|
if (idBytes.Length > 4)
|
||||||
|
throw new ArgumentException("ID CAN não pode ter mais que 4 bytes.");
|
||||||
|
|
||||||
|
uint id = 0;
|
||||||
|
|
||||||
|
foreach (var b in idBytes)
|
||||||
|
{
|
||||||
|
id <<= 8;
|
||||||
|
id |= b;
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] ConverterIdCan(uint canId)
|
||||||
|
{
|
||||||
|
if (canId <= 0xFF)
|
||||||
|
return new byte[] { (byte)(canId & 0xFF) };
|
||||||
|
|
||||||
|
if (canId <= 0xFFFF)
|
||||||
|
return new byte[]
|
||||||
|
{
|
||||||
|
(byte)((canId >> 8) & 0xFF),
|
||||||
|
(byte)(canId & 0xFF)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (canId <= 0xFFFFFF)
|
||||||
|
return new byte[]
|
||||||
|
{
|
||||||
|
(byte)((canId >> 16) & 0xFF),
|
||||||
|
(byte)((canId >> 8) & 0xFF),
|
||||||
|
(byte)(canId & 0xFF)
|
||||||
|
};
|
||||||
|
|
||||||
|
return new byte[]
|
||||||
|
{
|
||||||
|
(byte)((canId >> 24) & 0xFF),
|
||||||
|
(byte)((canId >> 16) & 0xFF),
|
||||||
|
(byte)((canId >> 8) & 0xFF),
|
||||||
|
(byte)(canId & 0xFF)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte CalcularChecksum(uint id, byte[] data)
|
||||||
|
{
|
||||||
|
int soma = 0;
|
||||||
|
|
||||||
|
// Soma os bytes do ID (LSB primeiro, padrão mais comum)
|
||||||
|
soma += (byte)(id & 0xFF);
|
||||||
|
soma += (byte)((id >> 8) & 0xFF);
|
||||||
|
soma += (byte)((id >> 16) & 0xFF);
|
||||||
|
soma += (byte)((id >> 24) & 0xFF);
|
||||||
|
|
||||||
|
if (data != null)
|
||||||
|
{
|
||||||
|
foreach (var b in data)
|
||||||
|
soma += b;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (byte)(soma & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GerarChaveMensagem(T_Code Dispositivo, uint Id, byte[] data)
|
||||||
|
{
|
||||||
|
byte parametro = 0x00;
|
||||||
|
|
||||||
|
// Para dispositivos que não sejam MKS, usar DataTx[1] se existir
|
||||||
|
if (Dispositivo != T_Code.Mks && (data?.Length ?? 0) > 1)
|
||||||
|
{
|
||||||
|
parametro = data[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{Id}_{data[0]}_{parametro}";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CanMessage
|
public class CanMessage
|
||||||
{
|
{
|
||||||
public DateTime Momento { get; set; }
|
public DateTime Momento { get; set; }
|
||||||
public T_Code Dispositivo { get; set; }
|
public T_Code Dispositivo { get; set; }
|
||||||
public byte IdTx { get; set; }
|
public uint IdTx { get; set; }
|
||||||
public byte IdRx { get; set; }
|
public uint IdRx { get; set; }
|
||||||
public byte funcCodeTx { get; set; }
|
public byte funcCodeTx { get; set; }
|
||||||
public byte funcCodeRx { get; set; }
|
public byte funcCodeRx { get; set; }
|
||||||
public byte[] DataTx { get; set; }
|
public byte[] DataTx { get; set; }
|
||||||
|
|
@ -44,6 +126,7 @@ namespace AgroBase.Services
|
||||||
public DateTime RespondidoEm { get; set; }
|
public DateTime RespondidoEm { get; set; }
|
||||||
|
|
||||||
public string ChaveInterna { get; set; }
|
public string ChaveInterna { get; set; }
|
||||||
|
public bool IsExtended => IdRx > 0x7FF || IdTx > 0x7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CanCommandFreqModel<T>
|
public class CanCommandFreqModel<T>
|
||||||
|
|
@ -156,9 +239,9 @@ namespace AgroBase.Services
|
||||||
public interface ICanService : IDisposable
|
public interface ICanService : IDisposable
|
||||||
{
|
{
|
||||||
bool PortaIsCan(SerialPort Porta = null);
|
bool PortaIsCan(SerialPort Porta = null);
|
||||||
void RegistrarHandler(byte endereco_rx, ICanMessageHandler handler);
|
void RegistrarHandler(uint endereco_rx, ICanMessageHandler handler);
|
||||||
bool Inicializar(int BitRate = 250);
|
bool Inicializar(int BitRate = 250);
|
||||||
void AdicionarMensagemNaFila(T_Code Dispositivo, byte idTx, byte idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true);
|
void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] idTx, byte[] idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true);
|
||||||
void Fechar();
|
void Fechar();
|
||||||
string _portName { get; set; }
|
string _portName { get; set; }
|
||||||
bool Iniciado { get; }
|
bool Iniciado { get; }
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@ namespace AgroBase.Services
|
||||||
private AsyncTaskTimerModel tmrProcessarCAN;
|
private AsyncTaskTimerModel tmrProcessarCAN;
|
||||||
private ConcurrentQueue<string> FramesRecebidos = new ConcurrentQueue<string>();
|
private ConcurrentQueue<string> FramesRecebidos = new ConcurrentQueue<string>();
|
||||||
private StringBuilder SerialCANBuffer = new StringBuilder();
|
private StringBuilder SerialCANBuffer = new StringBuilder();
|
||||||
private readonly ConcurrentDictionary<byte, ICanMessageHandler> _roteadores = new ConcurrentDictionary<byte, ICanMessageHandler>();
|
private readonly ConcurrentDictionary<uint, ICanMessageHandler> _roteadores = new ConcurrentDictionary<uint, ICanMessageHandler>();
|
||||||
private readonly ConcurrentDictionary<byte, bool> _roteadoresRegistrados = new ConcurrentDictionary<byte, bool>();
|
private readonly ConcurrentDictionary<uint, bool> _roteadoresRegistrados = new ConcurrentDictionary<uint, bool>();
|
||||||
private byte? _ultimoIdEnviado = null;
|
private uint? _ultimoIdEnviado = null;
|
||||||
|
|
||||||
private List<CanMessage> Mensagens = new List<CanMessage>();
|
private List<CanMessage> Mensagens = new List<CanMessage>();
|
||||||
private readonly object _LockMensagens = new object();
|
private readonly object _LockMensagens = new object();
|
||||||
|
|
@ -61,9 +61,9 @@ namespace AgroBase.Services
|
||||||
private DateTime _ultimoErroCritico = DateTime.MinValue;
|
private DateTime _ultimoErroCritico = DateTime.MinValue;
|
||||||
|
|
||||||
private bool DebugMode = false;
|
private bool DebugMode = false;
|
||||||
private void MostrarLog(string message, byte id)
|
private void MostrarLog(string message, uint id)
|
||||||
{
|
{
|
||||||
if (DebugMode && new List<byte>() { 0x11 }.Contains(id))
|
if (DebugMode) // && new List<uint>() { 0x18100140 }.Contains(id)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[CAN] {message}");
|
Console.WriteLine($"[CAN] {message}");
|
||||||
}
|
}
|
||||||
|
|
@ -170,7 +170,7 @@ namespace AgroBase.Services
|
||||||
return Iniciado;
|
return Iniciado;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegistrarHandler(byte endereco_rx, ICanMessageHandler handler)
|
public void RegistrarHandler(uint endereco_rx, ICanMessageHandler handler)
|
||||||
{
|
{
|
||||||
_roteadoresRegistrados.TryGetValue(endereco_rx, out bool registrado);
|
_roteadoresRegistrados.TryGetValue(endereco_rx, out bool registrado);
|
||||||
if (!registrado)
|
if (!registrado)
|
||||||
|
|
@ -311,109 +311,142 @@ namespace AgroBase.Services
|
||||||
|
|
||||||
private void ProcessarFrameCAN(string frame)
|
private void ProcessarFrameCAN(string frame)
|
||||||
{
|
{
|
||||||
// Ex: t00183301020304
|
// Ex:
|
||||||
if (!frame.StartsWith("t")) return;
|
// t00183301020304
|
||||||
|
// T18904001801234AB
|
||||||
|
if (string.IsNullOrWhiteSpace(frame))
|
||||||
|
return;
|
||||||
|
|
||||||
|
char tipo = frame[0];
|
||||||
|
if (tipo != 't' && tipo != 'T')
|
||||||
|
return; // ignora coisas que não sejam frame CAN
|
||||||
|
|
||||||
|
bool extended = (tipo == 'T');
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (frame.Length < 5) return;
|
// t: 3 hex de ID
|
||||||
|
// T: 8 hex de ID
|
||||||
|
int idHexLen = extended ? 8 : 3;
|
||||||
|
|
||||||
int id = Convert.ToInt32(frame.Substring(1, 3), 16);
|
// precisa ter pelo menos: prefixo + ID + DLC
|
||||||
int len = int.Parse(frame.Substring(4, 1));
|
if (frame.Length < 1 + idHexLen + 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// pega o ID em texto
|
||||||
|
string idStr = frame.Substring(1, idHexLen);
|
||||||
|
uint id = Convert.ToUInt32(idStr, 16);
|
||||||
|
|
||||||
|
// DLC (1 dígito logo após o ID)
|
||||||
|
int dlcIndex = 1 + idHexLen;
|
||||||
|
int len = int.Parse(frame.Substring(dlcIndex, 1));
|
||||||
|
|
||||||
|
if (len < 0 || len > 8)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// início dos dados em texto (2 chars por byte)
|
||||||
|
int dataStart = dlcIndex + 1;
|
||||||
|
int expectedLen = dataStart + len * 2;
|
||||||
|
if (frame.Length < expectedLen)
|
||||||
|
return; // frame truncado, descarta
|
||||||
|
|
||||||
byte[] data = new byte[len];
|
byte[] data = new byte[len];
|
||||||
|
|
||||||
if (data == null || data.Length == 0) return;
|
|
||||||
|
|
||||||
for (int i = 0; i < len; i++)
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
data[i] = Convert.ToByte(frame.Substring(5 + i * 2, 2), 16);
|
data[i] = Convert.ToByte(frame.Substring(dataStart + i * 2, 2), 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
MostrarLog($"Processando frame: {frame}", (byte)id);
|
// Para log interno, usar o LSB do ID como "canal"
|
||||||
|
byte canalLog = (byte)(id & 0xFF);
|
||||||
|
MostrarLog($"Processando frame: {frame}", canalLog);
|
||||||
|
|
||||||
_roteadores.TryGetValue((byte)id, out ICanMessageHandler handler);
|
// Agora o roteador é indexado por uint (ID CAN completo)
|
||||||
|
_roteadores.TryGetValue(id, out ICanMessageHandler handler);
|
||||||
|
|
||||||
T_Code dispositivo = handler?.Dispositivo ?? T_Code.Vzo;
|
T_Code dispositivo = handler?.Dispositivo ?? T_Code.Vzo;
|
||||||
byte fCodeRx = data.Length > 0 ? data[0] : (byte)0x00;
|
bool db = dispositivo == T_Code.Bat;
|
||||||
byte parametroRx = data.Length > 1 ? data[1] : (byte)0x00;
|
|
||||||
string chave = GerarChaveMensagem(dispositivo, (byte)id, data);
|
// Para dispositivos antigos, isso ainda faz sentido.
|
||||||
|
// Para o BMS, o handler simplesmente ignora funcCodeRx / parametroRx.
|
||||||
|
byte fCodeRx = db ? (byte)DalyBMSService.GetDataId(id) : data.Length > 0 ? data[0] : (byte)0x00;
|
||||||
|
byte parametroRx = db ? (byte)0x00 : data.Length > 1 ? data[1] : (byte)0x00;
|
||||||
|
|
||||||
|
// IMPORTANTE: idealmente GerarChaveMensagem aceita uint agora.
|
||||||
|
// Se hoje ela aceita byte, vale criar uma sobrecarga.
|
||||||
|
string chave = CanManager.GerarChaveMensagem(dispositivo, id, data);
|
||||||
|
|
||||||
CanMessage mensagem = null;
|
CanMessage mensagem = null;
|
||||||
|
|
||||||
lock (_LockMensagens)
|
lock (_LockMensagens)
|
||||||
{
|
{
|
||||||
mensagem = Mensagens.FirstOrDefault(x => x.Enviado && !x.Respondido && x.Dispositivo == dispositivo && x.IdRx == id && x.DataTx[1] == data[1]);
|
// Aqui você tinha:
|
||||||
|
// x.IdRx == id && x.DataTx[1] == data[1]
|
||||||
|
// Isso é muito específico do protocolo antigo.
|
||||||
|
// Pro BMS, o matching pode ser só por IdRx e Dispositivo.
|
||||||
|
mensagem = Mensagens.FirstOrDefault(x =>
|
||||||
|
x.Enviado &&
|
||||||
|
!x.Respondido &&
|
||||||
|
x.Dispositivo == dispositivo &&
|
||||||
|
x.IdRx == id &&
|
||||||
|
(db ? true : x.DataTx[1] == data[1]) // basteria nao checa o funccode
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mensagem == null)
|
if (mensagem == null)
|
||||||
{
|
{
|
||||||
mensagem = new CanMessage { Dispositivo = dispositivo, IdTx = (byte)id, IdRx = (byte)id, funcCodeRx = fCodeRx, DataRx = data, ChaveInterna = chave };
|
mensagem = new CanMessage
|
||||||
|
{
|
||||||
|
Dispositivo = dispositivo,
|
||||||
|
IdTx = id,
|
||||||
|
IdRx = id,
|
||||||
|
funcCodeRx = fCodeRx,
|
||||||
|
DataRx = data,
|
||||||
|
ChaveInterna = chave
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
mensagem.DataRx = data;
|
mensagem.DataRx = data;
|
||||||
|
}
|
||||||
|
|
||||||
mensagem.Respondido = true;
|
mensagem.Respondido = true;
|
||||||
mensagem.RespondidoEm = DateTime.Now;
|
mensagem.RespondidoEm = DateTime.Now;
|
||||||
|
|
||||||
if ((handler?.Dispositivo ?? T_Code.Vzo) != T_Code.Vzo)
|
if (dispositivo != T_Code.Vzo)
|
||||||
{
|
{
|
||||||
handler.ProcessarMensagem(mensagem);
|
handler.ProcessarMensagem(mensagem);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
MostrarLog($"Roteador não registrado! ID_Num não tratado: 0x{id:X2}, fCodeRx: 0x{fCodeRx:X2}, frame: {frame}", (byte)id);
|
MostrarLog(
|
||||||
|
$"Roteador não registrado! ID CAN não tratado: 0x{id:X8}, fCodeRx: 0x{fCodeRx:X2}, frame: {frame}",
|
||||||
|
canalLog
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*byte[] data = new byte[len];
|
|
||||||
for (int i = 0; i < len; i++)
|
|
||||||
{
|
|
||||||
data[i] = Convert.ToByte(frame.Substring(5 + i * 2, 2), 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte funcCodeRx = data[0];
|
|
||||||
var mensagem = new CanMessage();
|
|
||||||
|
|
||||||
lock (Mensagens)
|
|
||||||
{
|
|
||||||
mensagem = Mensagens.Where(x => x.IdTx == id).OrderBy(x => x.EnviadoEm).FirstOrDefault(x => x.Enviado && !x.Respondido && x.funcCodeRx == funcCodeRx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mensagem == null)
|
|
||||||
{
|
|
||||||
mensagem = new CanMessage { IdTx = (byte)id, DataRx = data };
|
|
||||||
}
|
|
||||||
|
|
||||||
mensagem.DataRx = data;
|
|
||||||
mensagem.Respondido = true;
|
|
||||||
mensagem.RespondidoEm = DateTime.Now;
|
|
||||||
|
|
||||||
if (_roteadores.TryGetValue(mensagem.IdTx, out var handler))
|
|
||||||
{
|
|
||||||
handler.ProcessarMensagem(mensagem);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[CAN] ID_Num não tratado: 0x{funcCodeRx:X2}");
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[CAN] Erro ao processar frame: {ex.Message}");
|
Console.WriteLine($"[CAN] Erro ao processar frame: {ex.Message} (frame={frame})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AdicionarMensagemNaFila(T_Code Dispositivo, byte idTx, byte idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true)
|
public void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] _idTx, byte[] _idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true)
|
||||||
{
|
{
|
||||||
if (_PortaCAN?.IsOpen != true) return;
|
if (_PortaCAN?.IsOpen != true) return;
|
||||||
|
|
||||||
var dados = new List<byte>() { funcCodeTx };
|
uint idTx = CanManager.ConverterIdCan(_idTx);
|
||||||
|
uint idRx = CanManager.ConverterIdCan(_idRx);
|
||||||
|
|
||||||
|
var dados = new List<byte>();
|
||||||
|
if (Dispositivo != T_Code.Bat)
|
||||||
|
dados.Add(funcCodeTx);
|
||||||
|
|
||||||
if (payload != null)
|
if (payload != null)
|
||||||
dados.AddRange(payload);
|
dados.AddRange(payload);
|
||||||
|
|
||||||
if (Dispositivo == T_Code.Mks)
|
if (Dispositivo == T_Code.Mks)
|
||||||
{
|
{
|
||||||
byte crc = CalcularChecksum(idTx, dados.ToArray());
|
byte crc = CanManager.CalcularChecksum(idTx, dados.ToArray());
|
||||||
dados.Add(crc);
|
dados.Add(crc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -427,7 +460,7 @@ namespace AgroBase.Services
|
||||||
funcCodeRx = funcCodeRx,
|
funcCodeRx = funcCodeRx,
|
||||||
DataTx = dados.ToArray(),
|
DataTx = dados.ToArray(),
|
||||||
ComResposta = get,
|
ComResposta = get,
|
||||||
ChaveInterna = GerarChaveMensagem(Dispositivo, idRx, dados.ToArray())
|
ChaveInterna = CanManager.GerarChaveMensagem(Dispositivo, idRx, dados.ToArray())
|
||||||
};
|
};
|
||||||
|
|
||||||
bool mensagemNaFila = false;
|
bool mensagemNaFila = false;
|
||||||
|
|
@ -482,7 +515,7 @@ namespace AgroBase.Services
|
||||||
.OrderBy(x => x)
|
.OrderBy(x => x)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
byte proximoId;
|
uint proximoId;
|
||||||
|
|
||||||
if (!_ultimoIdEnviado.HasValue || !idsDisponiveis.Contains(_ultimoIdEnviado.Value))
|
if (!_ultimoIdEnviado.HasValue || !idsDisponiveis.Contains(_ultimoIdEnviado.Value))
|
||||||
{
|
{
|
||||||
|
|
@ -541,13 +574,34 @@ namespace AgroBase.Services
|
||||||
|
|
||||||
private string CriarFrame(CanMessage msg)
|
private string CriarFrame(CanMessage msg)
|
||||||
{
|
{
|
||||||
string frame = $"t{msg.IdTx:X3}{msg.DataTx.Length}";
|
bool isExtended = msg.IdTx > 0x7FF;
|
||||||
|
|
||||||
|
// Prefixo SLCAN
|
||||||
|
char tipo = isExtended ? 'T' : 't';
|
||||||
|
|
||||||
|
// ID em hex com largura correta
|
||||||
|
string idHex = isExtended
|
||||||
|
? msg.IdTx.ToString("X8") // 29 bits → 8 hex
|
||||||
|
: msg.IdTx.ToString("X3"); // 11 bits → 3 hex
|
||||||
|
|
||||||
|
// DLC
|
||||||
|
int dlc = msg.DataTx?.Length ?? 0;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.Append(tipo);
|
||||||
|
sb.Append(idHex);
|
||||||
|
sb.Append(dlc);
|
||||||
|
|
||||||
|
if (msg.DataTx != null)
|
||||||
|
{
|
||||||
foreach (byte b in msg.DataTx)
|
foreach (byte b in msg.DataTx)
|
||||||
frame += b.ToString("X2");
|
sb.Append(b.ToString("X2"));
|
||||||
|
}
|
||||||
|
|
||||||
frame += "\r";
|
sb.Append("\r");
|
||||||
|
|
||||||
return frame;
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool EnviarComando(string comando)
|
private bool EnviarComando(string comando)
|
||||||
|
|
@ -659,26 +713,7 @@ namespace AgroBase.Services
|
||||||
Fechar();
|
Fechar();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte CalcularChecksum(byte id, byte[] data)
|
|
||||||
{
|
|
||||||
int soma = id;
|
|
||||||
foreach (var b in data)
|
|
||||||
soma += b;
|
|
||||||
return (byte)(soma & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GerarChaveMensagem(T_Code Dispositivo, byte Id, byte[] data)
|
|
||||||
{
|
|
||||||
byte parametro = 0x00;
|
|
||||||
|
|
||||||
// Para dispositivos que não sejam MKS, usar DataTx[1] se existir
|
|
||||||
if (Dispositivo != T_Code.Mks && (data?.Length ?? 0) > 1)
|
|
||||||
{
|
|
||||||
parametro = data[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{Id}_{data[0]}_{parametro}";
|
|
||||||
}
|
|
||||||
|
|
||||||
// alvo: ~200 fps (ajuste conforme bitrate/carga)
|
// alvo: ~200 fps (ajuste conforme bitrate/carga)
|
||||||
private const double TARGET_FPS = 200;
|
private const double TARGET_FPS = 200;
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,9 @@ namespace AgroBase.Services
|
||||||
private AsyncTaskTimerModel tmrProcessarCAN;
|
private AsyncTaskTimerModel tmrProcessarCAN;
|
||||||
private StringBuilder SerialCANBuffer = new StringBuilder();
|
private StringBuilder SerialCANBuffer = new StringBuilder();
|
||||||
private readonly ConcurrentQueue<(uint id, byte[] data)> FramesRecebidos = new ConcurrentQueue<(uint, byte[])>();
|
private readonly ConcurrentQueue<(uint id, byte[] data)> FramesRecebidos = new ConcurrentQueue<(uint, byte[])>();
|
||||||
private readonly ConcurrentDictionary<byte, ICanMessageHandler> _roteadores = new ConcurrentDictionary<byte, ICanMessageHandler>();
|
private readonly ConcurrentDictionary<uint, ICanMessageHandler> _roteadores = new ConcurrentDictionary<uint, ICanMessageHandler>();
|
||||||
private readonly ConcurrentDictionary<byte, bool> _roteadoresRegistrados = new ConcurrentDictionary<byte, bool>();
|
private readonly ConcurrentDictionary<uint, bool> _roteadoresRegistrados = new ConcurrentDictionary<uint, bool>();
|
||||||
private byte? _ultimoIdEnviado = null;
|
private uint? _ultimoIdEnviado = null;
|
||||||
private List<CanMessage> MensagensPendentes = new List<CanMessage>();
|
private List<CanMessage> MensagensPendentes = new List<CanMessage>();
|
||||||
private readonly object _LockMensagens = new object();
|
private readonly object _LockMensagens = new object();
|
||||||
private bool DebugMode = false;
|
private bool DebugMode = false;
|
||||||
|
|
@ -60,9 +60,9 @@ namespace AgroBase.Services
|
||||||
public double UltimoErroCritico { get; set; } = 0;
|
public double UltimoErroCritico { get; set; } = 0;
|
||||||
private DateTime _ultimoErroCritico = DateTime.MinValue;
|
private DateTime _ultimoErroCritico = DateTime.MinValue;
|
||||||
|
|
||||||
private void MostrarLog(string message, byte id)
|
private void MostrarLog(string message, uint id)
|
||||||
{
|
{
|
||||||
if (DebugMode/* && new List<byte>() { 0x01 }.Contains(id)*/)
|
if (DebugMode/* && new List<uint>() { 0x01 }.Contains(id)*/)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[CAN] {message}");
|
Console.WriteLine($"[CAN] {message}");
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +73,7 @@ namespace AgroBase.Services
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegistrarHandler(byte endereco_rx, ICanMessageHandler handler)
|
public void RegistrarHandler(uint endereco_rx, ICanMessageHandler handler)
|
||||||
{
|
{
|
||||||
_roteadoresRegistrados.TryGetValue(endereco_rx, out bool registrado);
|
_roteadoresRegistrados.TryGetValue(endereco_rx, out bool registrado);
|
||||||
if (!registrado)
|
if (!registrado)
|
||||||
|
|
@ -186,28 +186,52 @@ namespace AgroBase.Services
|
||||||
uint id = frame.Item1;
|
uint id = frame.Item1;
|
||||||
byte[] data = frame.Item2;
|
byte[] data = frame.Item2;
|
||||||
|
|
||||||
if (data == null || data.Length == 0) return;
|
if (data == null || data.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Aqui você consegue saber se é extended ou standard:
|
||||||
|
bool isExtended = id > 0x7FF;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
MostrarLog($"Processando frame: {BitConverter.ToString(data)}", (byte)id);
|
// opcional: deixar o log mais informativo
|
||||||
|
string tipoId = isExtended ? "EXT" : "STD";
|
||||||
|
MostrarLog($"Processando frame ({tipoId}) ID=0x{id:X}, Data={BitConverter.ToString(data)}", id);
|
||||||
|
|
||||||
_roteadores.TryGetValue((byte)id, out ICanMessageHandler handler);
|
_roteadores.TryGetValue(id, out ICanMessageHandler handler);
|
||||||
|
|
||||||
T_Code dispositivo = handler?.Dispositivo ?? T_Code.Vzo;
|
T_Code dispositivo = handler?.Dispositivo ?? T_Code.Vzo;
|
||||||
byte fCodeRx = data[0];
|
bool db = dispositivo == T_Code.Bat;
|
||||||
byte parametroRx = data.Length > 1 ? data[1] : (byte)0x00;
|
byte fCodeRx = db ? (byte)DalyBMSService.GetDataId(id) : data.Length > 0 ? data[0] : (byte)0x00;
|
||||||
string chave = GerarChaveMensagem(dispositivo, (byte)id, data);
|
byte parametroRx = db ? (byte)0x00 : data.Length > 1 ? data[1] : (byte)0x00;
|
||||||
|
string chave = CanManager.GerarChaveMensagem(dispositivo, id, data);
|
||||||
|
|
||||||
CanMessage mensagem = null;
|
CanMessage mensagem = null;
|
||||||
lock (_LockMensagens)
|
lock (_LockMensagens)
|
||||||
{
|
{
|
||||||
mensagem = MensagensPendentes.FirstOrDefault(x => x.Enviado && !x.Respondido && x.Dispositivo == dispositivo && x.IdRx == id && x.DataTx[1] == data[1]);
|
mensagem = MensagensPendentes.FirstOrDefault(x =>
|
||||||
|
x.Enviado &&
|
||||||
|
!x.Respondido &&
|
||||||
|
x.Dispositivo == dispositivo &&
|
||||||
|
x.IdRx == id && // usa o ID completo (11 ou 29 bits)
|
||||||
|
x.DataTx != null &&
|
||||||
|
x.DataTx.Length > 1 &&
|
||||||
|
(db ? true : x.DataTx[1] == data[1])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mensagem == null)
|
if (mensagem == null)
|
||||||
{
|
{
|
||||||
mensagem = new CanMessage { Dispositivo = dispositivo, IdTx = (byte)id, IdRx = (byte)id, funcCodeRx = fCodeRx, DataRx = data, ChaveInterna = chave };
|
mensagem = new CanMessage
|
||||||
|
{
|
||||||
|
Dispositivo = dispositivo,
|
||||||
|
IdRx = id,
|
||||||
|
funcCodeRx = fCodeRx,
|
||||||
|
DataRx = data,
|
||||||
|
ChaveInterna = chave
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mensagem.DataRx = data;
|
mensagem.DataRx = data;
|
||||||
mensagem.Respondido = true;
|
mensagem.Respondido = true;
|
||||||
mensagem.RespondidoEm = DateTime.Now;
|
mensagem.RespondidoEm = DateTime.Now;
|
||||||
|
|
@ -218,27 +242,33 @@ namespace AgroBase.Services
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
MostrarLog($"Roteador não registrado! ID_Num não tratado: 0x{id:X2}, fCodeRx: 0x{fCodeRx:X2}, frame: {frame}", (byte)id);
|
MostrarLog($"Roteador não registrado! ID_Num não tratado: 0x{id:X2}, fCodeRx: 0x{fCodeRx:X2}, frame: {BitConverter.ToString(data)}", (byte)id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MostrarLog($"Erro ao processar frame! Frame: {frame}, Erro: {ex.Message}", (byte)id);
|
MostrarLog($"Erro ao processar frame! Frame: {BitConverter.ToString(data)}, Erro: {ex.Message}", (byte)id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AdicionarMensagemNaFila(T_Code Dispositivo, byte idTx, byte idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true)
|
|
||||||
|
public void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] _idTx, byte[] _idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true)
|
||||||
{
|
{
|
||||||
if (!Connector.IsOpen) return;
|
if (!Connector.IsOpen) return;
|
||||||
|
|
||||||
var dados = new List<byte>() { funcCodeTx };
|
uint idTx = CanManager.ConverterIdCan(_idTx);
|
||||||
|
uint idRx = CanManager.ConverterIdCan(_idRx);
|
||||||
|
|
||||||
|
var dados = new List<byte>();
|
||||||
|
if (Dispositivo != T_Code.Bat)
|
||||||
|
dados.Add(funcCodeTx);
|
||||||
|
|
||||||
if (payload != null)
|
if (payload != null)
|
||||||
dados.AddRange(payload);
|
dados.AddRange(payload);
|
||||||
|
|
||||||
if (Dispositivo == T_Code.Mks)
|
if (Dispositivo == T_Code.Mks)
|
||||||
{
|
{
|
||||||
byte crc = CalcularChecksum(idTx, dados.ToArray());
|
byte crc = CanManager.CalcularChecksum(idTx, dados.ToArray());
|
||||||
dados.Add(crc);
|
dados.Add(crc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,7 +282,7 @@ namespace AgroBase.Services
|
||||||
funcCodeRx = funcCodeRx,
|
funcCodeRx = funcCodeRx,
|
||||||
DataTx = dados.ToArray(),
|
DataTx = dados.ToArray(),
|
||||||
ComResposta = get,
|
ComResposta = get,
|
||||||
ChaveInterna = GerarChaveMensagem(Dispositivo, idRx, dados.ToArray())
|
ChaveInterna = CanManager.GerarChaveMensagem(Dispositivo, idRx, dados.ToArray())
|
||||||
};
|
};
|
||||||
|
|
||||||
bool mensagemNaFila = false;
|
bool mensagemNaFila = false;
|
||||||
|
|
@ -307,7 +337,7 @@ namespace AgroBase.Services
|
||||||
.OrderBy(x => x)
|
.OrderBy(x => x)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
byte proximoId;
|
uint proximoId;
|
||||||
|
|
||||||
if (!_ultimoIdEnviado.HasValue || !idsDisponiveis.Contains(_ultimoIdEnviado.Value))
|
if (!_ultimoIdEnviado.HasValue || !idsDisponiveis.Contains(_ultimoIdEnviado.Value))
|
||||||
{
|
{
|
||||||
|
|
@ -328,7 +358,7 @@ namespace AgroBase.Services
|
||||||
return proximaMsg;
|
return proximaMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<byte, DateTime> _cooldownPorId = new ConcurrentDictionary<byte, DateTime>();
|
private readonly ConcurrentDictionary<uint, DateTime> _cooldownPorId = new ConcurrentDictionary<uint, DateTime>();
|
||||||
private static readonly TimeSpan COOLDOWN_MKS = TimeSpan.FromMilliseconds(6);
|
private static readonly TimeSpan COOLDOWN_MKS = TimeSpan.FromMilliseconds(6);
|
||||||
|
|
||||||
private bool PodeEnviarAgora(CanMessage m)
|
private bool PodeEnviarAgora(CanMessage m)
|
||||||
|
|
@ -382,20 +412,42 @@ namespace AgroBase.Services
|
||||||
|
|
||||||
private string CriarFrame(CanMessage msg)
|
private string CriarFrame(CanMessage msg)
|
||||||
{
|
{
|
||||||
string frame = $"t{msg.IdTx:X3}{msg.DataTx.Length}";
|
// Extended se passar do limite de 11 bits
|
||||||
|
bool isExtended = msg.IdTx > 0x7FF;
|
||||||
|
|
||||||
|
// Prefixo SLCAN só para LOG
|
||||||
|
// t = standard (11 bits)
|
||||||
|
// T = extended (29 bits)
|
||||||
|
char tipo = isExtended ? 'T' : 't';
|
||||||
|
|
||||||
|
// ID em hex com largura correta
|
||||||
|
string idHex = isExtended
|
||||||
|
? msg.IdTx.ToString("X8") // 29 bits → 8 hex
|
||||||
|
: msg.IdTx.ToString("X3"); // 11 bits → 3 hex
|
||||||
|
|
||||||
|
// DLC
|
||||||
|
int dlc = msg.DataTx?.Length ?? 0;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append(tipo);
|
||||||
|
sb.Append(idHex);
|
||||||
|
sb.Append(dlc);
|
||||||
|
|
||||||
|
if (msg.DataTx != null)
|
||||||
|
{
|
||||||
foreach (byte b in msg.DataTx)
|
foreach (byte b in msg.DataTx)
|
||||||
frame += b.ToString("X2");
|
sb.Append(b.ToString("X2"));
|
||||||
|
}
|
||||||
|
|
||||||
frame += "\r";
|
sb.Append('\r');
|
||||||
|
return sb.ToString();
|
||||||
return frame;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool EnviarComando(string comando, CanMessage msg = null)
|
private bool EnviarComando(string comando, CanMessage msg = null)
|
||||||
{
|
{
|
||||||
if (Connector.IsOpen)
|
if (Connector.IsOpen)
|
||||||
{
|
{
|
||||||
bool sucesso = Connector.Send(msg.IdTx, msg.DataTx);
|
bool sucesso = Connector.Send(msg.IdTx, msg.DataTx, isExtended: msg.IsExtended);
|
||||||
|
|
||||||
if (sucesso)
|
if (sucesso)
|
||||||
{
|
{
|
||||||
|
|
@ -485,27 +537,6 @@ namespace AgroBase.Services
|
||||||
Fechar();
|
Fechar();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte CalcularChecksum(byte id, byte[] data)
|
|
||||||
{
|
|
||||||
int soma = id;
|
|
||||||
foreach (var b in data)
|
|
||||||
soma += b;
|
|
||||||
return (byte)(soma & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GerarChaveMensagem(T_Code Dispositivo, byte Id, byte[] data)
|
|
||||||
{
|
|
||||||
if (data == null || data.Length == 0) return $"{Id}_empty";
|
|
||||||
|
|
||||||
// MKS: chaveia por funcCode apenas
|
|
||||||
if (Dispositivo == T_Code.Mks)
|
|
||||||
return $"{Id}_{data[0]}";
|
|
||||||
|
|
||||||
// Outros: mantém param se houver
|
|
||||||
byte parametro = (data.Length > 1) ? data[1] : (byte)0x00;
|
|
||||||
return $"{Id}_{data[0]}_{parametro}";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// alvo: ~200 fps (ajuste conforme bitrate/carga)
|
// alvo: ~200 fps (ajuste conforme bitrate/carga)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,491 @@
|
||||||
|
using AgroBase.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static AgroBase.Models.Enums;
|
||||||
|
using static AgroBase.Services.DalyBMSService.BatHandler;
|
||||||
|
|
||||||
|
namespace AgroBase.Services
|
||||||
|
{
|
||||||
|
public class DalyBMSService
|
||||||
|
{
|
||||||
|
public static DalyBmsModel DadosLeitura { get; set; } = new DalyBmsModel();
|
||||||
|
private static T_Code Dispositivo { get; set; } = T_Code.Bat;
|
||||||
|
public static BatHandler _BatHandler { get; set; } = new BatHandler();
|
||||||
|
public static bool Iniciado
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return CanManager.CanService.IsConnected && SerialService.DispositivosMapeados.Any(x => x.Dispositivo == Dispositivo) && DadosLeitura.Iniciado;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double TempoAtualizacao { get; set; } = 2;
|
||||||
|
private static DateTime UltimaAtualizacao { get; set; } = DateTime.MinValue;
|
||||||
|
|
||||||
|
public static async Task<bool> VerificaDispositivoConectado()
|
||||||
|
{
|
||||||
|
if (!CanManager.CanService.IsConnected) return false;
|
||||||
|
|
||||||
|
foreach (BatFuncCode f in Enum.GetValues(typeof(BatFuncCode)))
|
||||||
|
{
|
||||||
|
CanManager.CanService.RegistrarHandler(CanManager.ConverterIdCan(MakeResponseID(f)), _BatHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
RequisitarDado(BatFuncCode.SOC);
|
||||||
|
|
||||||
|
DateTime Inicio = DateTime.Now;
|
||||||
|
bool Respondido() => DadosLeitura.UltimoComandoRecebido > Inicio;
|
||||||
|
await FuncoesGlobais.AguardarCondicaoAsync(Respondido, 500, 50);
|
||||||
|
|
||||||
|
if (Respondido())
|
||||||
|
{
|
||||||
|
DefinirDispositivo();
|
||||||
|
}
|
||||||
|
|
||||||
|
return DadosLeitura.Iniciado;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DefinirDispositivo()
|
||||||
|
{
|
||||||
|
if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == Dispositivo))
|
||||||
|
{
|
||||||
|
if (DadosLeitura != null)
|
||||||
|
{
|
||||||
|
SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel()
|
||||||
|
{
|
||||||
|
Dispositivo = Dispositivo,
|
||||||
|
Endereco = CanManager.CanService._portName,
|
||||||
|
Versao = ("1").ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static void AtualizarDados()
|
||||||
|
{
|
||||||
|
if (!Iniciado)
|
||||||
|
return;
|
||||||
|
|
||||||
|
double dt = (DateTime.Now - UltimaAtualizacao).TotalSeconds;
|
||||||
|
if (dt < TempoAtualizacao)
|
||||||
|
return;
|
||||||
|
|
||||||
|
RequisitarDado(BatFuncCode.SOC);
|
||||||
|
RequisitarDado(BatFuncCode.TensaoMaxMin);
|
||||||
|
RequisitarDado(BatFuncCode.TensaoCelula);
|
||||||
|
RequisitarDado(BatFuncCode.TemperaturaMaxMin);
|
||||||
|
RequisitarDado(BatFuncCode.TemperaturaCelula);
|
||||||
|
RequisitarDado(BatFuncCode.CargaDescargaMOS);
|
||||||
|
RequisitarDado(BatFuncCode.StatusInformacao);
|
||||||
|
RequisitarDado(BatFuncCode.StatusBalanceamentoCelula);
|
||||||
|
RequisitarDado(BatFuncCode.StatusFalhaBateria);
|
||||||
|
|
||||||
|
UltimaAtualizacao = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Helpers de protocolo (ID, DataID, etc.)
|
||||||
|
|
||||||
|
private static void EnviarComando(T_Code Dispositivo, byte[] idTx, byte[] idRx, byte funcCode, byte[] payload = null)
|
||||||
|
{
|
||||||
|
CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, idTx, idRx, funcCode, funcCode, payload, get: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endereços lógicos que você quiser adotar para PC e BMS.
|
||||||
|
// São os "AddrA" e "AddrB" da fórmula 0x18 [DataID] [AddrA] [AddrB].
|
||||||
|
public static byte PriorityAddr { get; set; } = 0x18;
|
||||||
|
public static byte BmsAddr { get; set; } = 0x01;
|
||||||
|
public static byte PcAddr { get; set; } = 0x40;
|
||||||
|
|
||||||
|
public static BatFuncCode GetDataId(uint canId)
|
||||||
|
{
|
||||||
|
return (BatFuncCode)((canId >> 16) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] MakeRequestID(BatFuncCode dado)
|
||||||
|
{
|
||||||
|
return new byte[4] { PriorityAddr, (byte)dado, BmsAddr, PcAddr };
|
||||||
|
//return (uint)((0x18 << 24) | ((byte)dado << 16) | (BmsAddr << 8) | PcAddr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] MakeResponseID(BatFuncCode dado)
|
||||||
|
{
|
||||||
|
return new byte[4] { PriorityAddr, (byte)dado, PcAddr, BmsAddr };
|
||||||
|
//return (uint)((0x18 << 24) | ((byte)dado << 16) | (PcAddr << 8) | BmsAddr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RequisitarDado(BatFuncCode dado)
|
||||||
|
{
|
||||||
|
EnviarComando(Dispositivo, MakeRequestID(dado), MakeResponseID(dado), (byte)dado, payload: new byte[8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ushort DecodeUInt16BE(byte hi, byte lo)
|
||||||
|
{
|
||||||
|
return (ushort)((hi << 8) | lo);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public class BatHandler : ICanMessageHandler
|
||||||
|
{
|
||||||
|
public T_Code Dispositivo { get; set; } = DalyBMSService.Dispositivo;
|
||||||
|
|
||||||
|
public enum BatFuncCode : byte
|
||||||
|
{
|
||||||
|
SOC = 0x90,
|
||||||
|
TensaoMaxMin = 0x91,
|
||||||
|
TemperaturaMaxMin = 0x92,
|
||||||
|
CargaDescargaMOS = 0x93,
|
||||||
|
StatusInformacao = 0x94,
|
||||||
|
TensaoCelula = 0x95,
|
||||||
|
TemperaturaCelula = 0x96,
|
||||||
|
StatusBalanceamentoCelula = 0x97,
|
||||||
|
StatusFalhaBateria = 0x98
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessarMensagem(CanMessage mensagem)
|
||||||
|
{
|
||||||
|
if (mensagem == null) return;
|
||||||
|
if (mensagem.DataRx == null || mensagem.DataRx.Length < 8) return;
|
||||||
|
|
||||||
|
// TODO: trocar "mensagem.CanId" pelo nome real da propriedade que guarda o ID CAN 29 bits
|
||||||
|
uint canId = mensagem.IdRx;
|
||||||
|
|
||||||
|
BatFuncCode dataId = GetDataId(canId);
|
||||||
|
byte[] data = mensagem.DataRx;
|
||||||
|
|
||||||
|
lock (DadosLeitura)
|
||||||
|
{
|
||||||
|
DadosLeitura.Momento = DateTime.Now;
|
||||||
|
DadosLeitura.Iniciado = true;
|
||||||
|
DadosLeitura.UltimoComandoRecebido = DateTime.Now;
|
||||||
|
|
||||||
|
// Guarda payload bruto por DataID para debug
|
||||||
|
DadosLeitura.LeiturasBrutas[(byte)dataId] = data.ToArray();
|
||||||
|
|
||||||
|
switch (dataId)
|
||||||
|
{
|
||||||
|
case BatFuncCode.SOC:
|
||||||
|
Atualizar090_PackVoltageCurrentSoc(data);
|
||||||
|
break;
|
||||||
|
case BatFuncCode.TensaoMaxMin:
|
||||||
|
Atualizar091_CellMaxMin(data);
|
||||||
|
break;
|
||||||
|
case BatFuncCode.TemperaturaMaxMin:
|
||||||
|
Atualizar092_TempMaxMin(data);
|
||||||
|
break;
|
||||||
|
case BatFuncCode.CargaDescargaMOS:
|
||||||
|
Atualizar093_MosEstadoCapacidade(data);
|
||||||
|
break;
|
||||||
|
case BatFuncCode.StatusInformacao:
|
||||||
|
Atualizar094_StatusGeralIo(data);
|
||||||
|
break;
|
||||||
|
case BatFuncCode.TensaoCelula:
|
||||||
|
Atualizar095_TensoesCelulaFrame(data);
|
||||||
|
break;
|
||||||
|
case BatFuncCode.TemperaturaCelula:
|
||||||
|
Atualizar096_TemperaturasFrame(data);
|
||||||
|
break;
|
||||||
|
case BatFuncCode.StatusBalanceamentoCelula:
|
||||||
|
Atualizar097_Balanceamento(data);
|
||||||
|
break;
|
||||||
|
case BatFuncCode.StatusFalhaBateria:
|
||||||
|
Atualizar098_Alarmes(data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Outros DataID (se existirem) podem ser tratados aqui no futuro.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Atualizar090_PackVoltageCurrentSoc(byte[] d)
|
||||||
|
{
|
||||||
|
// 0..1: total voltage (0.1 V)
|
||||||
|
// 2..3: gather voltage (0.1 V)
|
||||||
|
// 4..5: current (offset 30000, 0.1 A)
|
||||||
|
// 6..7: SOC (0.1 %)
|
||||||
|
ushort totalVRaw = DecodeUInt16BE(d[0], d[1]);
|
||||||
|
ushort measVRaw = DecodeUInt16BE(d[2], d[3]);
|
||||||
|
ushort currRaw = DecodeUInt16BE(d[4], d[5]);
|
||||||
|
ushort socRaw = DecodeUInt16BE(d[6], d[7]);
|
||||||
|
|
||||||
|
double totalV = totalVRaw / 10.0;
|
||||||
|
double measV = measVRaw / 10.0;
|
||||||
|
double current = (currRaw - 30000) / 10.0;
|
||||||
|
double soc = socRaw / 10.0;
|
||||||
|
|
||||||
|
DadosLeitura.Pack.TensaoTotal_V = totalV;
|
||||||
|
DadosLeitura.Pack.TensaoMedida_V = measV;
|
||||||
|
DadosLeitura.Pack.Corrente_A = current;
|
||||||
|
DadosLeitura.Pack.SOC_percent = soc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Atualizar091_CellMaxMin(byte[] d)
|
||||||
|
{
|
||||||
|
// 0..1: Vmax (mV)
|
||||||
|
// 2 : index max
|
||||||
|
// 3..4: Vmin (mV)
|
||||||
|
// 5 : index min
|
||||||
|
ushort vmaxRaw = DecodeUInt16BE(d[0], d[1]);
|
||||||
|
byte idxMax = d[2];
|
||||||
|
ushort vminRaw = DecodeUInt16BE(d[3], d[4]);
|
||||||
|
byte idxMin = d[5];
|
||||||
|
|
||||||
|
double vmax = vmaxRaw / 1000.0;
|
||||||
|
double vmin = vminRaw / 1000.0;
|
||||||
|
|
||||||
|
// Marca flags nas células existentes
|
||||||
|
foreach (var c in DadosLeitura.Celulas)
|
||||||
|
{
|
||||||
|
c.EhMaxima = (c.Index == idxMax);
|
||||||
|
c.EhMinima = (c.Index == idxMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualiza no modelo principal
|
||||||
|
DadosLeitura.TensaoMax = vmax;
|
||||||
|
DadosLeitura.TensaoMin = vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Atualizar092_TempMaxMin(byte[] d)
|
||||||
|
{
|
||||||
|
// T = raw - 40
|
||||||
|
int tmax = d[0] - 40;
|
||||||
|
byte idxMax = d[1];
|
||||||
|
int tmin = d[2] - 40;
|
||||||
|
byte idxMin = d[3];
|
||||||
|
|
||||||
|
DadosLeitura.TemperaturaMax = tmax;
|
||||||
|
DadosLeitura.TemperaturaMin = tmin;
|
||||||
|
|
||||||
|
foreach (var t in DadosLeitura.Temperaturas)
|
||||||
|
{
|
||||||
|
t.EhMaxima = (t.Index == idxMax);
|
||||||
|
t.EhMinima = (t.Index == idxMin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Atualizar093_MosEstadoCapacidade(byte[] d)
|
||||||
|
{
|
||||||
|
// 0: state (0=idle,1=charge,2=discharge)
|
||||||
|
// 1: charge MOS
|
||||||
|
// 2: discharge MOS
|
||||||
|
// 3: life (raw)
|
||||||
|
// 4..7: remaining capacity (mAh)
|
||||||
|
byte state = d[0];
|
||||||
|
byte chargeMos = d[1];
|
||||||
|
byte dischgMos = d[2];
|
||||||
|
byte lifeRaw = d[3];
|
||||||
|
uint remainmAh = (uint)((d[4] << 24) | (d[5] << 16) | (d[6] << 8) | d[7]);
|
||||||
|
|
||||||
|
DadosLeitura.Pack.EstadoGeral = state;
|
||||||
|
DadosLeitura.Pack.ChargeMosOn = chargeMos != 0;
|
||||||
|
DadosLeitura.Pack.DischargeMosOn = dischgMos != 0;
|
||||||
|
DadosLeitura.Pack.CiclosAproximados = lifeRaw;
|
||||||
|
DadosLeitura.Pack.CapacidadeRestante_Ah = remainmAh / 1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Atualizar094_StatusGeralIo(byte[] d)
|
||||||
|
{
|
||||||
|
// 0: n series cells
|
||||||
|
// 1: n temps
|
||||||
|
// 2: charger conected
|
||||||
|
// 3: load connected
|
||||||
|
// 4: IO bits (0-3 DI, 4-7 DO)
|
||||||
|
byte nSeries = d[0];
|
||||||
|
byte nTemps = d[1];
|
||||||
|
byte charger = d[2];
|
||||||
|
byte load = d[3];
|
||||||
|
byte ioByte = d[4];
|
||||||
|
|
||||||
|
DadosLeitura.Pack.NumeroCelulasSerie = nSeries;
|
||||||
|
DadosLeitura.Pack.NumeroSensoresTemperatura = nTemps;
|
||||||
|
DadosLeitura.Pack.CarregadorConectado = (charger != 0);
|
||||||
|
DadosLeitura.Pack.LoadConectado = (load != 0);
|
||||||
|
DadosLeitura.Pack.IoStatusRaw = ioByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Atualizar095_TensoesCelulaFrame(byte[] d)
|
||||||
|
{
|
||||||
|
// d[0] = frame index
|
||||||
|
// d[1..2], [3..4], [5..6] = até 3 tensões de célula (mV)
|
||||||
|
byte frame = d[0];
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
int hiIndex = 1 + 2 * i;
|
||||||
|
int loIndex = hiIndex + 1;
|
||||||
|
if (loIndex >= d.Length)
|
||||||
|
break;
|
||||||
|
|
||||||
|
ushort raw = DecodeUInt16BE(d[hiIndex], d[loIndex]);
|
||||||
|
if (raw == 0 || raw == 0xFFFF)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// índice da célula é frame*3 + i + 1 (1-based)
|
||||||
|
int cellIndex = frame * 3 + i + 1;
|
||||||
|
double v = raw / 1000.0;
|
||||||
|
|
||||||
|
var cell = DadosLeitura.Celulas.FirstOrDefault(c => c.Index == cellIndex);
|
||||||
|
if (cell == null)
|
||||||
|
{
|
||||||
|
cell = new DalyCellVoltage
|
||||||
|
{
|
||||||
|
Index = cellIndex
|
||||||
|
};
|
||||||
|
DadosLeitura.Celulas.Add(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.Tensao_V = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Atualizar096_TemperaturasFrame(byte[] d)
|
||||||
|
{
|
||||||
|
// d[0] = frame index
|
||||||
|
// d[1..7] = até 7 sensores (raw = temp + 40, 0xFF=inválido)
|
||||||
|
byte frame = d[0];
|
||||||
|
|
||||||
|
for (int i = 1; i < 8 && i < d.Length; i++)
|
||||||
|
{
|
||||||
|
byte raw = d[i];
|
||||||
|
if (raw == 0xFF)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int tempC = raw - 40;
|
||||||
|
int sensorIndex = frame * 7 + (i - 1) + 1;
|
||||||
|
|
||||||
|
var sensor = DadosLeitura.Temperaturas.FirstOrDefault(t => t.Index == sensorIndex);
|
||||||
|
if (sensor == null)
|
||||||
|
{
|
||||||
|
sensor = new DalyTemperatureSensor
|
||||||
|
{
|
||||||
|
Index = sensorIndex
|
||||||
|
};
|
||||||
|
DadosLeitura.Temperaturas.Add(sensor);
|
||||||
|
}
|
||||||
|
|
||||||
|
sensor.Temperatura_C = tempC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Atualizar097_Balanceamento(byte[] d)
|
||||||
|
{
|
||||||
|
// 6 bytes (ou 8) de bits: cada bit = 1 célula (até 48 células)
|
||||||
|
// Ex: bit0 do byte0 => célula1, bit1=>célula2, ...
|
||||||
|
var map = new Dictionary<int, bool>();
|
||||||
|
|
||||||
|
for (int byteIndex = 0; byteIndex < d.Length; byteIndex++)
|
||||||
|
{
|
||||||
|
byte b = d[byteIndex];
|
||||||
|
for (int bit = 0; bit < 8; bit++)
|
||||||
|
{
|
||||||
|
int globalBit = byteIndex * 8 + bit;
|
||||||
|
if (globalBit >= 48) continue;
|
||||||
|
|
||||||
|
bool ativo = ((b >> bit) & 0x01) == 1;
|
||||||
|
int cellIndex = globalBit + 1;
|
||||||
|
|
||||||
|
map[cellIndex] = ativo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DadosLeitura.Balanceamento.CelulasEmBalanceamento = map;
|
||||||
|
|
||||||
|
// Atualiza flag direto nas células
|
||||||
|
foreach (var c in DadosLeitura.Celulas)
|
||||||
|
{
|
||||||
|
c.EmBalanceamento = map.TryGetValue(c.Index, out bool ativo) && ativo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Atualizar098_Alarmes(byte[] d)
|
||||||
|
{
|
||||||
|
// d[0..6] = bits de alarme, d[7] = fault code
|
||||||
|
var mensagens = new List<string>();
|
||||||
|
|
||||||
|
void AddIf(byte b, int bit, string msg)
|
||||||
|
{
|
||||||
|
if (((b >> bit) & 0x01) == 1)
|
||||||
|
mensagens.Add(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte b0 = d[0];
|
||||||
|
AddIf(b0, 0, "Cell volt high level 1");
|
||||||
|
AddIf(b0, 1, "Cell volt high level 2");
|
||||||
|
AddIf(b0, 2, "Cell volt low level 1");
|
||||||
|
AddIf(b0, 3, "Cell volt low level 2");
|
||||||
|
AddIf(b0, 4, "Sum volt high level 1");
|
||||||
|
AddIf(b0, 5, "Sum volt high level 2");
|
||||||
|
AddIf(b0, 6, "Sum volt low level 1");
|
||||||
|
AddIf(b0, 7, "Sum volt low level 2");
|
||||||
|
|
||||||
|
byte b1 = d[1];
|
||||||
|
AddIf(b1, 0, "Chg temp high level 1");
|
||||||
|
AddIf(b1, 1, "Chg temp high level 2");
|
||||||
|
AddIf(b1, 2, "Chg temp low level 1");
|
||||||
|
AddIf(b1, 3, "Chg temp low level 2");
|
||||||
|
AddIf(b1, 4, "Dischg temp high level 1");
|
||||||
|
AddIf(b1, 5, "Dischg temp high level 2");
|
||||||
|
AddIf(b1, 6, "Dischg temp low level 1");
|
||||||
|
AddIf(b1, 7, "Dischg temp low level 2");
|
||||||
|
|
||||||
|
byte b2 = d[2];
|
||||||
|
AddIf(b2, 0, "Chg overcurrent level 1");
|
||||||
|
AddIf(b2, 1, "Chg overcurrent level 2");
|
||||||
|
AddIf(b2, 2, "Dischg overcurrent level 1");
|
||||||
|
AddIf(b2, 3, "Dischg overcurrent level 2");
|
||||||
|
AddIf(b2, 4, "SOC high level 1");
|
||||||
|
AddIf(b2, 5, "SOC high level 2");
|
||||||
|
AddIf(b2, 6, "SOC low level 1");
|
||||||
|
AddIf(b2, 7, "SOC low level 2");
|
||||||
|
|
||||||
|
byte b3 = d[3];
|
||||||
|
AddIf(b3, 0, "Diff volt level 1");
|
||||||
|
AddIf(b3, 1, "Diff volt level 2");
|
||||||
|
AddIf(b3, 2, "Diff temp level 1");
|
||||||
|
AddIf(b3, 3, "Diff temp level 2");
|
||||||
|
|
||||||
|
byte b4 = d[4];
|
||||||
|
AddIf(b4, 0, "Chg MOS temp high alarm");
|
||||||
|
AddIf(b4, 1, "Dischg MOS temp high alarm");
|
||||||
|
AddIf(b4, 2, "Chg MOS temp sensor error");
|
||||||
|
AddIf(b4, 3, "Dischg MOS temp sensor error");
|
||||||
|
AddIf(b4, 4, "Chg MOS adhesion error");
|
||||||
|
AddIf(b4, 5, "Dischg MOS adhesion error");
|
||||||
|
AddIf(b4, 6, "Chg MOS open circuit error");
|
||||||
|
AddIf(b4, 7, "Dischg MOS open circuit error");
|
||||||
|
|
||||||
|
byte b5 = d[5];
|
||||||
|
AddIf(b5, 0, "AFE collect chip error");
|
||||||
|
AddIf(b5, 1, "Voltage collect dropped");
|
||||||
|
AddIf(b5, 2, "Cell temp sensor error");
|
||||||
|
AddIf(b5, 3, "EEPROM error");
|
||||||
|
AddIf(b5, 4, "RTC error");
|
||||||
|
AddIf(b5, 5, "Precharge failure");
|
||||||
|
AddIf(b5, 6, "Communication failure");
|
||||||
|
AddIf(b5, 7, "Internal communication failure");
|
||||||
|
|
||||||
|
byte b6 = d[6];
|
||||||
|
AddIf(b6, 0, "Current module fault");
|
||||||
|
AddIf(b6, 1, "Sum voltage detect fault");
|
||||||
|
AddIf(b6, 2, "Short circuit protect fault");
|
||||||
|
AddIf(b6, 3, "Low volt forbidden chg fault");
|
||||||
|
|
||||||
|
byte faultCode = d[7];
|
||||||
|
|
||||||
|
DadosLeitura.Alarmes.Mensagens = mensagens;
|
||||||
|
DadosLeitura.Alarmes.FaultCode = faultCode;
|
||||||
|
DadosLeitura.Alarmes.AlarmBytesRaw = d.Take(7).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -74,7 +74,7 @@ namespace AgroBase.Comum
|
||||||
{
|
{
|
||||||
payload.AddRange(dados);
|
payload.AddRange(dados);
|
||||||
}
|
}
|
||||||
CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, Dados._EnderecoCAN_Tx, Dados._EnderecoCAN_Rx, (byte)PosicaoTx, (byte)PosicaoRx, payload.ToArray(), get);
|
CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, new byte[] { Dados._EnderecoCAN_Tx }, new byte[] { Dados._EnderecoCAN_Rx }, (byte)PosicaoTx, (byte)PosicaoRx, payload.ToArray(), get);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Control InstanciarDispositivo(int index)
|
public Control InstanciarDispositivo(int index)
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ namespace AgroBase.Services
|
||||||
|
|
||||||
private static void EnviarComando(T_Code Dispositivo, byte idTx, byte idRx, byte funcCode, byte[] payload = null, bool get = true)
|
private static void EnviarComando(T_Code Dispositivo, byte idTx, byte idRx, byte funcCode, byte[] payload = null, bool get = true)
|
||||||
{
|
{
|
||||||
CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, idTx, idRx, funcCode, funcCode, payload, get);
|
CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, new byte[] { idTx }, new byte[] { idRx }, funcCode, funcCode, payload, get);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetModoOperacao(byte idTx, byte idRx, byte modo)
|
public static void SetModoOperacao(byte idTx, byte idRx, byte modo)
|
||||||
|
|
@ -283,10 +283,12 @@ namespace AgroBase.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MKS057DModel ObterOuCriar(byte idTx, byte idRx)
|
private MKS057DModel ObterOuCriar(uint _idTx, uint _idRx)
|
||||||
{
|
{
|
||||||
lock (DadosLeitura)
|
lock (DadosLeitura)
|
||||||
{
|
{
|
||||||
|
byte idTx = CanManager.ConverterIdCan(_idTx)[0];
|
||||||
|
byte idRx = CanManager.ConverterIdCan(_idRx)[0];
|
||||||
var item = DadosLeitura.FirstOrDefault(x => x.EnderecoTx == idTx && x.EnderecoRx == idRx);
|
var item = DadosLeitura.FirstOrDefault(x => x.EnderecoTx == idTx && x.EnderecoRx == idRx);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ namespace AgroBase.Services
|
||||||
|
|
||||||
private static void EnviarComando(T_Code Dispositivo, byte idTx, byte idRx, byte funcCode, byte[] payload = null, bool get = true)
|
private static void EnviarComando(T_Code Dispositivo, byte idTx, byte idRx, byte funcCode, byte[] payload = null, bool get = true)
|
||||||
{
|
{
|
||||||
CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, idTx, idRx, funcCode, funcCode, payload, get);
|
CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, new byte[] { idTx }, new byte[] { idRx }, funcCode, funcCode, payload, get);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RequisitarDado(byte idTx, byte idRx, OidParametros dado)
|
public static void RequisitarDado(byte idTx, byte idRx, OidParametros dado)
|
||||||
|
|
@ -280,10 +280,12 @@ namespace AgroBase.Services
|
||||||
SinalZNaoEncontrado = 19
|
SinalZNaoEncontrado = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
private OIDModel ObterOuCriar(byte idTx, byte idRx)
|
private OIDModel ObterOuCriar(uint _idTx, uint _idRx)
|
||||||
{
|
{
|
||||||
lock (DadosLeitura)
|
lock (DadosLeitura)
|
||||||
{
|
{
|
||||||
|
byte idTx = CanManager.ConverterIdCan(_idTx)[0];
|
||||||
|
byte idRx = CanManager.ConverterIdCan(_idRx)[0];
|
||||||
var item = DadosLeitura.FirstOrDefault(x => x.EnderecoTx == idTx && x.EnderecoRx == idRx);
|
var item = DadosLeitura.FirstOrDefault(x => x.EnderecoTx == idTx && x.EnderecoRx == idRx);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static AgroBase.Models.Enums;
|
||||||
using static AgroBase.Models.Operadores.OperadoresModels;
|
using static AgroBase.Models.Operadores.OperadoresModels;
|
||||||
|
|
||||||
namespace AgroBase.Services.Operadores
|
namespace AgroBase.Services.Operadores
|
||||||
|
|
@ -744,6 +745,43 @@ namespace AgroBase.Services.Operadores
|
||||||
("last_ec", DadosCan.UltimoErroCritico)
|
("last_ec", DadosCan.UltimoErroCritico)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var DadosBateria = DalyBMSService.DadosLeitura;
|
||||||
|
RedisService.AtualizarCampos(
|
||||||
|
RedisService.ModKey(T_Code.Bat), // ajusta o T_Code se o seu enum tiver outro nome
|
||||||
|
("timestamp", agora),
|
||||||
|
("iniciado", DadosBateria.Iniciado),
|
||||||
|
("momento_bms", DadosBateria.Momento),
|
||||||
|
// Dados principais do pack
|
||||||
|
("tensao_total_v", DadosBateria.Pack.TensaoTotal_V),
|
||||||
|
("tensao_medida_v", DadosBateria.Pack.TensaoMedida_V),
|
||||||
|
("corrente_a", DadosBateria.Pack.Corrente_A),
|
||||||
|
("soc_pct", DadosBateria.Pack.SOC_percent),
|
||||||
|
("num_celulas_serie", DadosBateria.Pack.NumeroCelulasSerie),
|
||||||
|
("num_sensores_temp", DadosBateria.Pack.NumeroSensoresTemperatura),
|
||||||
|
("estado_geral", DadosBateria.Pack.EstadoGeral),
|
||||||
|
("charge_mos_on", DadosBateria.Pack.ChargeMosOn),
|
||||||
|
("discharge_mos_on", DadosBateria.Pack.DischargeMosOn),
|
||||||
|
("cap_restante_ah", DadosBateria.Pack.CapacidadeRestante_Ah),
|
||||||
|
("ciclos_aproximados", DadosBateria.Pack.CiclosAproximados),
|
||||||
|
// Tensão de células
|
||||||
|
("tensao_min_celula_v", DadosBateria.TensaoMin > 0 ? DadosBateria.TensaoMin : DadosBateria.Celulas.Min(c => c.Tensao_V)),
|
||||||
|
("tensao_max_celula_v", DadosBateria.TensaoMax > 0 ? DadosBateria.TensaoMax : DadosBateria.Celulas.Max(c => c.Tensao_V)),
|
||||||
|
("idx_celula_min", DadosBateria.Celulas.FirstOrDefault(c => c.EhMinima)?.Index ?? 0),
|
||||||
|
("idx_celula_max", DadosBateria.Celulas.FirstOrDefault(c => c.EhMaxima)?.Index ?? 0),
|
||||||
|
// Temperaturas
|
||||||
|
("temp_min_c", DadosBateria.TemperaturaMin > -200 ? DadosBateria.TemperaturaMin : DadosBateria.Temperaturas.Min(t => t.Temperatura_C)),
|
||||||
|
("temp_max_c", DadosBateria.TemperaturaMax > -200 ? DadosBateria.TemperaturaMax : DadosBateria.Temperaturas.Max(t => t.Temperatura_C)),
|
||||||
|
// Balanceamento
|
||||||
|
("celulas_em_balanceamento", DadosBateria.Balanceamento.QtdeCelulasEmBalanceamento),
|
||||||
|
// Alarmes
|
||||||
|
("tem_alarme", DadosBateria.Alarmes?.TemAlarme ?? false),
|
||||||
|
("fault_code", DadosBateria.Alarmes?.FaultCode ?? 0x00),
|
||||||
|
("qtd_alarmes", DadosBateria.Alarmes?.Mensagens?.Count ?? 0),
|
||||||
|
("alarmes_texto", DadosBateria.Alarmes.Mensagens != null && DadosBateria.Alarmes.Mensagens.Count > 0 ? string.Join(" | ", DadosBateria.Alarmes.Mensagens) : string.Empty),
|
||||||
|
// Opcional: bytes brutos de alarme se quiser debugar depois
|
||||||
|
("alarm_bytes_raw", DadosBateria.Alarmes.AlarmBytesRaw != null ? BitConverter.ToString(DadosBateria.Alarmes.AlarmBytesRaw) : string.Empty)
|
||||||
|
);
|
||||||
|
|
||||||
var DadosTrj = _Sensoriamento?.Trajetoria;
|
var DadosTrj = _Sensoriamento?.Trajetoria;
|
||||||
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Trj),
|
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Trj),
|
||||||
("timestamp", agora),
|
("timestamp", agora),
|
||||||
|
|
@ -759,6 +797,5 @@ namespace AgroBase.Services.Operadores
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -442,6 +442,16 @@ namespace AgroBase.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (true && !DalyBMSService.Iniciado)
|
||||||
|
{
|
||||||
|
AtualizarConsole($"{CanManager.CanService._portName} - Procurando dispositivo BAT");
|
||||||
|
bool DispBat = await DalyBMSService.VerificaDispositivoConectado();
|
||||||
|
if (DispBat)
|
||||||
|
{
|
||||||
|
AtualizarConsole("Dispositivo BAT encontrado");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return CanManager.CanService.Iniciado;
|
return CanManager.CanService.Iniciado;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,22 +156,41 @@ public class WaveshareCANFDService
|
||||||
|
|
||||||
public bool Send(uint id, byte[] data, bool isExtended = false, bool isRemote = false)
|
public bool Send(uint id, byte[] data, bool isExtended = false, bool isRemote = false)
|
||||||
{
|
{
|
||||||
if (!isOpen || data == null || data.Length > 8) return false;
|
if (!isOpen || data == null || data.Length > 8)
|
||||||
|
return false;
|
||||||
|
|
||||||
id &= 0x7FF; // <- garante Standard ID
|
// Segurança: garante que o ID está no range certo
|
||||||
|
if (isExtended)
|
||||||
|
{
|
||||||
|
// 29 bits
|
||||||
|
id &= 0x1FFFFFFF;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 11 bits
|
||||||
|
id &= 0x7FF;
|
||||||
|
}
|
||||||
|
|
||||||
var obj = new VCI_CAN_OBJ
|
var obj = new VCI_CAN_OBJ
|
||||||
{
|
{
|
||||||
ID = id,
|
ID = id,
|
||||||
SendType = 0,
|
SendType = 0, // normal
|
||||||
RemoteFlag = (byte)(isRemote ? 1 : 0),
|
RemoteFlag = (byte)(isRemote ? 1 : 0), // 0 = data frame, 1 = remote frame
|
||||||
ExternFlag = (byte)(isExtended ? 1 : 0),
|
ExternFlag = (byte)(isExtended ? 1 : 0), // 0 = standard, 1 = extended
|
||||||
DataLen = (byte)data.Length,
|
DataLen = (byte)data.Length,
|
||||||
Data = new byte[8],
|
Data = new byte[8],
|
||||||
Reserved = new byte[3]
|
Reserved = new byte[3]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// copia os dados pro buffer fixo de 8 bytes
|
||||||
|
Array.Clear(obj.Data, 0, obj.Data.Length);
|
||||||
Array.Copy(data, obj.Data, data.Length);
|
Array.Copy(data, obj.Data, data.Length);
|
||||||
return VCI_Transmit(DEVICE_TYPE, DEVICE_INDEX, channelIndex, ref obj, 1) > 0;
|
|
||||||
|
lock (_canLock)
|
||||||
|
{
|
||||||
|
uint sent = VCI_Transmit(DEVICE_TYPE, DEVICE_INDEX, channelIndex, ref obj, 1);
|
||||||
|
return sent == 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task tmrLeitura_Tick()
|
private async Task tmrLeitura_Tick()
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -21,19 +21,19 @@
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"Arquivo": "modelseg",
|
"Arquivo": "modelseg",
|
||||||
"Diretorio": "C:\\AgroBaseModels\\Ruas\\",
|
"Diretorio": "C:\\AgroBaseModels\\Ruas\\",
|
||||||
"Extensao": ".blob",
|
"Extensao": ".pt",
|
||||||
"Versao": "1_2",
|
"Versao": "1_3",
|
||||||
"TipoArquivo": 0,
|
"TipoArquivo": 0,
|
||||||
"ArquivoDownload": "models/street_detector_model_seg-1_2.blob",
|
"ArquivoDownload": "models/street_detector_model_seg-1_3.pt",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"Arquivo": "modelseg",
|
"Arquivo": "modelseg",
|
||||||
"Diretorio": "C:\\AgroBaseModels\\Ruas\\",
|
"Diretorio": "C:\\AgroBaseModels\\Ruas\\",
|
||||||
"Extensao": ".txt",
|
"Extensao": ".txt",
|
||||||
"Versao": "1_2",
|
"Versao": "1_3",
|
||||||
"TipoArquivo": 0,
|
"TipoArquivo": 0,
|
||||||
"ArquivoDownload": "models/street_detector_labelmap_seg-1_2.txt"
|
"ArquivoDownload": "models/street_detector_labelmap_seg-1_3.txt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ def main():
|
||||||
from health_worker.modulos.ponte_ip import ModuloIPBribge
|
from health_worker.modulos.ponte_ip import ModuloIPBribge
|
||||||
from health_worker.modulos.can import ModuloCAN
|
from health_worker.modulos.can import ModuloCAN
|
||||||
from health_worker.modulos.trajetoria import ModuloTrajetorira
|
from health_worker.modulos.trajetoria import ModuloTrajetorira
|
||||||
|
from health_worker.modulos.bateria import ModuloBateria
|
||||||
from health_worker.config import mostrar_log
|
from health_worker.config import mostrar_log
|
||||||
from shared.contexto_global_redis import ContextoGlobalRedis, CmdKey, CtxKey
|
from shared.contexto_global_redis import ContextoGlobalRedis, CmdKey, CtxKey
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ def main():
|
||||||
T_Code.Ipb: ModuloIPBribge(),
|
T_Code.Ipb: ModuloIPBribge(),
|
||||||
T_Code.Can: ModuloCAN(),
|
T_Code.Can: ModuloCAN(),
|
||||||
T_Code.Trj: ModuloTrajetorira(),
|
T_Code.Trj: ModuloTrajetorira(),
|
||||||
|
T_Code.Bat: ModuloBateria(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def loop_ativo():
|
def loop_ativo():
|
||||||
|
|
|
||||||
|
|
@ -514,7 +514,7 @@ class CameraManager:
|
||||||
t1 = time.time()
|
t1 = time.time()
|
||||||
grid_conf["ultima_chamada"] = self._ultima_analise_matriz_confianca.get("ultima_chamada", t0)
|
grid_conf["ultima_chamada"] = self._ultima_analise_matriz_confianca.get("ultima_chamada", t0)
|
||||||
self._calcular_performance(t0, t1, grid_conf)
|
self._calcular_performance(t0, t1, grid_conf)
|
||||||
snapshot = self.data_fuser.update(grid_conf, velocidade_ms=vel)
|
snapshot = self.data_fuser.update(grid_conf, velocidade_ms=vel, status_seg=self._ultima_analise_segmentacao.get("dados_visuais", {}).get("status_corredor"))
|
||||||
self._ultima_analise_matriz_confianca = grid_conf
|
self._ultima_analise_matriz_confianca = grid_conf
|
||||||
|
|
||||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from shared.enums import StatusCarroMapa
|
||||||
|
|
||||||
|
|
||||||
class CostmapFuser:
|
class CostmapFuser:
|
||||||
|
|
@ -124,7 +125,9 @@ class CostmapFuser:
|
||||||
det_score_f=None, # (H,W) 0..1
|
det_score_f=None, # (H,W) 0..1
|
||||||
det_label_id=None, # (H,W) int, -1 = none
|
det_label_id=None, # (H,W) int, -1 = none
|
||||||
det_strength=None, # (H,W) 0..1 ~ conf*cov dominante
|
det_strength=None, # (H,W) 0..1 ~ conf*cov dominante
|
||||||
thr_det_consider=0.15
|
thr_det_consider=0.15,
|
||||||
|
|
||||||
|
status_seg=StatusCarroMapa.Direcionando
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
H, W = custo_f.shape
|
H, W = custo_f.shape
|
||||||
|
|
@ -402,7 +405,7 @@ class CostmapFuser:
|
||||||
"N_on": N_on,
|
"N_on": N_on,
|
||||||
"N_off": N_off
|
"N_off": N_off
|
||||||
}
|
}
|
||||||
blocked_out = blocked_raw
|
blocked_out = status_seg == StatusCarroMapa.Parado or blocked_raw
|
||||||
|
|
||||||
if use_persistence:
|
if use_persistence:
|
||||||
v = float(max(0.0, velocidade_mps or 0.0))
|
v = float(max(0.0, velocidade_mps or 0.0))
|
||||||
|
|
@ -444,13 +447,14 @@ class CostmapFuser:
|
||||||
elif st["latched"] and (st["off"] >= N_off):
|
elif st["latched"] and (st["off"] >= N_off):
|
||||||
st["latched"] = False
|
st["latched"] = False
|
||||||
|
|
||||||
|
blocked_out = status_seg == StatusCarroMapa.Parado or bool(st["latched"])
|
||||||
|
|
||||||
decision.update({
|
decision.update({
|
||||||
"parar": False, # bool(st["latched"]),
|
"parar": blocked_out,
|
||||||
"v_max_sugerida_mps": v_max_sug,
|
"v_max_sugerida_mps": v_max_sug,
|
||||||
"frames_on": int(st["on"]),
|
"frames_on": int(st["on"]),
|
||||||
"frames_off": int(st["off"])
|
"frames_off": int(st["off"])
|
||||||
})
|
})
|
||||||
blocked_out = bool(st["latched"])
|
|
||||||
|
|
||||||
st["reason"] = reason
|
st["reason"] = reason
|
||||||
# guarda também as duas distâncias pra debug
|
# guarda também as duas distâncias pra debug
|
||||||
|
|
@ -494,7 +498,7 @@ class CostmapFuser:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
mostrar_log("Erro no _row_scale_x")
|
mostrar_log("Erro no _row_scale_x")
|
||||||
|
|
||||||
def update(self, grid_dict, ts=None, velocidade_ms=0.0):
|
def update(self, grid_dict, ts=None, velocidade_ms=0.0, status_seg=StatusCarroMapa.Direcionando):
|
||||||
"""
|
"""
|
||||||
grid_dict deve conter (grid_h,grid_w): "custo","conf","anom","navegavel"
|
grid_dict deve conter (grid_h,grid_w): "custo","conf","anom","navegavel"
|
||||||
opcional: "z_ref" (m) 2D
|
opcional: "z_ref" (m) 2D
|
||||||
|
|
@ -598,7 +602,8 @@ class CostmapFuser:
|
||||||
det_score_f = det_score,
|
det_score_f = det_score,
|
||||||
det_label_id = det_lbl,
|
det_label_id = det_lbl,
|
||||||
det_strength = det_sdom,
|
det_strength = det_sdom,
|
||||||
thr_det_consider = 0.25 # só considera detecção acima desse score
|
thr_det_consider = 0.25,
|
||||||
|
status_seg=status_seg
|
||||||
)
|
)
|
||||||
|
|
||||||
# incrementa seq
|
# incrementa seq
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,30 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Augmenta imagens/máscaras *por grupo*, agora com suporte a RAW.
|
Augmenta imagens/máscaras *por grupo* usando RAW4 (float32, 0..1, R G IR B).
|
||||||
|
|
||||||
Modos suportados:
|
Novo fluxo (GAL5000):
|
||||||
|
|
||||||
1) Modo novo (RAW):
|
Entrada:
|
||||||
MODELO/dataset/original/group/<grupo>/previews
|
MODELO/dataset/original/group/<grupo>/raws (.raw, 4 canais float32)
|
||||||
MODELO/dataset/original/group/<grupo>/raws
|
MODELO/dataset/original/group/<grupo>/masks (.png/.jpg)
|
||||||
MODELO/dataset/original/group/<grupo>/masks
|
|
||||||
(opcional) MODELO/dataset/original/group/<grupo>/masks2
|
(opcional) MODELO/dataset/original/group/<grupo>/masks2
|
||||||
|
|
||||||
Saída:
|
Saída:
|
||||||
MODELO/dataset/augmented/group/<grupo>/previews
|
MODELO/dataset/augmented/group/<grupo>/previews (RGB gerado do RAW blurado)
|
||||||
MODELO/dataset/augmented/group/<grupo>/raws
|
MODELO/dataset/augmented/group/<grupo>/raws (RAW4 augmentado: geom + blur)
|
||||||
MODELO/dataset/augmented/group/<grupo>/masks
|
MODELO/dataset/augmented/group/<grupo>/masks
|
||||||
MODELO/dataset/augmented/group/<grupo>/masks2 (se existir)
|
MODELO/dataset/augmented/group/<grupo>/masks2
|
||||||
|
|
||||||
2) Modo antigo (sem RAW, só images/masks):
|
Regras:
|
||||||
MODELO/dataset/original/group/<grupo>/images
|
- GEOMETRIA (flip, shift, rotate) aplicada em RAW + masks (+ masks2).
|
||||||
MODELO/dataset/original/group/<grupo>/masks
|
- BLUR / MOTION BLUR aplicado no RAW, canal por canal, com o mesmo kernel.
|
||||||
|
- Preview é SEMPRE derivado do RAW já transformado (geom + blur).
|
||||||
Saída:
|
- Máscaras não sofrem blur, apenas geom.
|
||||||
MODELO/dataset/augmented/group/<grupo>/images
|
|
||||||
MODELO/dataset/augmented/group/<grupo>/masks
|
|
||||||
|
|
||||||
Fallback legacy:
|
|
||||||
MODELO/dataset/original/{images,masks}
|
|
||||||
MODELO/dataset/augmented/{images,masks}
|
|
||||||
|
|
||||||
Transformações:
|
|
||||||
- Geométricas (HorizontalFlip, ShiftScaleRotate) → aplicam em preview, mask, mask2 e RAW.
|
|
||||||
- Fotométricas (brightness/contrast, gamma, blur, noise, flare, etc.) → só em preview RGB.
|
|
||||||
|
|
||||||
Uso:
|
Uso:
|
||||||
python _5_augmentation.py --copies 5
|
python _5_augmentation_raw.py --copies 5
|
||||||
python _5_augmentation.py --copies 5 --groups chao,chao_erva,cana
|
python _5_augmentation_raw.py --copies 5 --groups chao,chao_erva,cana
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
@ -50,6 +39,8 @@ import numpy as np
|
||||||
with open("config.json", "r", encoding="utf-8") as f:
|
with open("config.json", "r", encoding="utf-8") as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
MODELO = config.get("camera", ".")
|
MODELO = config.get("camera", ".")
|
||||||
|
RESOLUCAO = config.get("raw_size")
|
||||||
|
|
||||||
USE_MASKS2 = config.get("dual_head", False)
|
USE_MASKS2 = config.get("dual_head", False)
|
||||||
|
|
||||||
# Pastas base
|
# Pastas base
|
||||||
|
|
@ -57,7 +48,7 @@ DATASET_BASE = os.path.join(MODELO, "dataset")
|
||||||
ORIG_GROUP_ROOT = os.path.join(DATASET_BASE, "original", "group")
|
ORIG_GROUP_ROOT = os.path.join(DATASET_BASE, "original", "group")
|
||||||
AUG_GROUP_ROOT = os.path.join(DATASET_BASE, "augmented", "group")
|
AUG_GROUP_ROOT = os.path.join(DATASET_BASE, "augmented", "group")
|
||||||
|
|
||||||
# Fallback (modo antigo, sem grupos)
|
# Fallback (modo antigo, sem grupos) – mantido por compat
|
||||||
ORIG_OLD_IMG = os.path.join(DATASET_BASE, "original", "images")
|
ORIG_OLD_IMG = os.path.join(DATASET_BASE, "original", "images")
|
||||||
ORIG_OLD_MSK = os.path.join(DATASET_BASE, "original", "masks")
|
ORIG_OLD_MSK = os.path.join(DATASET_BASE, "original", "masks")
|
||||||
ORIG_OLD_MSK2 = os.path.join(DATASET_BASE, "original", "masks2")
|
ORIG_OLD_MSK2 = os.path.join(DATASET_BASE, "original", "masks2")
|
||||||
|
|
@ -67,16 +58,10 @@ AUG_OLD_MSK2 = os.path.join(DATASET_BASE, "augmented", "masks2")
|
||||||
|
|
||||||
# Extensões aceitas
|
# Extensões aceitas
|
||||||
IMG_EXTS = (".jpg", ".jpeg", ".png")
|
IMG_EXTS = (".jpg", ".jpeg", ".png")
|
||||||
MSK_EXTS = (".png", ".jpg", ".jpeg") # prioriza PNG
|
MSK_EXTS = (".png", ".jpg", ".jpeg")
|
||||||
MSK2_EXTS = (".png", ".jpg", ".jpeg")
|
MSK2_EXTS = (".png", ".jpg", ".jpeg")
|
||||||
RAW_EXTS = (".raw",)
|
RAW_EXTS = (".raw",)
|
||||||
|
|
||||||
# Config de RAW (mosaico 1 canal; R G / IR B é tratado depois no loader)
|
|
||||||
RAW_DTYPE = np.uint16
|
|
||||||
RAW_CHANNELS = 1 # tratamos como grayscale (H, W)
|
|
||||||
# Se RAW tiver tamanho diferente, ajuste no loader depois.
|
|
||||||
# Aqui assumimos que altura/largura batem com preview/mask.
|
|
||||||
|
|
||||||
|
|
||||||
def garantir_dir(p):
|
def garantir_dir(p):
|
||||||
os.makedirs(p, exist_ok=True)
|
os.makedirs(p, exist_ok=True)
|
||||||
|
|
@ -86,7 +71,8 @@ def garantir_dir(p):
|
||||||
# Pipelines de augmentations
|
# Pipelines de augmentations
|
||||||
# ===============================
|
# ===============================
|
||||||
|
|
||||||
# Geométricas: aplicam em preview, mask, mask2 e RAW
|
# 1) Geométricas: aplicam em preview “temporário”, mask, mask2 e RAW.
|
||||||
|
# RAW é passado como 'raw' em additional_targets para receber só geom.
|
||||||
train_geo = ReplayCompose([
|
train_geo = ReplayCompose([
|
||||||
A.HorizontalFlip(p=0.5),
|
A.HorizontalFlip(p=0.5),
|
||||||
A.ShiftScaleRotate(
|
A.ShiftScaleRotate(
|
||||||
|
|
@ -99,32 +85,144 @@ train_geo = ReplayCompose([
|
||||||
),
|
),
|
||||||
], additional_targets={
|
], additional_targets={
|
||||||
'mask2': 'mask',
|
'mask2': 'mask',
|
||||||
'raw': 'mask', # tratamos RAW como "mask" pra NÃO sofrer fotométricas
|
'raw': 'mask', # trata raw como “mask” para receber apenas geom
|
||||||
})
|
})
|
||||||
|
|
||||||
# Fotométricas: só preview RGB
|
|
||||||
train_photo = A.Compose([
|
|
||||||
A.OneOf([
|
|
||||||
A.RandomBrightnessContrast(0.2, 0.2, p=1.0),
|
|
||||||
A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=20, val_shift_limit=15, p=1.0),
|
|
||||||
A.RandomGamma(gamma_limit=(90, 110), p=1.0),
|
|
||||||
], p=0.70),
|
|
||||||
|
|
||||||
A.OneOf([
|
# ===============================
|
||||||
A.MotionBlur(blur_limit=3, p=1.0),
|
# Funções de blur físico no RAW
|
||||||
A.GaussianBlur(blur_limit=3, p=1.0),
|
# ===============================
|
||||||
], p=0.20),
|
|
||||||
|
|
||||||
A.OneOf([
|
def motion_blur_kernel(ksize=5, angle=0.0):
|
||||||
A.GaussNoise(var_limit=(5.0, 15.0), p=1.0),
|
"""
|
||||||
A.ImageCompression(quality_lower=50, quality_upper=85, p=1.0),
|
Gera um kernel de motion blur 2D com tamanho ksize e ângulo em graus.
|
||||||
], p=0.20),
|
Usado tanto para RAW quanto para preview.
|
||||||
|
"""
|
||||||
|
ksize = int(ksize)
|
||||||
|
if ksize < 3:
|
||||||
|
ksize = 3
|
||||||
|
if ksize % 2 == 0:
|
||||||
|
ksize += 1
|
||||||
|
|
||||||
A.RandomShadow(p=0.10),
|
kernel = np.zeros((ksize, ksize), dtype=np.float32)
|
||||||
A.RandomSunFlare(p=0.10),
|
# linha horizontal no centro
|
||||||
A.ChannelShuffle(p=0.05),
|
kernel[ksize // 2, :] = 1.0
|
||||||
A.CoarseDropout(max_holes=6, max_height=16, max_width=16, p=0.10),
|
|
||||||
])
|
# rotaciona a linha para o ângulo desejado
|
||||||
|
center = (ksize / 2.0 - 0.5, ksize / 2.0 - 0.5)
|
||||||
|
M = cv2.getRotationMatrix2D(center, angle, 1.0)
|
||||||
|
kernel = cv2.warpAffine(kernel, M, (ksize, ksize))
|
||||||
|
|
||||||
|
s = kernel.sum()
|
||||||
|
if s > 0:
|
||||||
|
kernel /= s
|
||||||
|
return kernel
|
||||||
|
|
||||||
|
|
||||||
|
def apply_coherent_blur_raw(raw, p_motion=0.20, p_gauss=0.20):
|
||||||
|
"""
|
||||||
|
Aplica blur/motion blur de forma COERENTE no RAW, canal por canal.
|
||||||
|
|
||||||
|
- Mesmo kernel em TODOS os canais.
|
||||||
|
- Máscaras não passam aqui.
|
||||||
|
- Se nada for sorteado, retorna o RAW original.
|
||||||
|
"""
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
r = np.random.rand()
|
||||||
|
if r >= (p_motion + p_gauss):
|
||||||
|
# sem blur nesta amostra
|
||||||
|
return raw
|
||||||
|
|
||||||
|
# escolhe tamanho de kernel
|
||||||
|
k_choices = [3, 5, 7]
|
||||||
|
ksize = int(np.random.choice(k_choices))
|
||||||
|
|
||||||
|
if r < p_motion:
|
||||||
|
# Motion blur com ângulo pequeno (vibração / movimento leve)
|
||||||
|
angle = float(np.random.uniform(-20.0, 20.0))
|
||||||
|
kernel = motion_blur_kernel(ksize=ksize, angle=angle)
|
||||||
|
else:
|
||||||
|
# Gaussian blur (leve desfoque de foco)
|
||||||
|
if ksize % 2 == 0:
|
||||||
|
ksize += 1
|
||||||
|
g1d = cv2.getGaussianKernel(ksize, 0)
|
||||||
|
kernel = g1d @ g1d.T # vira kernel 2D
|
||||||
|
|
||||||
|
# Aplica kernel canal a canal
|
||||||
|
if raw.ndim == 2:
|
||||||
|
raw_blur = cv2.filter2D(raw, ddepth=-1, kernel=kernel)
|
||||||
|
else:
|
||||||
|
raw_blur = np.empty_like(raw)
|
||||||
|
h, w, c = raw.shape
|
||||||
|
for ch in range(c):
|
||||||
|
raw_blur[..., ch] = cv2.filter2D(
|
||||||
|
raw[..., ch],
|
||||||
|
ddepth=-1,
|
||||||
|
kernel=kernel,
|
||||||
|
borderType=cv2.BORDER_REFLECT_101,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Garante que continua em [0,1] para float
|
||||||
|
if np.issubdtype(raw_blur.dtype, np.floating):
|
||||||
|
raw_blur = np.clip(raw_blur, 0.0, 1.0)
|
||||||
|
|
||||||
|
return raw_blur
|
||||||
|
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# IO de imagens / RAW
|
||||||
|
# ===============================
|
||||||
|
|
||||||
|
def raw_to_preview_rgb(raw_np: np.ndarray) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
raw_np: (H,W,C) float32 em 0..1, C=4 ou 5.
|
||||||
|
|
||||||
|
Layout esperado (após _load_raw):
|
||||||
|
- C=4 -> [R,G,B,IR]
|
||||||
|
- C=5 -> [R,G,B,IR,NDVI] (se um dia tiver)
|
||||||
|
|
||||||
|
Usamos sempre R,G,B para o preview e devolvemos (H,W,3) uint8.
|
||||||
|
"""
|
||||||
|
assert raw_np.ndim == 3, "raw_np deve ser (H,W,C)"
|
||||||
|
|
||||||
|
H, W, C = raw_np.shape
|
||||||
|
if C not in (4, 5):
|
||||||
|
raise RuntimeError(f"Esperado C=4 ou 5, veio {C} em raw_to_preview_rgb")
|
||||||
|
|
||||||
|
# RAW já vem como [R,G,B,IR] do _load_raw
|
||||||
|
r = raw_np[..., 0].astype(np.float32)
|
||||||
|
g = raw_np[..., 1].astype(np.float32)
|
||||||
|
b = raw_np[..., 2].astype(np.float32)
|
||||||
|
|
||||||
|
# Monta RGB em (H,W,3) para trabalhar
|
||||||
|
rgb = np.stack([r, g, b], axis=-1) # (H,W,3)
|
||||||
|
|
||||||
|
# Auto white balance simples (referenciado no G)
|
||||||
|
means = rgb.reshape(-1, 3).mean(axis=0) # (3,)
|
||||||
|
ref = means[1] # canal G
|
||||||
|
gains = ref / (means + 1e-6)
|
||||||
|
gains = np.clip(gains, 0.5, 2.0)
|
||||||
|
rgb_wb = rgb * gains # broadcasting em (3,)
|
||||||
|
|
||||||
|
# Força do WB
|
||||||
|
alpha = 1.0 # teste 0.4..0.8 se quiser misturar com original
|
||||||
|
if alpha >= 0.999:
|
||||||
|
rgb = rgb_wb
|
||||||
|
else:
|
||||||
|
rgb_orig = rgb.copy()
|
||||||
|
rgb = (1 - alpha) * rgb_orig + alpha * rgb_wb
|
||||||
|
|
||||||
|
rgb = np.clip(rgb, 0.0, 1.0)
|
||||||
|
|
||||||
|
# Gamma mais suave
|
||||||
|
rgb = np.power(rgb, 1/1.9, out=rgb)
|
||||||
|
|
||||||
|
# Converte pra uint8 OpenCV-friendly
|
||||||
|
rgb8 = (rgb * 255.0).clip(0, 255).astype(np.uint8) # (H,W,3)
|
||||||
|
|
||||||
|
return rgb8
|
||||||
|
|
||||||
|
|
||||||
def load_rgb(path):
|
def load_rgb(path):
|
||||||
|
|
@ -138,40 +236,101 @@ def save_rgb(path, arr_rgb):
|
||||||
Image.fromarray(arr_rgb).save(path)
|
Image.fromarray(arr_rgb).save(path)
|
||||||
|
|
||||||
|
|
||||||
def load_raw_grayscale(path, like_shape=None):
|
def _load_raw(path: str, raw_hw) -> np.ndarray:
|
||||||
"""
|
ext = os.path.splitext(path)[1].lower()
|
||||||
Carrega RAW como (H, W) em uint8 ou uint16 detectando pelo tamanho do arquivo.
|
if ext == ".npz":
|
||||||
"""
|
z = np.load(path)
|
||||||
if like_shape is None:
|
key = list(z.keys())[0]
|
||||||
raise RuntimeError("like_shape é obrigatório para RAW (preciso do HxW).")
|
arr = z[key]
|
||||||
|
elif ext == ".npy":
|
||||||
h, w = like_shape[:2]
|
arr = np.load(path)
|
||||||
npx = h * w
|
elif ext == ".raw":
|
||||||
|
if raw_hw is None:
|
||||||
fsize = os.path.getsize(path)
|
raise RuntimeError(
|
||||||
if fsize == npx: # 1 byte por pixel
|
f"Para ler .raw preciso de raw_hw=(H,W). "
|
||||||
dtype = np.uint8
|
f"Passe raw_hw ao criar o dataset."
|
||||||
elif fsize == npx * 2: # 2 bytes por pixel
|
)
|
||||||
dtype = np.uint16
|
H, W = raw_hw
|
||||||
|
size_bytes = os.path.getsize(path)
|
||||||
|
mosa_bytes = H * W # mosaico uint8
|
||||||
|
raw4_bytes = 4 * H * W * 4 # 4 canais * H * W * 4 bytes (float32)
|
||||||
|
if size_bytes == mosa_bytes:
|
||||||
|
# MODO ANTIGO: mosaico uint8
|
||||||
|
arr = np.fromfile(path, dtype=np.uint8)
|
||||||
|
raw2d = arr.reshape(H, W)
|
||||||
|
if (H % 2) != 0 or (W % 2) != 0:
|
||||||
|
raise RuntimeError(f"raw_hw deve ser par em H e W p/ mosaico 2x2 (R,G,IR,B), veio H={H}, W={W}")
|
||||||
|
H2, W2 = H // 2, W // 2
|
||||||
|
r_sub = raw2d[0::2, 0::2]
|
||||||
|
g_sub = raw2d[0::2, 1::2]
|
||||||
|
ir_sub = raw2d[1::2, 0::2]
|
||||||
|
b_sub = raw2d[1::2, 1::2]
|
||||||
|
from PIL import Image
|
||||||
|
def upsample(ch_2d: np.ndarray) -> np.ndarray:
|
||||||
|
im = Image.fromarray(ch_2d) # uint8
|
||||||
|
im = im.resize((W, H), resample=Image.BILINEAR)
|
||||||
|
return np.array(im)
|
||||||
|
r_full = upsample(r_sub)
|
||||||
|
g_full = upsample(g_sub)
|
||||||
|
ir_full = upsample(ir_sub)
|
||||||
|
b_full = upsample(b_sub)
|
||||||
|
# (H, W, 4) uint8 [R,G,B,IR]
|
||||||
|
arr = np.stack([r_full, g_full, b_full, ir_full], axis=-1).astype(np.uint8)
|
||||||
|
elif size_bytes == raw4_bytes:
|
||||||
|
# NOVO MODO: RAW4 float32 salvo pelo normalize
|
||||||
|
arr_f32 = np.fromfile(path, dtype=np.float32)
|
||||||
|
raw4 = arr_f32.reshape(4, H, W) # (C,H,W) [R,G,IR,B]
|
||||||
|
arr = np.transpose(raw4, (1, 2, 0)) # (H,W,4) [R,G,IR,B]
|
||||||
|
# REORGANIZA PARA CONTRATO INTERNO: [R,G,B,IR]
|
||||||
|
arr = arr[..., [0, 1, 3, 2]]
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Tamanho inesperado para RAW {path}: {fsize} bytes "
|
f"Tamanho inesperado em {path}: {size_bytes} bytes "
|
||||||
f"(esperado {npx} (u8) ou {npx*2} (u16) para {w}x{h})."
|
f"(esperado {mosa_bytes} ou {raw4_bytes})"
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Extensão não suportada para RAW: {ext} ({path})")
|
||||||
|
# Até aqui queremos (H,W,4) base
|
||||||
|
if arr.ndim != 3:
|
||||||
|
raise RuntimeError(f"RAW precisa ser 3D, veio {arr.shape} em {path}")
|
||||||
|
if arr.shape[0] == 4 and arr.shape[-1] != 4:
|
||||||
|
arr = np.transpose(arr, (1, 2, 0)) # (H,W,4)
|
||||||
|
if arr.shape[-1] != 4:
|
||||||
|
raise RuntimeError(f"Esperava 4 canais base, veio shape={arr.shape} em {path}")
|
||||||
|
return arr # (H,W,4)
|
||||||
|
|
||||||
data = np.fromfile(path, dtype=dtype)
|
def _scale_to_float01(raw: np.ndarray, max_value = None) -> np.ndarray:
|
||||||
if data.size != npx:
|
if raw.dtype == np.uint16:
|
||||||
raise RuntimeError(f"RAW {path}: size={data.size} != {npx} (HxW)")
|
mv = float(max_value) if max_value is not None else 65535.0
|
||||||
|
elif raw.dtype == np.uint8:
|
||||||
return data.reshape((h, w))
|
mv = float(max_value) if max_value is not None else 255.0
|
||||||
|
else:
|
||||||
|
# float já vem normalizado ou não; se passar max_value, aplica
|
||||||
|
mv = float(max_value) if max_value is not None else None
|
||||||
|
raw_f = raw.astype(np.float32)
|
||||||
|
if mv is not None and mv > 0:
|
||||||
|
raw_f = raw_f / mv
|
||||||
|
# clip pra não explodir
|
||||||
|
raw_f = np.clip(raw_f, 0.0, 1.0)
|
||||||
|
return raw_f
|
||||||
|
|
||||||
|
|
||||||
def save_raw_grayscale(path, arr):
|
def save_raw_any(path, arr):
|
||||||
np.asarray(arr).tofile(path)
|
arr = np.asarray(arr)
|
||||||
|
arr.tofile(path)
|
||||||
|
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Helpers de diretório / mapeamento
|
||||||
|
# ===============================
|
||||||
|
|
||||||
def list_groups(root):
|
def list_groups(root):
|
||||||
"""Lista grupos válidos (tem subpasta 'masks' e pelo menos 'images' ou 'previews')."""
|
"""
|
||||||
|
Lista grupos válidos para o NOVO fluxo:
|
||||||
|
- group/<g>/raws
|
||||||
|
- group/<g>/masks
|
||||||
|
(previews/images não são mais obrigatórios)
|
||||||
|
"""
|
||||||
if not os.path.isdir(root):
|
if not os.path.isdir(root):
|
||||||
return []
|
return []
|
||||||
grupos = []
|
grupos = []
|
||||||
|
|
@ -180,8 +339,8 @@ def list_groups(root):
|
||||||
if not os.path.isdir(gdir):
|
if not os.path.isdir(gdir):
|
||||||
continue
|
continue
|
||||||
has_masks = os.path.isdir(os.path.join(gdir, "masks"))
|
has_masks = os.path.isdir(os.path.join(gdir, "masks"))
|
||||||
has_imgs = os.path.isdir(os.path.join(gdir, "images")) or os.path.isdir(os.path.join(gdir, "previews"))
|
has_raws = os.path.isdir(os.path.join(gdir, "raws"))
|
||||||
if has_masks and has_imgs:
|
if has_masks and has_raws:
|
||||||
grupos.append(name)
|
grupos.append(name)
|
||||||
return grupos
|
return grupos
|
||||||
|
|
||||||
|
|
@ -252,148 +411,167 @@ def ensure_aug_dirs(group_name=None, use_masks2=False, use_raw=False, is_preview
|
||||||
return img_out, msk_out, msk2_out, raw_out
|
return img_out, msk_out, msk2_out, raw_out
|
||||||
|
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Núcleo: augmentação de um sample
|
||||||
|
# ===============================
|
||||||
|
|
||||||
def augment_sample(
|
def augment_sample(
|
||||||
img_path,
|
raw_path,
|
||||||
msk_path,
|
msk_path,
|
||||||
img_out_dir,
|
img_out_dir,
|
||||||
msk_out_dir,
|
msk_out_dir,
|
||||||
copies,
|
copies,
|
||||||
msk2_path=None,
|
msk2_path=None,
|
||||||
msk2_out_dir=None,
|
msk2_out_dir=None,
|
||||||
raw_path=None,
|
|
||||||
raw_out_dir=None,
|
raw_out_dir=None,
|
||||||
):
|
):
|
||||||
base_img, img_ext = os.path.splitext(os.path.basename(img_path))
|
"""
|
||||||
base = base_img
|
Faz a augmentação a partir de UM RAW4 + máscara (e opcionalmente máscara2).
|
||||||
|
|
||||||
|
Passos:
|
||||||
|
1) Carrega máscara (para saber HxW).
|
||||||
|
2) Carrega RAW4 float32 usando HxW da máscara.
|
||||||
|
3) Gera preview RGB a partir do RAW4 (para servir de "image" no Albumentations).
|
||||||
|
4) Aplica GEOMETRIA (mesma em preview, mask, mask2 e raw).
|
||||||
|
5) Aplica BLUR/MOTION BLUR no RAW geométrico, canal a canal, com o mesmo kernel.
|
||||||
|
6) Gera preview final a partir do RAW blurado.
|
||||||
|
7) Salva:
|
||||||
|
- preview_n em img_out_dir
|
||||||
|
- mask_n em msk_out_dir
|
||||||
|
- raw_n em raw_out_dir
|
||||||
|
- mask2_n em msk2_out_dir (se houver)
|
||||||
|
"""
|
||||||
|
base_raw, raw_ext = os.path.splitext(os.path.basename(raw_path))
|
||||||
|
base = base_raw
|
||||||
|
|
||||||
base_msk, msk_ext = os.path.splitext(os.path.basename(msk_path))
|
base_msk, msk_ext = os.path.splitext(os.path.basename(msk_path))
|
||||||
msk2_ext = os.path.splitext(os.path.basename(msk2_path))[1] if msk2_path else None
|
if base_msk != base:
|
||||||
raw_ext = os.path.splitext(os.path.basename(raw_path))[1] if raw_path else None
|
print(f"[WARN] RAW '{base}' e máscara '{base_msk}' com bases diferentes, mas seguindo mesmo assim.")
|
||||||
|
|
||||||
img = load_rgb(img_path)
|
msk2_ext = os.path.splitext(os.path.basename(msk2_path))[1] if msk2_path else None
|
||||||
|
|
||||||
|
# 1) Máscaras
|
||||||
msk = load_rgb(msk_path)
|
msk = load_rgb(msk_path)
|
||||||
msk2 = load_rgb(msk2_path) if msk2_path else None
|
msk2 = load_rgb(msk2_path) if msk2_path else None
|
||||||
|
|
||||||
if raw_path is not None and raw_out_dir is not None:
|
# 2) RAW4
|
||||||
# usa shape do preview pra validar RAW
|
raw = _load_raw(raw_path, raw_hw=(RESOLUCAO[1], RESOLUCAO[0])) # (H,W,4) uint8
|
||||||
raw = load_raw_grayscale(raw_path, like_shape=img.shape)
|
raw = _scale_to_float01(raw) # float32 0..1
|
||||||
else:
|
|
||||||
raw = None
|
# 3) Preview inicial derivado do RAW (antes de geom/blur, só referência)
|
||||||
|
img = raw_to_preview_rgb(raw)
|
||||||
|
|
||||||
gen = 0
|
gen = 0
|
||||||
for i in range(copies):
|
for i in range(copies):
|
||||||
# 1) GEOM: mesma transformação em img, mask, mask2 e raw
|
# 4) GEOMETRIA: mesma transformação em img, mask, mask2 e raw
|
||||||
if raw is not None:
|
|
||||||
if msk2 is not None and msk2_out_dir:
|
if msk2 is not None and msk2_out_dir:
|
||||||
aug_geo = train_geo(image=img, mask=msk, mask2=msk2, raw=raw)
|
aug_geo = train_geo(image=img, mask=msk, mask2=msk2, raw=raw)
|
||||||
else:
|
else:
|
||||||
aug_geo = train_geo(image=img, mask=msk, raw=raw)
|
aug_geo = train_geo(image=img, mask=msk, raw=raw)
|
||||||
else:
|
|
||||||
if msk2 is not None and msk2_out_dir:
|
|
||||||
aug_geo = train_geo(image=img, mask=msk, mask2=msk2)
|
|
||||||
else:
|
|
||||||
aug_geo = train_geo(image=img, mask=msk)
|
|
||||||
|
|
||||||
img_g = aug_geo["image"]
|
|
||||||
msk_g = aug_geo["mask"]
|
msk_g = aug_geo["mask"]
|
||||||
raw_g = aug_geo.get("raw", None)
|
raw_g = aug_geo.get("raw", None)
|
||||||
msk2_g = aug_geo.get("mask2", None) if (msk2 is not None and msk2_out_dir) else None
|
msk2_g = aug_geo.get("mask2", None) if (msk2 is not None and msk2_out_dir) else None
|
||||||
|
|
||||||
# 2) FOTO: só preview RGB
|
# 5) BLUR FÍSICO: aplica (ou não) o MESMO blur no RAW geométrico
|
||||||
img_p = train_photo(image=img_g)["image"]
|
raw_b = apply_coherent_blur_raw(raw_g)
|
||||||
|
|
||||||
out_img = os.path.join(img_out_dir, f"{base}_aug_{i:02d}{img_ext}")
|
# 6) Preview final derivado do RAW blurado
|
||||||
|
img_b = raw_to_preview_rgb(raw_b)
|
||||||
|
|
||||||
|
# 7) Salva cópias
|
||||||
|
out_img = os.path.join(img_out_dir, f"{base}_aug_{i:02d}.png") # preview em PNG
|
||||||
out_msk = os.path.join(msk_out_dir, f"{base}_aug_{i:02d}{msk_ext}")
|
out_msk = os.path.join(msk_out_dir, f"{base}_aug_{i:02d}{msk_ext}")
|
||||||
save_rgb(out_img, img_p)
|
save_rgb(out_img, img_b)
|
||||||
save_rgb(out_msk, msk_g)
|
save_rgb(out_msk, msk_g)
|
||||||
|
|
||||||
if msk2_g is not None and msk2_out_dir:
|
if msk2_g is not None and msk2_out_dir:
|
||||||
out_msk2 = os.path.join(msk2_out_dir, f"{base}_aug_{i:02d}{msk2_ext}")
|
out_msk2 = os.path.join(msk2_out_dir, f"{base}_aug_{i:02d}{msk2_ext}")
|
||||||
save_rgb(out_msk2, msk2_g)
|
save_rgb(out_msk2, msk2_g)
|
||||||
|
|
||||||
if raw_g is not None and raw_out_dir:
|
if raw_b is not None and raw_out_dir:
|
||||||
out_raw = os.path.join(raw_out_dir, f"{base}_aug_{i:02d}{raw_ext}")
|
out_raw = os.path.join(raw_out_dir, f"{base}_aug_{i:02d}{raw_ext}")
|
||||||
save_raw_grayscale(out_raw, raw_g)
|
save_raw_any(out_raw, raw_b)
|
||||||
|
|
||||||
gen += 1
|
gen += 1
|
||||||
|
|
||||||
return gen
|
return gen
|
||||||
|
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Processamento por grupo
|
||||||
|
# ===============================
|
||||||
|
|
||||||
def process_group(group_name, copies):
|
def process_group(group_name, copies):
|
||||||
"""
|
"""
|
||||||
Processa um grupo único.
|
Processa um grupo único usando APENAS:
|
||||||
Suporta:
|
- group/<g>/raws
|
||||||
- group/<g>/previews + raws + masks (+ masks2)
|
- group/<g>/masks (+ masks2 opcional)
|
||||||
- group/<g>/images + masks (+ masks2) [modo antigo]
|
|
||||||
|
Previews de entrada são ignoradas.
|
||||||
|
As de saída são geradas do RAW4 (geom + blur).
|
||||||
"""
|
"""
|
||||||
gdir = os.path.join(ORIG_GROUP_ROOT, group_name)
|
gdir = os.path.join(ORIG_GROUP_ROOT, group_name)
|
||||||
|
|
||||||
# prioridade para novo modo (previews/raws)
|
|
||||||
img_dir_previews = os.path.join(gdir, "previews")
|
|
||||||
img_dir_images = os.path.join(gdir, "images")
|
|
||||||
raw_dir = os.path.join(gdir, "raws")
|
raw_dir = os.path.join(gdir, "raws")
|
||||||
msk_dir = os.path.join(gdir, "masks")
|
msk_dir = os.path.join(gdir, "masks")
|
||||||
msk2_dir = os.path.join(gdir, "masks2")
|
msk2_dir = os.path.join(gdir, "masks2")
|
||||||
|
|
||||||
use_preview_mode = os.path.isdir(img_dir_previews)
|
if not (os.path.isdir(raw_dir) and os.path.isdir(msk_dir)):
|
||||||
img_dir = img_dir_previews if use_preview_mode else img_dir_images
|
print(f"[WARN] Grupo '{group_name}' inválido (precisa de 'raws' e 'masks'). Pulando.")
|
||||||
|
|
||||||
if not (os.path.isdir(img_dir) and os.path.isdir(msk_dir)):
|
|
||||||
print(f"[WARN] Grupo '{group_name}' inválido (sem images/previews ou masks). Pulando.")
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
use_raw = os.path.isdir(raw_dir)
|
use_raw = True
|
||||||
use_masks2 = USE_MASKS2 and os.path.isdir(msk2_dir)
|
use_masks2 = USE_MASKS2 and os.path.isdir(msk2_dir)
|
||||||
|
|
||||||
imgs = [f for f in os.listdir(img_dir) if os.path.splitext(f.lower())[1] in IMG_EXTS]
|
raw_map = map_raws_by_base(raw_dir)
|
||||||
msk_map = map_by_base_priorizando_png(msk_dir, MSK_EXTS)
|
msk_map = map_by_base_priorizando_png(msk_dir, MSK_EXTS)
|
||||||
raw_map = map_raws_by_base(raw_dir) if use_raw else {}
|
|
||||||
msk2_map = map_by_base_priorizando_png(msk2_dir, MSK2_EXTS) if use_masks2 else {}
|
msk2_map = map_by_base_priorizando_png(msk2_dir, MSK2_EXTS) if use_masks2 else {}
|
||||||
|
|
||||||
img_out_dir, msk_out_dir, msk2_out_dir, raw_out_dir = ensure_aug_dirs(
|
img_out_dir, msk_out_dir, msk2_out_dir, raw_out_dir = ensure_aug_dirs(
|
||||||
group_name,
|
group_name,
|
||||||
use_masks2=use_masks2,
|
use_masks2=use_masks2,
|
||||||
use_raw=use_raw,
|
use_raw=use_raw,
|
||||||
is_preview_mode=use_preview_mode
|
is_preview_mode=True
|
||||||
)
|
)
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
for img_file in sorted(imgs):
|
for base, raw_file in sorted(raw_map.items()):
|
||||||
base, _ = os.path.splitext(img_file)
|
|
||||||
msk_file = msk_map.get(base)
|
msk_file = msk_map.get(base)
|
||||||
if not msk_file:
|
if not msk_file:
|
||||||
print(f"[WARN] [{group_name}] Máscara não encontrada para {img_file}, pulando.")
|
print(f"[WARN] [{group_name}] Máscara não encontrada para RAW {base}, pulando.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raw_file = raw_map.get(base) if use_raw else None
|
|
||||||
if use_raw and not raw_file:
|
|
||||||
print(f"[WARN] [{group_name}] RAW não encontrado para {img_file}, gerando só preview/mask (+mask2).")
|
|
||||||
|
|
||||||
msk2_file = msk2_map.get(base) if use_masks2 else None
|
msk2_file = msk2_map.get(base) if use_masks2 else None
|
||||||
if use_masks2 and not msk2_file:
|
if use_masks2 and not msk2_file:
|
||||||
print(f"[WARN] [{group_name}] mask2 não encontrada para {img_file}, gerando só img+mask(+raw).")
|
print(f"[WARN] [{group_name}] mask2 não encontrada para {base}, gerando só RAW+preview+mask.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
count += augment_sample(
|
count += augment_sample(
|
||||||
os.path.join(img_dir, img_file),
|
raw_path=raw_file,
|
||||||
msk_file,
|
msk_path=msk_file,
|
||||||
img_out_dir,
|
img_out_dir=img_out_dir,
|
||||||
msk_out_dir,
|
msk_out_dir=msk_out_dir,
|
||||||
copies=copies,
|
copies=copies,
|
||||||
msk2_path=msk2_file,
|
msk2_path=msk2_file,
|
||||||
msk2_out_dir=msk2_out_dir,
|
msk2_out_dir=msk2_out_dir,
|
||||||
raw_path=raw_file,
|
|
||||||
raw_out_dir=raw_out_dir,
|
raw_out_dir=raw_out_dir,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERRO] [{group_name}] {img_file}: {e}")
|
print(f"[ERRO] [{group_name}] {base}: {e}")
|
||||||
print(f"[OK] Grupo '{group_name}' → {count} amostras geradas.")
|
print(f"[OK] Grupo '{group_name}' → {count} amostras geradas.")
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Fallback legacy (sem RAW)
|
||||||
|
# ===============================
|
||||||
|
|
||||||
def process_legacy(copies):
|
def process_legacy(copies):
|
||||||
"""Fallback: modo sem grupos (original/images e original/masks)."""
|
"""
|
||||||
|
Fallback: modo sem grupos (original/images e original/masks).
|
||||||
|
Mantido por compat, mas não usa RAW.
|
||||||
|
"""
|
||||||
if not (os.path.isdir(ORIG_OLD_IMG) and os.path.isdir(ORIG_OLD_MSK)):
|
if not (os.path.isdir(ORIG_OLD_IMG) and os.path.isdir(ORIG_OLD_MSK)):
|
||||||
print("[WARN] Modo legacy não encontrado. Nada a fazer.")
|
print("[WARN] Modo legacy não encontrado. Nada a fazer.")
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -412,7 +590,7 @@ def process_legacy(copies):
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
for img_file in sorted(imgs):
|
for img_file in sorted(imgs):
|
||||||
base, _ = os.path.splitext(img_file)
|
base, img_ext = os.path.splitext(img_file)
|
||||||
msk_file = msk_map.get(base)
|
msk_file = msk_map.get(base)
|
||||||
if not msk_file:
|
if not msk_file:
|
||||||
print(f"[WARN] (legacy) Máscara não encontrada para {img_file}, pulando.")
|
print(f"[WARN] (legacy) Máscara não encontrada para {img_file}, pulando.")
|
||||||
|
|
@ -422,23 +600,44 @@ def process_legacy(copies):
|
||||||
print(f"[WARN] (legacy) mask2 não encontrada para {img_file}, gerando só img+mask.")
|
print(f"[WARN] (legacy) mask2 não encontrada para {img_file}, gerando só img+mask.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
count += augment_sample(
|
img_path = os.path.join(ORIG_OLD_IMG, img_file)
|
||||||
os.path.join(ORIG_OLD_IMG, img_file),
|
img = load_rgb(img_path)
|
||||||
msk_file,
|
base_msk, msk_ext = os.path.splitext(os.path.basename(msk_file))
|
||||||
img_out_dir,
|
msk = load_rgb(msk_file)
|
||||||
msk_out_dir,
|
msk2 = load_rgb(msk2_file) if msk2_file else None
|
||||||
copies=copies,
|
|
||||||
msk2_path=msk2_file,
|
for i in range(copies):
|
||||||
msk2_out_dir=msk2_out_dir,
|
if msk2 is not None and msk2_out_dir:
|
||||||
raw_path=None,
|
aug_geo = train_geo(image=img, mask=msk, mask2=msk2)
|
||||||
raw_out_dir=None,
|
else:
|
||||||
)
|
aug_geo = train_geo(image=img, mask=msk)
|
||||||
|
|
||||||
|
img_g = aug_geo["image"]
|
||||||
|
msk_g = aug_geo["mask"]
|
||||||
|
msk2_g = aug_geo.get("mask2", None) if (msk2 is not None and msk2_out_dir) else None
|
||||||
|
|
||||||
|
out_img = os.path.join(img_out_dir, f"{base}_aug_{i:02d}{img_ext}")
|
||||||
|
out_msk = os.path.join(msk_out_dir, f"{base_msk}_aug_{i:02d}{msk_ext}")
|
||||||
|
save_rgb(out_img, img_g)
|
||||||
|
save_rgb(out_msk, msk_g)
|
||||||
|
|
||||||
|
if msk2_g is not None and msk2_out_dir:
|
||||||
|
msk2_ext = os.path.splitext(msk2_file)[1]
|
||||||
|
out_msk2 = os.path.join(msk2_out_dir, f"{base_msk}_aug_{i:02d}{msk2_ext}")
|
||||||
|
save_rgb(out_msk2, msk2_g)
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERRO] (legacy) {img_file}: {e}")
|
print(f"[ERRO] (legacy) {img_file}: {e}")
|
||||||
print(f"[OK] Legacy → {count} amostras geradas.")
|
print(f"[OK] Legacy → {count} amostras geradas.")
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# main
|
||||||
|
# ===============================
|
||||||
|
|
||||||
def main(copies=5, groups_csv=None):
|
def main(copies=5, groups_csv=None):
|
||||||
total = 0
|
total = 0
|
||||||
if os.path.isdir(ORIG_GROUP_ROOT):
|
if os.path.isdir(ORIG_GROUP_ROOT):
|
||||||
|
|
@ -462,7 +661,7 @@ def main(copies=5, groups_csv=None):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
ap = argparse.ArgumentParser(description="Augmentação por grupos (preview/raw/masks ou images/masks)")
|
ap = argparse.ArgumentParser(description="Augmentação por grupos usando RAW4 (preview gerado do RAW).")
|
||||||
ap.add_argument("--copies", type=int, default=5, help="Número de cópias augmentadas por imagem (default=5).")
|
ap.add_argument("--copies", type=int, default=5, help="Número de cópias augmentadas por imagem (default=5).")
|
||||||
ap.add_argument("--groups", type=str, default=None, help="Lista de grupos separados por vírgula (ex: chao,erva_cana).")
|
ap.add_argument("--groups", type=str, default=None, help="Lista de grupos separados por vírgula (ex: chao,erva_cana).")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,11 @@ with open("config.json", "r", encoding="utf-8") as f:
|
||||||
MODELO = config["camera"]
|
MODELO = config["camera"]
|
||||||
USE_MASKS2 = config["dual_head"]
|
USE_MASKS2 = config["dual_head"]
|
||||||
RESOLUCAO = tuple(config["resolucao"]) # [W,H]
|
RESOLUCAO = tuple(config["resolucao"]) # [W,H]
|
||||||
|
MODEL_NAME = config["model_name"]
|
||||||
|
CHANNELS = int(config.get("channels", 4))
|
||||||
pasta_base = os.path.join(MODELO, "dataset")
|
pasta_base = os.path.join(MODELO, "dataset")
|
||||||
labelmap_path = os.path.join(pasta_base, "labelmap.txt")
|
labelmap_path = os.path.join(pasta_base, "labelmap.txt")
|
||||||
|
save_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME, f"raw{CHANNELS}")
|
||||||
|
|
||||||
RESOLUCOES = {f"{RESOLUCAO[0]}x{RESOLUCAO[1]}": (RESOLUCAO[0], RESOLUCAO[1])}
|
RESOLUCOES = {f"{RESOLUCAO[0]}x{RESOLUCAO[1]}": (RESOLUCAO[0], RESOLUCAO[1])}
|
||||||
FONTES = ["original", "augmented"]
|
FONTES = ["original", "augmented"]
|
||||||
|
|
@ -98,28 +101,20 @@ def map_raws_by_base(raw_dir: str) -> Dict[str, str]:
|
||||||
by_base[base] = os.path.join(raw_dir, fname)
|
by_base[base] = os.path.join(raw_dir, fname)
|
||||||
return by_base
|
return by_base
|
||||||
|
|
||||||
def load_raw_gray_autodtype(path: str, src_hw: Tuple[int,int]) -> np.ndarray:
|
def load_raw4_float(path: str, src_hw: Tuple[int,int]) -> np.ndarray:
|
||||||
"""
|
|
||||||
Carrega RAW como (H,W) detectando uint8/uint16 pelo tamanho do arquivo.
|
|
||||||
"""
|
|
||||||
h, w = src_hw
|
h, w = src_hw
|
||||||
npx = h * w
|
npx = h * w
|
||||||
fsize = os.path.getsize(path)
|
fsize = os.path.getsize(path)
|
||||||
|
|
||||||
if fsize == npx:
|
num_floats = fsize // 4
|
||||||
dtype = np.uint8
|
if num_floats != 4 * npx:
|
||||||
elif fsize == npx * 2:
|
|
||||||
dtype = np.uint16
|
|
||||||
else:
|
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Tamanho inesperado para RAW {path}: {fsize} bytes "
|
f"RAW {path}: esperado 4 canais float32, "
|
||||||
f"(esperado {npx} (u8) ou {npx*2} (u16) para {w}x{h})."
|
f"mas num_floats={num_floats}, H*W={npx}"
|
||||||
)
|
)
|
||||||
|
|
||||||
data = np.fromfile(path, dtype=dtype)
|
data = np.fromfile(path, dtype=np.float32)
|
||||||
if data.size != npx:
|
return data.reshape(4, h, w) # (C,H,W)
|
||||||
raise RuntimeError(f"RAW {path}: size={data.size} != {npx} (HxW)")
|
|
||||||
return data.reshape((h, w))
|
|
||||||
|
|
||||||
def save_raw(path: str, arr: np.ndarray):
|
def save_raw(path: str, arr: np.ndarray):
|
||||||
np.asarray(arr).tofile(path)
|
np.asarray(arr).tofile(path)
|
||||||
|
|
@ -231,9 +226,18 @@ def normalize_group_raw(fonte_root: str, fonte_nome: str, cor_para_id, ignore_id
|
||||||
if usar_raw and out_raw:
|
if usar_raw and out_raw:
|
||||||
if raw_path:
|
if raw_path:
|
||||||
src_h, src_w = prev_bgr.shape[:2]
|
src_h, src_w = prev_bgr.shape[:2]
|
||||||
mosaic = load_raw_gray_autodtype(raw_path, (src_h, src_w))
|
|
||||||
out_w, out_h = dim # dim = (W, H)
|
raw4 = load_raw4_float(raw_path, (src_h, src_w)) # (4, Hsrc, Wsrc)
|
||||||
raw4 = mosaic_to_raw4_resized_buf(mosaic, out_h, out_w) # float32, shape (4,H,W) em 0..1
|
out_w, out_h = dim
|
||||||
|
|
||||||
|
if (src_w, src_h) != (out_w, out_h):
|
||||||
|
# redimensiona cada canal
|
||||||
|
chans = []
|
||||||
|
for k in range(raw4.shape[0]):
|
||||||
|
ch = raw4[k]
|
||||||
|
ch_res = cv2.resize(ch, (out_w, out_h), interpolation=cv2.INTER_AREA)
|
||||||
|
chans.append(ch_res.astype(np.float32))
|
||||||
|
raw4 = np.stack(chans, axis=0) # (4, out_h, out_w)
|
||||||
|
|
||||||
# Atualiza acumuladores de stats
|
# Atualiza acumuladores de stats
|
||||||
# raw4: (C,H,W) -> (C,N)
|
# raw4: (C,H,W) -> (C,N)
|
||||||
|
|
@ -302,7 +306,8 @@ def main(args):
|
||||||
"pixels_per_channel": int(GLOBAL_PIXELS),
|
"pixels_per_channel": int(GLOBAL_PIXELS),
|
||||||
}
|
}
|
||||||
|
|
||||||
stats_path = os.path.join(pasta_base, "norm_stats.json")
|
garantir_dir(save_path)
|
||||||
|
stats_path = os.path.join(save_path, "norm_stats.json")
|
||||||
with open(stats_path, "w", encoding="utf-8") as f:
|
with open(stats_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(stats, f, indent=2, ensure_ascii=False)
|
json.dump(stats, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
#python _8_train_segformer_b3_raw.py --config config.json --epochs 20 --batch 4 --lr 3e-5 --wd 0.01 --num_workers 4 --amp --amp_val --grad_accum 2 --class_weights auto
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "max_split_size_mb:128")
|
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "max_split_size_mb:128")
|
||||||
|
|
||||||
|
|
@ -311,7 +314,7 @@ def main():
|
||||||
|
|
||||||
# Caminho padrão: dentro do dataset, nome do arquivo de stats
|
# Caminho padrão: dentro do dataset, nome do arquivo de stats
|
||||||
# (ajusta aqui pro nome que você realmente usou: norm_stats.json, por ex.)
|
# (ajusta aqui pro nome que você realmente usou: norm_stats.json, por ex.)
|
||||||
norm_stats_path = os.path.join(dataset_path, "norm_stats.json")
|
norm_stats_path = os.path.join(save_path, "norm_stats.json")
|
||||||
if args.norm_stats is not None:
|
if args.norm_stats is not None:
|
||||||
norm_stats_path = args.norm_stats
|
norm_stats_path = args.norm_stats
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,18 @@ Teste/visualização do SegFormer RAW (4 ou 5 canais) treinado no _8_train_segfo
|
||||||
|
|
||||||
Modos:
|
Modos:
|
||||||
|
|
||||||
1) Dataset (padrão):
|
1) Dataset (com GT):
|
||||||
- Lê os RAWs + masks da pasta dataset/split/<split_folder>
|
- Usa RawSegDataset (raws + masks) a partir de split (train/val/test)
|
||||||
- Mostra: [preview RAW (RGB)] | [mask GT] | [mask predita]
|
- Mostra: [preview RAW (RGB)] | [mask GT] | [overlay predito]
|
||||||
- Navegação:
|
|
||||||
D -> próxima
|
|
||||||
A -> anterior
|
|
||||||
Q -> sair
|
|
||||||
|
|
||||||
2) Câmera ao vivo (--camera):
|
2) Pasta sem máscara (--test_folder sem masks):
|
||||||
|
- Faz inferência só com os RAWs (sem GT)
|
||||||
|
- Mostra: [preview RAW (RGB)] | [overlay predito]
|
||||||
|
|
||||||
|
3) Câmera ao vivo (--camera):
|
||||||
- Usa Gal5000Camera para pegar RAW4 (R,G,IR,B)
|
- Usa Gal5000Camera para pegar RAW4 (R,G,IR,B)
|
||||||
- Monta entrada com 4 ou 5 canais (IR bruto / NDVI / IR+NDVI) conforme config
|
- Monta os 4/5 canais conforme config
|
||||||
- Faz inferência com SegFormer
|
- Mostra overlay em tempo real
|
||||||
- Mostra RGB + overlay da segmentação em tempo real
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
@ -30,29 +29,189 @@ import cv2
|
||||||
import torch
|
import torch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from utils import converter_mask_ids_para_rgb, desenhar_legenda_horizontal
|
from utils import converter_mask_ids_para_bgr, desenhar_legenda_horizontal
|
||||||
|
|
||||||
from raw_segformer_service import RawSegformerService, RawSegDataset
|
from raw_segformer_service import RawSegformerService, RawSegDataset # :contentReference[oaicite:0]{index=0}
|
||||||
from gal5000.gal_service import Gal5000Camera
|
from gal5000.gal_service import Gal5000Camera
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Helpers para modo "sem máscara"
|
||||||
|
# (copia condensada da lógica do RawSegDataset)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
_RAW_EXTS = (".npy", ".npz", ".raw")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_raw_4ch(path: str, raw_hw):
|
||||||
|
"""
|
||||||
|
Lê um RAW no formato usado pelo pipeline:
|
||||||
|
- mosaico uint8 antigo, ou
|
||||||
|
- RAW4 float32 novo, shape (4,H,W), salvo pelo normalize
|
||||||
|
|
||||||
|
Retorna: arr (H,W,4) na ordem [R,G,B,IR] em dtype original.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
H, W = raw_hw
|
||||||
|
ext = os.path.splitext(path)[1].lower()
|
||||||
|
|
||||||
|
if ext == ".npz":
|
||||||
|
z = np.load(path)
|
||||||
|
key = list(z.keys())[0]
|
||||||
|
arr = z[key]
|
||||||
|
elif ext == ".npy":
|
||||||
|
arr = np.load(path)
|
||||||
|
elif ext == ".raw":
|
||||||
|
size_bytes = os.path.getsize(path)
|
||||||
|
mosa_bytes = H * W # mosaico uint8
|
||||||
|
raw4_bytes = 4 * H * W * 4 # 4 canais * H * W * 4 bytes (float32)
|
||||||
|
|
||||||
|
if size_bytes == mosa_bytes:
|
||||||
|
# Modo antigo: mosaico 2x2 uint8
|
||||||
|
arr_flat = np.fromfile(path, dtype=np.uint8)
|
||||||
|
raw2d = arr_flat.reshape(H, W)
|
||||||
|
|
||||||
|
if (H % 2) != 0 or (W % 2) != 0:
|
||||||
|
raise RuntimeError(f"raw_hw deve ser par em H e W (veio H={H}, W={W})")
|
||||||
|
|
||||||
|
H2, W2 = H // 2, W // 2
|
||||||
|
r_sub = raw2d[0::2, 0::2]
|
||||||
|
g_sub = raw2d[0::2, 1::2]
|
||||||
|
ir_sub = raw2d[1::2, 0::2]
|
||||||
|
b_sub = raw2d[1::2, 1::2]
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
def upsample(ch_2d: np.ndarray) -> np.ndarray:
|
||||||
|
im = Image.fromarray(ch_2d) # uint8
|
||||||
|
im = im.resize((W, H), resample=Image.BILINEAR)
|
||||||
|
return np.array(im)
|
||||||
|
|
||||||
|
r_full = upsample(r_sub)
|
||||||
|
g_full = upsample(g_sub)
|
||||||
|
ir_full = upsample(ir_sub)
|
||||||
|
b_full = upsample(b_sub)
|
||||||
|
|
||||||
|
# (H, W, 4) uint8 [R,G,B,IR]
|
||||||
|
arr = np.stack([r_full, g_full, b_full, ir_full], axis=-1).astype(np.uint8)
|
||||||
|
|
||||||
|
elif size_bytes == raw4_bytes:
|
||||||
|
# Novo modo: RAW4 float32 salvo pelo normalize_raw
|
||||||
|
arr_f32 = np.fromfile(path, dtype=np.float32)
|
||||||
|
raw4 = arr_f32.reshape(4, H, W) # (C,H,W) [R,G,IR,B]
|
||||||
|
arr = np.transpose(raw4, (1, 2, 0)) # (H,W,4) [R,G,IR,B]
|
||||||
|
# Reorganiza pra contrato interno: [R,G,B,IR]
|
||||||
|
arr = arr[..., [0, 1, 3, 2]]
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Tamanho inesperado em {path}: {size_bytes} bytes "
|
||||||
|
f"(esperado {mosa_bytes} ou {raw4_bytes})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Extensão não suportada para RAW: {ext} ({path})")
|
||||||
|
|
||||||
|
if arr.ndim != 3:
|
||||||
|
raise RuntimeError(f"RAW precisa ser 3D, veio {arr.shape} em {path}")
|
||||||
|
|
||||||
|
if arr.shape[0] == 4 and arr.shape[-1] != 4:
|
||||||
|
arr = np.transpose(arr, (1, 2, 0)) # (H,W,4)
|
||||||
|
|
||||||
|
if arr.shape[-1] != 4:
|
||||||
|
raise RuntimeError(f"Esperava 4 canais base, veio shape={arr.shape} em {path}")
|
||||||
|
|
||||||
|
return arr # (H,W,4) [R,G,B,IR]
|
||||||
|
|
||||||
|
|
||||||
|
def _scale_to_float01(raw: np.ndarray, max_value: float | None = None) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Normaliza para float32 0..1 (igual RawSegDataset._scale_to_float01):
|
||||||
|
- uint8 / uint16: divide por max_value ou 255/65535
|
||||||
|
- float: usa max_value se fornecido; senão assume 0..1
|
||||||
|
"""
|
||||||
|
if raw.dtype == np.uint16:
|
||||||
|
mv = float(max_value) if max_value is not None else 65535.0
|
||||||
|
elif raw.dtype == np.uint8:
|
||||||
|
mv = float(max_value) if max_value is not None else 255.0
|
||||||
|
else:
|
||||||
|
mv = float(max_value) if max_value is not None else None
|
||||||
|
|
||||||
|
raw_f = raw.astype(np.float32)
|
||||||
|
if mv is not None and mv > 0:
|
||||||
|
raw_f /= mv
|
||||||
|
|
||||||
|
raw_f = np.clip(raw_f, 0.0, 1.0)
|
||||||
|
return raw_f
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_raw_paths(root: str) -> list[str]:
|
||||||
|
"""
|
||||||
|
Coleta caminhos de RAWs em 'root' usando a mesma convenção do dataset:
|
||||||
|
- se root/group existe: procura group/*/raws
|
||||||
|
- senão: procura root/raws
|
||||||
|
"""
|
||||||
|
paths = []
|
||||||
|
|
||||||
|
dir_group = os.path.join(root, "group")
|
||||||
|
if os.path.isdir(dir_group):
|
||||||
|
# modo com grupos
|
||||||
|
for g in sorted(os.listdir(dir_group)):
|
||||||
|
gdir = os.path.join(dir_group, g)
|
||||||
|
if not os.path.isdir(gdir):
|
||||||
|
continue
|
||||||
|
g_raw = os.path.join(gdir, "raws")
|
||||||
|
if not os.path.isdir(g_raw):
|
||||||
|
continue
|
||||||
|
for fn in sorted(os.listdir(g_raw)):
|
||||||
|
if fn.lower().endswith(_RAW_EXTS):
|
||||||
|
paths.append(os.path.join(g_raw, fn))
|
||||||
|
else:
|
||||||
|
# modo simples
|
||||||
|
dir_raws = os.path.join(root, "raws")
|
||||||
|
if not os.path.isdir(dir_raws):
|
||||||
|
raise RuntimeError(f"Modo sem máscara: não achei pasta 'raws' em {root}")
|
||||||
|
for fn in sorted(os.listdir(dir_raws)):
|
||||||
|
if fn.lower().endswith(_RAW_EXTS):
|
||||||
|
paths.append(os.path.join(dir_raws, fn))
|
||||||
|
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Script principal
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
# Config / treino
|
# Config / treino
|
||||||
parser.add_argument("--config", default="config.json", help="Caminho do config.json (mesmo do treino)")
|
parser.add_argument("--config", default="config.json", help="Caminho do config.json (mesmo do treino)")
|
||||||
parser.add_argument("--split_folder", type=str, default="val", help="train, val ou test (dentro de dataset/split)")
|
parser.add_argument("--split_folder", type=str, default="val", help="train, val ou test (dentro de dataset/split)")
|
||||||
parser.add_argument("--root_override", type=str, default=None, help="Se quiser apontar direto para uma pasta split (sobrepõe split_folder)")
|
parser.add_argument("--root_override", type=str, default=None, help="Aponta direto para uma pasta split (sobrepõe split_folder)")
|
||||||
parser.add_argument("--ckpt", type=str, default=None, help="Caminho do .pt (se não informar, usa best_miou.pt da pasta raw4)")
|
parser.add_argument(
|
||||||
parser.add_argument("--raw_max", type=float, default=None, help="Mesmo raw_max do treino (se usou). Usado internamente no dataset.")
|
"--test_folder",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"Pasta de teste para visualização. "
|
||||||
|
"Se tiver masks, usa dataset normal (mostra GT). "
|
||||||
|
"Se tiver apenas raws, entra em modo inferência-only."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument("--ckpt", type=str, default=None, help="Caminho do .pt (se não informar, usa best_miou.pt da pasta rawX)")
|
||||||
|
parser.add_argument("--raw_max", type=float, default=None, help="Mesmo raw_max do treino (se usou).")
|
||||||
|
|
||||||
# Modo câmera
|
# Modo câmera
|
||||||
parser.add_argument("--camera", action="store_true", help="Usa a GAL5000 ao vivo via DLL (realtime)")
|
parser.add_argument("--camera", action="store_true", help="Usa a GAL5000 ao vivo via DLL (realtime)")
|
||||||
parser.add_argument("--norm_stats", type=str, default=None, help="Caminho para JSON com mean/std por canal (ex: norm_stats.json).")
|
parser.add_argument("--norm_stats", type=str, default=None, help="Caminho para JSON com mean/std por canal (ex: norm_stats.json).")
|
||||||
parser.add_argument("--dll_dir", type=str, default=r"C:\ZendionInc\agrobot_base\Python\gal5000\dlls", help="Pasta onde fica VT_SDK64.dll e dependências")
|
parser.add_argument(
|
||||||
|
"--dll_dir",
|
||||||
|
type=str,
|
||||||
|
default=r"C:\ZendionInc\agrobot_base\Python\gal5000\dlls",
|
||||||
|
help="Pasta onde fica VT_SDK64.dll e dependências",
|
||||||
|
)
|
||||||
parser.add_argument("--dll_name", type=str, default="VT_SDK64.dll")
|
parser.add_argument("--dll_name", type=str, default="VT_SDK64.dll")
|
||||||
parser.add_argument("--resize_h", type=int, default=None, help="Altura para inferência (velocidade)")
|
parser.add_argument("--resize_h", type=int, default=None, help="Altura para inferência (override)")
|
||||||
parser.add_argument("--resize_w", type=int, default=None, help="Largura para inferência (velocidade)")
|
parser.add_argument("--resize_w", type=int, default=None, help="Largura para inferência (override)")
|
||||||
parser.add_argument("--alpha", type=float, default=0.45, help="Alpha do overlay da máscara")
|
parser.add_argument("--alpha", type=float, default=0.45, help="Alpha do overlay da máscara")
|
||||||
parser.add_argument("--fps_win", type=int, default=30, help="Janela para FPS médio")
|
parser.add_argument("--fps_win", type=int, default=30, help="Janela para FPS médio")
|
||||||
parser.add_argument("--timeout_ms", type=int, default=2000, help="Timeout da captura da câmera em ms")
|
parser.add_argument("--timeout_ms", type=int, default=2000, help="Timeout da captura da câmera em ms")
|
||||||
|
|
@ -67,16 +226,17 @@ def main():
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
|
|
||||||
MODELO = config["camera"] # ex: "gal5000"
|
MODELO = config["camera"] # ex: "gal5000"
|
||||||
MODEL_NAME = config["model_name"] # ex: "segformer_b3"
|
#MODEL_NAME = config["model_name"] # ex: "segformer_b0"
|
||||||
modelo_folder = config["modelo"] # ex: "dif"
|
MODEL_NAME = "pulv2_1008x800"
|
||||||
|
modelo_folder = config["modelo"] # ex: "weed_1008x800"
|
||||||
|
|
||||||
USE_NDVI = bool(config.get("use_ndvi", False))
|
USE_NDVI = bool(config.get("use_ndvi", False))
|
||||||
CHANNELS = int(config.get("channels", 4))
|
CHANNELS = int(config.get("channels", 4))
|
||||||
RESOLUCAO = config["resolucao"]
|
RESOLUCAO = config["resolucao"]
|
||||||
W, H = RESOLUCAO[0], RESOLUCAO[1]
|
W, H = RESOLUCAO[0], RESOLUCAO[1]
|
||||||
if (args.resize_h is not None):
|
if args.resize_h is not None:
|
||||||
H = args.resize_h
|
H = args.resize_h
|
||||||
if (args.resize_w is not None):
|
if args.resize_w is not None:
|
||||||
W = args.resize_w
|
W = args.resize_w
|
||||||
|
|
||||||
print(f"[cfg] resolucao nominal RAW: {W}x{H}")
|
print(f"[cfg] resolucao nominal RAW: {W}x{H}")
|
||||||
|
|
@ -86,12 +246,12 @@ def main():
|
||||||
labelmap_path = os.path.join(dataset_path, "labelmap.txt")
|
labelmap_path = os.path.join(dataset_path, "labelmap.txt")
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Normalização fixa (como no treino)
|
# Normalização fixa (igual treino)
|
||||||
# ==========================
|
# ==========================
|
||||||
norm_mean = None
|
norm_mean = None
|
||||||
norm_std = None
|
norm_std = None
|
||||||
|
|
||||||
norm_stats_path = os.path.join(dataset_path, "norm_stats.json")
|
norm_stats_path = os.path.join(MODELO, "backup", modelo_folder, MODEL_NAME, f"raw{CHANNELS}", "norm_stats.json")
|
||||||
if args.norm_stats is not None:
|
if args.norm_stats is not None:
|
||||||
norm_stats_path = args.norm_stats
|
norm_stats_path = args.norm_stats
|
||||||
if os.path.isfile(norm_stats_path):
|
if os.path.isfile(norm_stats_path):
|
||||||
|
|
@ -107,10 +267,8 @@ def main():
|
||||||
print(f"[NORM] mean={stats_mean}")
|
print(f"[NORM] mean={stats_mean}")
|
||||||
print(f"[NORM] std ={stats_std}")
|
print(f"[NORM] std ={stats_std}")
|
||||||
|
|
||||||
# JSON vem como ["R","G","IR","B"]
|
|
||||||
idx_by_name = {name: i for i, name in enumerate(stats_channels)}
|
idx_by_name = {name: i for i, name in enumerate(stats_channels)}
|
||||||
|
|
||||||
# Sempre montamos na ordem interna base [R,G,B,IR]
|
|
||||||
m_R = stats_mean[idx_by_name["R"]]
|
m_R = stats_mean[idx_by_name["R"]]
|
||||||
m_G = stats_mean[idx_by_name["G"]]
|
m_G = stats_mean[idx_by_name["G"]]
|
||||||
m_IR = stats_mean[idx_by_name["IR"]]
|
m_IR = stats_mean[idx_by_name["IR"]]
|
||||||
|
|
@ -121,20 +279,13 @@ def main():
|
||||||
s_IR = stats_std[idx_by_name["IR"]]
|
s_IR = stats_std[idx_by_name["IR"]]
|
||||||
s_B = stats_std[idx_by_name["B"]]
|
s_B = stats_std[idx_by_name["B"]]
|
||||||
|
|
||||||
# Agora montamos mean/std na ordem REAL dos canais que o modelo vê:
|
|
||||||
# - channels=4, use_ndvi=False -> [R,G,B,IR]
|
|
||||||
# - channels=4, use_ndvi=True -> [R,G,B,NDVI] (NDVI norm dinâmica)
|
|
||||||
# - channels=5, use_ndvi=True -> [R,G,B,IR,NDVI]
|
|
||||||
if CHANNELS == 4 and not USE_NDVI:
|
if CHANNELS == 4 and not USE_NDVI:
|
||||||
# [R,G,B,IR]
|
|
||||||
norm_mean = [m_R, m_G, m_B, m_IR]
|
norm_mean = [m_R, m_G, m_B, m_IR]
|
||||||
norm_std = [s_R, s_G, s_B, s_IR]
|
norm_std = [s_R, s_G, s_B, s_IR]
|
||||||
elif CHANNELS == 4 and USE_NDVI:
|
elif CHANNELS == 4 and USE_NDVI:
|
||||||
# [R,G,B,NDVI] -> NDVI fica com normalização identidade (0,1)
|
|
||||||
norm_mean = [m_R, m_G, m_B, 0.0]
|
norm_mean = [m_R, m_G, m_B, 0.0]
|
||||||
norm_std = [s_R, s_G, s_B, 1.0]
|
norm_std = [s_R, s_G, s_B, 1.0]
|
||||||
elif CHANNELS == 5 and USE_NDVI:
|
elif CHANNELS == 5 and USE_NDVI:
|
||||||
# [R,G,B,IR,NDVI]
|
|
||||||
norm_mean = [m_R, m_G, m_B, m_IR, 0.0]
|
norm_mean = [m_R, m_G, m_B, m_IR, 0.0]
|
||||||
norm_std = [s_R, s_G, s_B, s_IR, 1.0]
|
norm_std = [s_R, s_G, s_B, s_IR, 1.0]
|
||||||
else:
|
else:
|
||||||
|
|
@ -150,8 +301,6 @@ def main():
|
||||||
if args.ckpt is not None:
|
if args.ckpt is not None:
|
||||||
ckpt_path = args.ckpt
|
ckpt_path = args.ckpt
|
||||||
else:
|
else:
|
||||||
# Caminho padrão igual ao treino:
|
|
||||||
# save_path = MODELO/backup/modelo/model_name/raw4
|
|
||||||
save_path = os.path.join(MODELO, "backup", modelo_folder, MODEL_NAME, f"raw{CHANNELS}")
|
save_path = os.path.join(MODELO, "backup", modelo_folder, MODEL_NAME, f"raw{CHANNELS}")
|
||||||
ckpt_path = os.path.join(save_path, "best_miou.pt")
|
ckpt_path = os.path.join(save_path, "best_miou.pt")
|
||||||
|
|
||||||
|
|
@ -160,7 +309,7 @@ def main():
|
||||||
|
|
||||||
print(f"[model] ckpt = {ckpt_path}")
|
print(f"[model] ckpt = {ckpt_path}")
|
||||||
|
|
||||||
# ---- Instancia service do modelo RAW (4 ou 5 canais) ----
|
# ---- Instancia service do modelo RAW ----
|
||||||
model_svc = RawSegformerService(
|
model_svc = RawSegformerService(
|
||||||
config_path=args.config,
|
config_path=args.config,
|
||||||
ckpt_path=ckpt_path,
|
ckpt_path=ckpt_path,
|
||||||
|
|
@ -182,7 +331,13 @@ def main():
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
if args.camera:
|
if args.camera:
|
||||||
print("[mode] Câmera ao vivo (GAL5000 + SegFormer RAW)")
|
print("[mode] Câmera ao vivo (GAL5000 + SegFormer RAW)")
|
||||||
cam = Gal5000Camera(dll_dir=args.dll_dir, dll_name=args.dll_name, raw_w=W, raw_h=H, use_auto_exposure=True)
|
cam = Gal5000Camera(
|
||||||
|
dll_dir=args.dll_dir,
|
||||||
|
dll_name=args.dll_name,
|
||||||
|
raw_w=W,
|
||||||
|
raw_h=H,
|
||||||
|
use_auto_exposure=True
|
||||||
|
)
|
||||||
|
|
||||||
win = "GAL5000 RAW + SegFormer (Q=quit)"
|
win = "GAL5000 RAW + SegFormer (Q=quit)"
|
||||||
cv2.namedWindow(win, cv2.WINDOW_NORMAL)
|
cv2.namedWindow(win, cv2.WINDOW_NORMAL)
|
||||||
|
|
@ -194,22 +349,17 @@ def main():
|
||||||
cam.start_streaming()
|
cam.start_streaming()
|
||||||
while True:
|
while True:
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
# raw4_base: (4,H,W) float32 0..1 [R,G,IR,B] já redimensionado
|
|
||||||
raw4_base, dbg = cam.grab_raw4(out_h=H, out_w=W, timeout_ms=args.timeout_ms)
|
raw4_base, dbg = cam.grab_raw4(out_h=H, out_w=W, timeout_ms=args.timeout_ms)
|
||||||
t1 = time.time()
|
t1 = time.time()
|
||||||
|
|
||||||
# Separa canais básicos
|
|
||||||
r = raw4_base[0]
|
r = raw4_base[0]
|
||||||
g = raw4_base[1]
|
g = raw4_base[1]
|
||||||
ir = raw4_base[2]
|
ir = raw4_base[2]
|
||||||
b = raw4_base[3]
|
b = raw4_base[3]
|
||||||
|
|
||||||
# Deixa o service montar a entrada final com 4 ou 5 canais
|
raw_input = model_svc.build_raw_input(r, g, ir, b)
|
||||||
# (R,G,B,IR) | (R,G,B,NDVI) | (R,G,B,IR,NDVI)
|
|
||||||
raw_input = model_svc.build_raw_input(r, g, ir, b) # (C,H,W) float32 0..1
|
|
||||||
t2 = time.time()
|
t2 = time.time()
|
||||||
|
|
||||||
# Inferência + previews via service
|
|
||||||
pred_ids = model_svc.infer_raw(raw_input)
|
pred_ids = model_svc.infer_raw(raw_input)
|
||||||
t3 = time.time()
|
t3 = time.time()
|
||||||
_, _, overlay, _, t_pvw = model_svc.preview_infer_cached(raw_input, pred_ids, alpha=args.alpha)
|
_, _, overlay, _, t_pvw = model_svc.preview_infer_cached(raw_input, pred_ids, alpha=args.alpha)
|
||||||
|
|
@ -234,7 +384,8 @@ def main():
|
||||||
]
|
]
|
||||||
y = 24
|
y = 24
|
||||||
for line in lines:
|
for line in lines:
|
||||||
cv2.putText(overlay, line, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
|
cv2.putText(overlay, line, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.5,
|
||||||
|
(255, 255, 255), 1, cv2.LINE_AA)
|
||||||
y += 22
|
y += 22
|
||||||
|
|
||||||
cv2.imshow(win, overlay)
|
cv2.imshow(win, overlay)
|
||||||
|
|
@ -246,18 +397,24 @@ def main():
|
||||||
return
|
return
|
||||||
|
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
# MODO DATASET (A/D)
|
# MODO DATASET / TEST_FOLDER
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
|
|
||||||
# ---- Define raiz do split ----
|
# ---- Define raiz do split/test ----
|
||||||
if args.root_override is not None:
|
if args.test_folder is not None:
|
||||||
|
raw_root = args.test_folder
|
||||||
|
elif args.root_override is not None:
|
||||||
raw_root = args.root_override
|
raw_root = args.root_override
|
||||||
else:
|
else:
|
||||||
raw_root = os.path.join(dataset_path, "split", args.split_folder)
|
raw_root = os.path.join(dataset_path, "split", args.split_folder)
|
||||||
|
|
||||||
print(f"[data] raw_root = {raw_root}")
|
print(f"[data] raw_root = {raw_root}")
|
||||||
|
|
||||||
# Dataset RAW com 4 ou 5 canais, mesma lógica do treino
|
has_gt = True
|
||||||
|
ds = None
|
||||||
|
raw_paths = []
|
||||||
|
|
||||||
|
try:
|
||||||
ds = RawSegDataset(
|
ds = RawSegDataset(
|
||||||
raw_root,
|
raw_root,
|
||||||
labelmap_path=labelmap_path,
|
labelmap_path=labelmap_path,
|
||||||
|
|
@ -267,60 +424,95 @@ def main():
|
||||||
use_ndvi=USE_NDVI,
|
use_ndvi=USE_NDVI,
|
||||||
channels=CHANNELS,
|
channels=CHANNELS,
|
||||||
)
|
)
|
||||||
|
print("[mode] Dataset com GT (masks) detectado. Mostrando GT + overlay.")
|
||||||
|
except RuntimeError as e:
|
||||||
|
msg = str(e)
|
||||||
|
if "Nenhum par raw/mask encontrado" in msg:
|
||||||
|
print("[mode] Nenhum par raw/mask encontrado. Entrando em modo inferência-only (sem GT).")
|
||||||
|
has_gt = False
|
||||||
|
raw_paths = _collect_raw_paths(raw_root)
|
||||||
|
if not raw_paths:
|
||||||
|
raise RuntimeError(f"Modo inferência-only: não encontrei nenhum RAW em {raw_root}")
|
||||||
|
print(f"[data] RAWs encontrados: {len(raw_paths)}")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if has_gt:
|
||||||
|
n = len(ds)
|
||||||
|
else:
|
||||||
|
n = len(raw_paths)
|
||||||
|
|
||||||
idx = 0
|
idx = 0
|
||||||
n = len(ds)
|
|
||||||
print(f"[data] total de amostras: {n}")
|
print(f"[data] total de amostras: {n}")
|
||||||
print("Controles: D=próxima, A=anterior, Q=sair")
|
print("Controles: D=próxima, A=anterior, Q=sair")
|
||||||
win_name = "RAW | GT | Pred"
|
|
||||||
cv2.namedWindow(win_name, cv2.WINDOW_NORMAL) # janela redimensionável
|
win_name = "RAW preview | GT | Overlay (SegFormer)" if has_gt else "RAW preview | Overlay (SegFormer)"
|
||||||
|
cv2.namedWindow(win_name, cv2.WINDOW_NORMAL)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
if has_gt:
|
||||||
|
# ------- Caminho com GT -------
|
||||||
sample = ds[idx]
|
sample = ds[idx]
|
||||||
img_raw = sample["image"] # tensor (C,H,W) em 0..1 (C=4 ou 5)
|
img_raw = sample["image"] # tensor (C,H,W) em 0..1
|
||||||
mask_gt = sample["mask"] # tensor (H,W) long
|
mask_gt = sample["mask"] # tensor (H,W) long
|
||||||
|
|
||||||
# Converte para numpy (C,H,W) float32 0..1
|
|
||||||
raw_np = img_raw.cpu().numpy().astype(np.float32)
|
raw_np = img_raw.cpu().numpy().astype(np.float32)
|
||||||
|
pred_ids, preview_rgb, pred_rgb, overlay, t_inf, t_pvw = model_svc.infer_and_preview(
|
||||||
|
raw_np, alpha=args.alpha
|
||||||
|
)
|
||||||
|
|
||||||
# Service faz toda a magia (normalização, NDVI, etc) em cima desse raw_np
|
|
||||||
pred_ids, preview_rgb, pred_rgb, overlay, t_inf, t_pvw = model_svc.infer_and_preview(raw_np, alpha=args.alpha)
|
|
||||||
|
|
||||||
print(f"t_inf: {t_inf:.1f} ms t_pvw: {t_pvw:.1f} ms")
|
|
||||||
|
|
||||||
# GT em RGB
|
|
||||||
mask_gt_np = mask_gt.cpu().numpy().astype(np.uint8)
|
mask_gt_np = mask_gt.cpu().numpy().astype(np.uint8)
|
||||||
gt_rgb = converter_mask_ids_para_rgb(mask_gt_np, colormap_rgb, ignore_id)
|
gt_rgb = converter_mask_ids_para_bgr(mask_gt_np, colormap_rgb, ignore_id)
|
||||||
|
|
||||||
# Empilha: [RAW preview | GT | Pred]
|
|
||||||
h, w, _ = preview_rgb.shape
|
h, w, _ = preview_rgb.shape
|
||||||
gt_resized = cv2.resize(gt_rgb, (w, h), interpolation=cv2.INTER_NEAREST)
|
gt_resized = cv2.resize(gt_rgb, (w, h), interpolation=cv2.INTER_NEAREST)
|
||||||
pred_resized = cv2.resize(pred_rgb, (w, h), interpolation=cv2.INTER_NEAREST)
|
overlay_resized = cv2.resize(overlay, (w, h), interpolation=cv2.INTER_NEAREST)
|
||||||
|
|
||||||
resultado = np.concatenate([preview_rgb, gt_resized, pred_resized], axis=1)
|
resultado = np.concatenate([preview_rgb, gt_resized, overlay_resized], axis=1)
|
||||||
|
|
||||||
# Legenda embaixo
|
|
||||||
legenda = desenhar_legenda_horizontal(colormap_rgb, classes)
|
legenda = desenhar_legenda_horizontal(colormap_rgb, classes)
|
||||||
legenda_resized = cv2.resize(legenda, (resultado.shape[1], legenda.shape[0]), interpolation=cv2.INTER_NEAREST)
|
legenda_resized = cv2.resize(legenda, (resultado.shape[1], legenda.shape[0]), interpolation=cv2.INTER_NEAREST)
|
||||||
resultado_completo = np.concatenate([resultado, legenda_resized], axis=0)
|
resultado_completo = np.concatenate([resultado, legenda_resized], axis=0)
|
||||||
|
|
||||||
# Índice + info de canais
|
header_txt = f"idx {idx + 1}/{n} | C={CHANNELS} NDVI={int(USE_NDVI)}"
|
||||||
cv2.putText(resultado_completo, f"idx {idx + 1}/{n} | C={CHANNELS} NDVI={int(USE_NDVI)}", (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
|
else:
|
||||||
|
# ------- Caminho sem GT (só RAW) -------
|
||||||
|
raw_path = raw_paths[idx]
|
||||||
|
raw4 = _load_raw_4ch(raw_path, raw_hw=(H, W)) # (H,W,4) [R,G,B,IR]
|
||||||
|
raw4_f = _scale_to_float01(raw4, max_value=args.raw_max) # float32 0..1
|
||||||
|
|
||||||
# >>> Ajuste dinâmico ao tamanho da janela <<<
|
# Monta canais finais para o modelo (4 ou 5) via serviço
|
||||||
|
r = raw4_f[..., 0]
|
||||||
|
g = raw4_f[..., 1]
|
||||||
|
b = raw4_f[..., 2]
|
||||||
|
ir = raw4_f[..., 3]
|
||||||
|
|
||||||
|
raw_input = model_svc.build_raw_input(r, g, ir, b) # (C,H,W) float32
|
||||||
|
pred_ids = model_svc.infer_raw(raw_input)
|
||||||
|
_, preview_rgb, _, overlay, _, _ = model_svc.preview_infer_cached(raw_input, pred_ids, alpha=args.alpha)
|
||||||
|
|
||||||
|
h, w, _ = preview_rgb.shape
|
||||||
|
overlay_resized = cv2.resize(overlay, (w, h), interpolation=cv2.INTER_NEAREST)
|
||||||
|
resultado = np.concatenate([preview_rgb, overlay_resized], axis=1)
|
||||||
|
|
||||||
|
header_txt = f"idx {idx + 1}/{n} | C={CHANNELS} NDVI={int(USE_NDVI)} | {os.path.basename(raw_path)}"
|
||||||
|
|
||||||
|
resultado_completo = resultado
|
||||||
|
|
||||||
|
# Escreve header
|
||||||
|
cv2.putText(resultado_completo, header_txt, (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
|
||||||
|
|
||||||
|
# Ajuste dinâmico da janela
|
||||||
try:
|
try:
|
||||||
x, y, win_w, win_h = cv2.getWindowImageRect(win_name)
|
_x, _y, win_w, win_h = cv2.getWindowImageRect(win_name)
|
||||||
except:
|
except Exception:
|
||||||
win_w, win_h = 0, 0
|
win_w, win_h = 0, 0
|
||||||
|
|
||||||
if win_w > 0 and win_h > 0:
|
if win_w > 0 and win_h > 0:
|
||||||
# Redimensiona o painel todo pra caber na janela atual
|
|
||||||
display = cv2.resize(resultado_completo, (win_w, win_h), interpolation=cv2.INTER_NEAREST)
|
display = cv2.resize(resultado_completo, (win_w, win_h), interpolation=cv2.INTER_NEAREST)
|
||||||
else:
|
else:
|
||||||
display = resultado_completo
|
display = resultado_completo
|
||||||
|
|
||||||
# OpenCV usa BGR
|
cv2.imshow(win_name, display)
|
||||||
cv2.imshow(win_name, cv2.cvtColor(display, cv2.COLOR_RGB2BGR))
|
|
||||||
key = cv2.waitKey(0) & 0xFF
|
key = cv2.waitKey(0) & 0xFF
|
||||||
|
|
||||||
if key in (ord("q"), ord("Q"), 27):
|
if key in (ord("q"), ord("Q"), 27):
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
{
|
{
|
||||||
"camera": "oak-d",
|
"camera": "gal5000",
|
||||||
"modelo": "segformer_b0",
|
"modelo": "segformer_b0",
|
||||||
"model_name": "nav",
|
"model_name": "pulv_n_1008x800",
|
||||||
"dual_head": false,
|
"dual_head": false,
|
||||||
"main_class_name": "navegavel",
|
"main_class_name": "pulverizar",
|
||||||
"es_classes": "",
|
"es_classes": "",
|
||||||
"model_to_use": "geral",
|
"model_to_use": "geral",
|
||||||
"raw_size": [1296, 1028],
|
"raw_size": [1296, 1028],
|
||||||
"resolucao": [1024, 576],
|
"resolucao": [1008, 800],
|
||||||
"roi_inicio": 0.0,
|
"roi_inicio": 0.0,
|
||||||
"roi_tamanho": 1.0,
|
"roi_tamanho": 1.0,
|
||||||
"shaves": 3,
|
"shaves": 3,
|
||||||
"channels": 3,
|
"channels": 5,
|
||||||
"use_ndvi": true,
|
"use_ndvi": true,
|
||||||
"backbone": "nvidia/segformer-b0-finetuned-ade-512-512"
|
"backbone": "nvidia/segformer-b0-finetuned-ade-512-512"
|
||||||
}
|
}
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
{
|
{
|
||||||
"camera": "gal5000",
|
"camera": "oak-d",
|
||||||
"modelo": "segformer_b0",
|
"modelo": "segformer_b0",
|
||||||
"model_name": "ndvi_big",
|
"model_name": "nav",
|
||||||
"dual_head": false,
|
"dual_head": false,
|
||||||
"main_class_name": "erva",
|
"main_class_name": "navegavel",
|
||||||
"es_classes": "",
|
"es_classes": "",
|
||||||
"model_to_use": "geral",
|
"model_to_use": "geral",
|
||||||
"raw_size": [1296, 1028],
|
"raw_size": [1296, 1028],
|
||||||
"resolucao": [1008, 800],
|
"resolucao": [1024, 576],
|
||||||
"roi_inicio": 0.0,
|
"roi_inicio": 0.0,
|
||||||
"roi_tamanho": 1.0,
|
"roi_tamanho": 1.0,
|
||||||
"shaves": 3,
|
"shaves": 3,
|
||||||
"channels": 5,
|
"channels": 3,
|
||||||
"use_ndvi": true,
|
"use_ndvi": true,
|
||||||
"backbone": "nvidia/segformer-b0-finetuned-ade-512-512"
|
"backbone": "nvidia/segformer-b0-finetuned-ade-512-512"
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,465 @@
|
||||||
|
import ctypes
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
# ========= CONFIG BÁSICA =========
|
||||||
|
|
||||||
|
DLL_NAME = "ControlCANFD.dll"
|
||||||
|
DEVICE_TYPE = 41
|
||||||
|
DEVICE_INDEX = 0
|
||||||
|
CHANNEL_INDEX = 0 # 0 = CAN1, 1 = CAN2
|
||||||
|
|
||||||
|
BMS_ADDR = 0x01
|
||||||
|
PC_ADDR = 0x40
|
||||||
|
|
||||||
|
# Baud 250 kbps para Waveshare USB-CAN-FD (RefType=0)
|
||||||
|
BAUD_250K = 0x1C0008
|
||||||
|
|
||||||
|
|
||||||
|
# ========= ESTRUTURAS DA DLL =========
|
||||||
|
|
||||||
|
class VCI_INIT_CONFIG(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("AccCode", ctypes.c_uint),
|
||||||
|
("AccMask", ctypes.c_uint),
|
||||||
|
("Reserved", ctypes.c_uint),
|
||||||
|
("Filter", ctypes.c_ubyte),
|
||||||
|
("Timing0", ctypes.c_ubyte),
|
||||||
|
("Timing1", ctypes.c_ubyte),
|
||||||
|
("Mode", ctypes.c_ubyte),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class VCI_CAN_OBJ(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("ID", ctypes.c_uint),
|
||||||
|
("TimeStamp", ctypes.c_uint),
|
||||||
|
("TimeFlag", ctypes.c_ubyte),
|
||||||
|
("SendType", ctypes.c_ubyte),
|
||||||
|
("RemoteFlag", ctypes.c_ubyte),
|
||||||
|
("ExternFlag", ctypes.c_ubyte),
|
||||||
|
("DataLen", ctypes.c_ubyte),
|
||||||
|
("Data", ctypes.c_ubyte * 8),
|
||||||
|
("Reserved", ctypes.c_ubyte * 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ========= HELPERS DE PROTOCOLO DALY =========
|
||||||
|
|
||||||
|
def u16_be(hi: int, lo: int) -> int:
|
||||||
|
return ((hi & 0xFF) << 8) | (lo & 0xFF)
|
||||||
|
|
||||||
|
|
||||||
|
def bits_set(byte_val: int):
|
||||||
|
return [i for i in range(8) if (byte_val >> i) & 1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_data_id_from_can_id(can_id: int) -> int:
|
||||||
|
"""
|
||||||
|
Protocolo Daly:
|
||||||
|
ID (29 bits) = [Priority][DataID][AddrA][AddrB]
|
||||||
|
DataID está no segundo byte (bits 16..23)
|
||||||
|
"""
|
||||||
|
return (can_id >> 16) & 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
def make_request_id(data_id: int) -> int:
|
||||||
|
"""
|
||||||
|
PC -> BMS:
|
||||||
|
0x18 [DataID] [BMS_ADDR] [PC_ADDR]
|
||||||
|
Ex: DataID=0x90 => 0x18900140
|
||||||
|
"""
|
||||||
|
return (0x18 << 24) | (data_id << 16) | (BMS_ADDR << 8) | PC_ADDR
|
||||||
|
|
||||||
|
|
||||||
|
def expected_response_id(data_id: int) -> int:
|
||||||
|
"""
|
||||||
|
BMS -> PC:
|
||||||
|
0x18 [DataID] [PC_ADDR] [BMS_ADDR]
|
||||||
|
Ex: DataID=0x90 => 0x18904001
|
||||||
|
"""
|
||||||
|
return (0x18 << 24) | (data_id << 16) | (PC_ADDR << 8) | BMS_ADDR
|
||||||
|
|
||||||
|
|
||||||
|
# ========= PARSERS POR DATAID =========
|
||||||
|
|
||||||
|
def parse_0x90(data: List[int]) -> None:
|
||||||
|
total_v_raw = u16_be(data[0], data[1])
|
||||||
|
meas_v_raw = u16_be(data[2], data[3])
|
||||||
|
current_raw = u16_be(data[4], data[5])
|
||||||
|
soc_raw = u16_be(data[6], data[7])
|
||||||
|
|
||||||
|
total_v = total_v_raw / 10.0
|
||||||
|
meas_v = meas_v_raw / 10.0
|
||||||
|
current = (current_raw - 30000) / 10.0
|
||||||
|
soc = soc_raw / 10.0
|
||||||
|
|
||||||
|
print("== 0x90: Tensão / Corrente / SOC ==")
|
||||||
|
print(f" Tensão total (cumulative): {total_v:.1f} V (raw={total_v_raw})")
|
||||||
|
print(f" Tensão medida (gather) : {meas_v:.1f} V (raw={meas_v_raw})")
|
||||||
|
print(f" Corrente : {current:.1f} A (raw={current_raw})")
|
||||||
|
print(f" SOC : {soc:.1f} % (raw={soc_raw})")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x91(data: List[int]) -> None:
|
||||||
|
vmax_raw = u16_be(data[0], data[1])
|
||||||
|
vmax_idx = data[2]
|
||||||
|
vmin_raw = u16_be(data[3], data[4])
|
||||||
|
vmin_idx = data[5]
|
||||||
|
|
||||||
|
print("== 0x91: Tensão máx/mín de célula ==")
|
||||||
|
print(f" Vmax: {vmax_raw/1000.0:.3f} V (raw={vmax_raw} mV) célula #{vmax_idx}")
|
||||||
|
print(f" Vmin: {vmin_raw/1000.0:.3f} V (raw={vmin_raw} mV) célula #{vmin_idx}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x92(data: List[int]) -> None:
|
||||||
|
tmax = data[0] - 40
|
||||||
|
tmax_i = data[1]
|
||||||
|
tmin = data[2] - 40
|
||||||
|
tmin_i = data[3]
|
||||||
|
|
||||||
|
print("== 0x92: Temperatura máx/mín ==")
|
||||||
|
print(f" T_max: {tmax} °C (sensor #{tmax_i})")
|
||||||
|
print(f" T_min: {tmin} °C (sensor #{tmin_i})")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x93(data: List[int]) -> None:
|
||||||
|
state = data[0]
|
||||||
|
charge_mos = data[1]
|
||||||
|
dischg_mos = data[2]
|
||||||
|
life_raw = data[3]
|
||||||
|
remain_mAh = (
|
||||||
|
(data[4] << 24)
|
||||||
|
| (data[5] << 16)
|
||||||
|
| (data[6] << 8)
|
||||||
|
| data[7]
|
||||||
|
)
|
||||||
|
|
||||||
|
state_map = {0: "parado", 1: "carga", 2: "descarga"}
|
||||||
|
|
||||||
|
print("== 0x93: Estado MOS / capacidade restante ==")
|
||||||
|
print(f" Estado geral : {state} ({state_map.get(state,'desconhecido')})")
|
||||||
|
print(f" Charge MOS state : {charge_mos}")
|
||||||
|
print(f" Discharge MOS state : {dischg_mos}")
|
||||||
|
print(f" BMS life (raw, 'ciclos'): {life_raw}")
|
||||||
|
print(f" Capacidade restante : {remain_mAh} mAh ({remain_mAh/1000.0:.3f} Ah)")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x94(data: List[int]) -> None:
|
||||||
|
n_series = data[0]
|
||||||
|
n_temps = data[1]
|
||||||
|
charger = data[2]
|
||||||
|
load = data[3]
|
||||||
|
io_byte = data[4]
|
||||||
|
|
||||||
|
di_bits = bits_set(io_byte & 0x0F)
|
||||||
|
do_bits = bits_set((io_byte >> 4) & 0x0F)
|
||||||
|
|
||||||
|
print("== 0x94: Status geral / IO ==")
|
||||||
|
print(f" Nº células em série : {n_series}")
|
||||||
|
print(f" Nº sensores de temp : {n_temps}")
|
||||||
|
print(f" Carregador conectado: {'sim' if charger else 'não'}")
|
||||||
|
print(f" Load conectado : {'sim' if load else 'não'}")
|
||||||
|
print(f" DI ativos (0-3) : {['DI'+str(i+1) for i in di_bits] or ['nenhum']}")
|
||||||
|
print(f" DO ativos (0-3) : {['DO'+str(i+1) for i in do_bits] or ['nenhum']}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x95(data: List[int]) -> None:
|
||||||
|
frame = data[0]
|
||||||
|
cells = []
|
||||||
|
for i in range(3):
|
||||||
|
hi = data[1 + 2*i]
|
||||||
|
lo = data[1 + 2*i + 1]
|
||||||
|
raw = u16_be(hi, lo)
|
||||||
|
if raw == 0 or raw == 0xFFFF:
|
||||||
|
continue
|
||||||
|
cell_index = frame * 3 + i + 1 # 1-based
|
||||||
|
cells.append((cell_index, raw))
|
||||||
|
|
||||||
|
print("== 0x95: Tensões individuais de célula (frame múltiplo) ==")
|
||||||
|
print(f" Frame: {frame}")
|
||||||
|
if not cells:
|
||||||
|
print(" (sem células válidas neste frame)")
|
||||||
|
else:
|
||||||
|
for idx, raw in cells:
|
||||||
|
print(f" Célula #{idx:2d}: {raw/1000.0:.3f} V (raw={raw} mV)")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x96(data: List[int]) -> None:
|
||||||
|
frame = data[0]
|
||||||
|
temps = []
|
||||||
|
for i in range(1, 8):
|
||||||
|
raw = data[i]
|
||||||
|
if raw == 0xFF:
|
||||||
|
continue
|
||||||
|
temp_c = raw - 40
|
||||||
|
sensor_index = frame * 7 + (i - 1) + 1
|
||||||
|
temps.append((sensor_index, temp_c))
|
||||||
|
|
||||||
|
print("== 0x96: Temperaturas individuais (frame múltiplo) ==")
|
||||||
|
print(f" Frame: {frame}")
|
||||||
|
if not temps:
|
||||||
|
print(" (sem temperaturas válidas neste frame)")
|
||||||
|
else:
|
||||||
|
for idx, t in temps:
|
||||||
|
print(f" Temp sensor #{idx:2d}: {t} °C")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x97(data: List[int]) -> None:
|
||||||
|
active_cells = []
|
||||||
|
for byte_index, b in enumerate(data):
|
||||||
|
for bit in range(8):
|
||||||
|
global_bit = byte_index * 8 + bit
|
||||||
|
if global_bit >= 48:
|
||||||
|
continue
|
||||||
|
if (b >> bit) & 1:
|
||||||
|
cell_idx = global_bit + 1
|
||||||
|
active_cells.append(cell_idx)
|
||||||
|
|
||||||
|
print("== 0x97: Estado de balanceamento das células ==")
|
||||||
|
if not active_cells:
|
||||||
|
print(" Nenhuma célula está em balanceamento (todas 0).")
|
||||||
|
else:
|
||||||
|
print(f" Células em balanceamento ({len(active_cells)}): {active_cells}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x98(data: List[int]) -> None:
|
||||||
|
alarms = []
|
||||||
|
|
||||||
|
b = data[0]
|
||||||
|
if b & (1 << 0): alarms.append("Cell volt high level 1")
|
||||||
|
if b & (1 << 1): alarms.append("Cell volt high level 2")
|
||||||
|
if b & (1 << 2): alarms.append("Cell volt low level 1")
|
||||||
|
if b & (1 << 3): alarms.append("Cell volt low level 2")
|
||||||
|
if b & (1 << 4): alarms.append("Sum volt high level 1")
|
||||||
|
if b & (1 << 5): alarms.append("Sum volt high level 2")
|
||||||
|
if b & (1 << 6): alarms.append("Sum volt low level 1")
|
||||||
|
if b & (1 << 7): alarms.append("Sum volt low level 2")
|
||||||
|
|
||||||
|
b = data[1]
|
||||||
|
if b & (1 << 0): alarms.append("Chg temp high level 1")
|
||||||
|
if b & (1 << 1): alarms.append("Chg temp high level 2")
|
||||||
|
if b & (1 << 2): alarms.append("Chg temp low level 1")
|
||||||
|
if b & (1 << 3): alarms.append("Chg temp low level 2")
|
||||||
|
if b & (1 << 4): alarms.append("Dischg temp high level 1")
|
||||||
|
if b & (1 << 5): alarms.append("Dischg temp high level 2")
|
||||||
|
if b & (1 << 6): alarms.append("Dischg temp low level 1")
|
||||||
|
if b & (1 << 7): alarms.append("Dischg temp low level 2")
|
||||||
|
|
||||||
|
b = data[2]
|
||||||
|
if b & (1 << 0): alarms.append("Chg overcurrent level 1")
|
||||||
|
if b & (1 << 1): alarms.append("Chg overcurrent level 2")
|
||||||
|
if b & (1 << 2): alarms.append("Dischg overcurrent level 1")
|
||||||
|
if b & (1 << 3): alarms.append("Dischg overcurrent level 2")
|
||||||
|
if b & (1 << 4): alarms.append("SOC high level 1")
|
||||||
|
if b & (1 << 5): alarms.append("SOC high level 2")
|
||||||
|
if b & (1 << 6): alarms.append("SOC low level 1")
|
||||||
|
if b & (1 << 7): alarms.append("SOC low level 2")
|
||||||
|
|
||||||
|
b = data[3]
|
||||||
|
if b & (1 << 0): alarms.append("Diff volt level 1")
|
||||||
|
if b & (1 << 1): alarms.append("Diff volt level 2")
|
||||||
|
if b & (1 << 2): alarms.append("Diff temp level 1")
|
||||||
|
if b & (1 << 3): alarms.append("Diff temp level 2")
|
||||||
|
|
||||||
|
b = data[4]
|
||||||
|
if b & (1 << 0): alarms.append("Chg MOS temp high alarm")
|
||||||
|
if b & (1 << 1): alarms.append("Dischg MOS temp high alarm")
|
||||||
|
if b & (1 << 2): alarms.append("Chg MOS temp sensor error")
|
||||||
|
if b & (1 << 3): alarms.append("Dischg MOS temp sensor error")
|
||||||
|
if b & (1 << 4): alarms.append("Chg MOS adhesion error")
|
||||||
|
if b & (1 << 5): alarms.append("Dischg MOS adhesion error")
|
||||||
|
if b & (1 << 6): alarms.append("Chg MOS open circuit error")
|
||||||
|
if b & (1 << 7): alarms.append("Dischg MOS open circuit error")
|
||||||
|
|
||||||
|
b = data[5]
|
||||||
|
if b & (1 << 0): alarms.append("AFE collect chip error")
|
||||||
|
if b & (1 << 1): alarms.append("Voltage collect dropped")
|
||||||
|
if b & (1 << 2): alarms.append("Cell temp sensor error")
|
||||||
|
if b & (1 << 3): alarms.append("EEPROM error")
|
||||||
|
if b & (1 << 4): alarms.append("RTC error")
|
||||||
|
if b & (1 << 5): alarms.append("Precharge failure")
|
||||||
|
if b & (1 << 6): alarms.append("Communication failure")
|
||||||
|
if b & (1 << 7): alarms.append("Internal communication failure")
|
||||||
|
|
||||||
|
b = data[6]
|
||||||
|
if b & (1 << 0): alarms.append("Current module fault")
|
||||||
|
if b & (1 << 1): alarms.append("Sum voltage detect fault")
|
||||||
|
if b & (1 << 2): alarms.append("Short circuit protect fault")
|
||||||
|
if b & (1 << 3): alarms.append("Low volt forbidden chg fault")
|
||||||
|
|
||||||
|
fault_code = data[7]
|
||||||
|
|
||||||
|
print("== 0x98: Status de falhas / alarmes ==")
|
||||||
|
if not alarms and fault_code == 0:
|
||||||
|
print(" Nenhum alarme ativo.")
|
||||||
|
else:
|
||||||
|
print(" Alarmes ativos:")
|
||||||
|
for a in alarms:
|
||||||
|
print(" - " + a)
|
||||||
|
print(f" Fault code bruto: 0x{fault_code:02X} ({fault_code})")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_daly_frame(can_id: int, data: List[int]) -> None:
|
||||||
|
data_id = get_data_id_from_can_id(can_id)
|
||||||
|
print(f"\n=== Frame recebido ===")
|
||||||
|
print(f" CAN ID : 0x{can_id:08X}")
|
||||||
|
print(f" DataID : 0x{data_id:02X}")
|
||||||
|
print(f" Data : {' '.join(f'{b:02X}' for b in data)}")
|
||||||
|
|
||||||
|
if data_id == 0x90:
|
||||||
|
parse_0x90(data)
|
||||||
|
elif data_id == 0x91:
|
||||||
|
parse_0x91(data)
|
||||||
|
elif data_id == 0x92:
|
||||||
|
parse_0x92(data)
|
||||||
|
elif data_id == 0x93:
|
||||||
|
parse_0x93(data)
|
||||||
|
elif data_id == 0x94:
|
||||||
|
parse_0x94(data)
|
||||||
|
elif data_id == 0x95:
|
||||||
|
parse_0x95(data)
|
||||||
|
elif data_id == 0x96:
|
||||||
|
parse_0x96(data)
|
||||||
|
elif data_id == 0x97:
|
||||||
|
parse_0x97(data)
|
||||||
|
elif data_id == 0x98:
|
||||||
|
parse_0x98(data)
|
||||||
|
else:
|
||||||
|
print(" DataID ainda não implementado neste parser.")
|
||||||
|
|
||||||
|
|
||||||
|
# ========= INICIALIZAÇÃO DO CAN =========
|
||||||
|
|
||||||
|
def init_can():
|
||||||
|
dll = ctypes.cdll.LoadLibrary(os.path.abspath(DLL_NAME))
|
||||||
|
|
||||||
|
ret = dll.VCI_OpenDevice(DEVICE_TYPE, DEVICE_INDEX, 0)
|
||||||
|
if ret != 1:
|
||||||
|
raise RuntimeError("Falha ao abrir dispositivo CAN")
|
||||||
|
|
||||||
|
baud = ctypes.c_uint(BAUD_250K)
|
||||||
|
ret = dll.VCI_SetReference(DEVICE_TYPE, DEVICE_INDEX, CHANNEL_INDEX, 0, ctypes.byref(baud))
|
||||||
|
if ret != 1:
|
||||||
|
raise RuntimeError("Falha ao configurar baudrate 250 kbps")
|
||||||
|
|
||||||
|
cfg = VCI_INIT_CONFIG()
|
||||||
|
cfg.AccCode = 0
|
||||||
|
cfg.AccMask = 0xFFFFFFFF
|
||||||
|
cfg.Reserved = 0
|
||||||
|
cfg.Filter = 0
|
||||||
|
cfg.Timing0 = 0
|
||||||
|
cfg.Timing1 = 0
|
||||||
|
cfg.Mode = 0
|
||||||
|
|
||||||
|
ret = dll.VCI_InitCAN(DEVICE_TYPE, DEVICE_INDEX, CHANNEL_INDEX, ctypes.byref(cfg))
|
||||||
|
if ret != 1:
|
||||||
|
raise RuntimeError("Falha ao inicializar CAN")
|
||||||
|
|
||||||
|
ret = dll.VCI_StartCAN(DEVICE_TYPE, DEVICE_INDEX, CHANNEL_INDEX)
|
||||||
|
if ret != 1:
|
||||||
|
raise RuntimeError("Falha ao iniciar CAN")
|
||||||
|
|
||||||
|
dll.VCI_ClearBuffer(DEVICE_TYPE, DEVICE_INDEX, CHANNEL_INDEX)
|
||||||
|
|
||||||
|
return dll
|
||||||
|
|
||||||
|
|
||||||
|
# ========= ENVIO + RECEPÇÃO PARA UM DATAID =========
|
||||||
|
|
||||||
|
def send_and_receive(dll, data_id: int, timeout_s: float = 0.5):
|
||||||
|
req_id = make_request_id(data_id)
|
||||||
|
resp_id_expect = expected_response_id(data_id)
|
||||||
|
|
||||||
|
send_obj = VCI_CAN_OBJ()
|
||||||
|
send_obj.ID = req_id
|
||||||
|
send_obj.SendType = 0
|
||||||
|
send_obj.RemoteFlag = 0
|
||||||
|
send_obj.ExternFlag = 1
|
||||||
|
send_obj.TimeFlag = 0
|
||||||
|
send_obj.DataLen = 8
|
||||||
|
for i in range(8):
|
||||||
|
send_obj.Data[i] = 0x00
|
||||||
|
|
||||||
|
ret = dll.VCI_Transmit(
|
||||||
|
DEVICE_TYPE,
|
||||||
|
DEVICE_INDEX,
|
||||||
|
CHANNEL_INDEX,
|
||||||
|
ctypes.byref(send_obj),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"\n📤 Enviando DataID=0x{data_id:02X} "
|
||||||
|
f"ID=0x{req_id:08X} DLC=8 Data=00 00 00 00 00 00 00 00 "
|
||||||
|
f"(ret={ret})"
|
||||||
|
)
|
||||||
|
|
||||||
|
recv_buffer = (VCI_CAN_OBJ * 50)()
|
||||||
|
start = time.time()
|
||||||
|
while time.time() - start < timeout_s:
|
||||||
|
count = dll.VCI_Receive(
|
||||||
|
DEVICE_TYPE,
|
||||||
|
DEVICE_INDEX,
|
||||||
|
CHANNEL_INDEX,
|
||||||
|
ctypes.byref(recv_buffer),
|
||||||
|
50,
|
||||||
|
50
|
||||||
|
)
|
||||||
|
if count > 0:
|
||||||
|
for i in range(count):
|
||||||
|
obj = recv_buffer[i]
|
||||||
|
if obj.DataLen == 0:
|
||||||
|
continue
|
||||||
|
if obj.ID != resp_id_expect:
|
||||||
|
# outros frames no barramento, se existirem
|
||||||
|
continue
|
||||||
|
data = [obj.Data[j] for j in range(obj.DataLen)]
|
||||||
|
parse_daly_frame(obj.ID, data)
|
||||||
|
return
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
print(f"⚠️ Sem resposta do BMS para DataID 0x{data_id:02X} em {timeout_s:.1f}s.")
|
||||||
|
|
||||||
|
|
||||||
|
# ========= MAIN INTERATIVO =========
|
||||||
|
|
||||||
|
def main():
|
||||||
|
dll = init_can()
|
||||||
|
print("✅ Dispositivo aberto, CAN iniciado em 250 kbps.\n")
|
||||||
|
print("Daly BMS console:")
|
||||||
|
print(" Digite o DataID (em hex) que deseja ler (90..98),")
|
||||||
|
print(" ou 'q' para sair.")
|
||||||
|
print("Exemplos: 90, 91, 92, 93, 94, 95, 96, 97, 98\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
s = input("DataID (hex) > ").strip()
|
||||||
|
if not s:
|
||||||
|
continue
|
||||||
|
if s.lower() in ("q", "quit", "exit"):
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
if s.lower().startswith("0x"):
|
||||||
|
s = s[2:]
|
||||||
|
data_id = int(s, 16)
|
||||||
|
except ValueError:
|
||||||
|
print("⚠️ Valor inválido. Digite algo como 90, 91, 98, ou 'q' pra sair.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not (0x90 <= data_id <= 0x98):
|
||||||
|
print("⚠️ Este console está preparado para 0x90 a 0x98.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
send_and_receive(dll, data_id, timeout_s=0.8)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n⏹ Interrompido pelo usuário.")
|
||||||
|
finally:
|
||||||
|
# se quiser, poderia chamar VCI_CloseDevice aqui
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import ctypes
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Carrega a DLL
|
||||||
|
dll = ctypes.cdll.LoadLibrary(os.path.abspath("ControlCANFD.dll"))
|
||||||
|
|
||||||
|
DEVICE_TYPE = 41
|
||||||
|
DEVICE_INDEX = 0
|
||||||
|
CHANNEL_INDEX = 0 # troca pra 1 se estiver usando CAN2
|
||||||
|
|
||||||
|
class VCI_INIT_CONFIG(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("AccCode", ctypes.c_uint),
|
||||||
|
("AccMask", ctypes.c_uint),
|
||||||
|
("Reserved", ctypes.c_uint),
|
||||||
|
("Filter", ctypes.c_ubyte),
|
||||||
|
("Timing0", ctypes.c_ubyte),
|
||||||
|
("Timing1", ctypes.c_ubyte),
|
||||||
|
("Mode", ctypes.c_ubyte),
|
||||||
|
]
|
||||||
|
|
||||||
|
class VCI_CAN_OBJ(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("ID", ctypes.c_uint),
|
||||||
|
("TimeStamp", ctypes.c_uint),
|
||||||
|
("TimeFlag", ctypes.c_ubyte),
|
||||||
|
("SendType", ctypes.c_ubyte),
|
||||||
|
("RemoteFlag", ctypes.c_ubyte),
|
||||||
|
("ExternFlag", ctypes.c_ubyte),
|
||||||
|
("DataLen", ctypes.c_ubyte),
|
||||||
|
("Data", ctypes.c_ubyte * 8),
|
||||||
|
("Reserved", ctypes.c_ubyte * 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
def u16_be(hi, lo):
|
||||||
|
return ((hi & 0xFF) << 8) | (lo & 0xFF)
|
||||||
|
|
||||||
|
def parse_0x90(data_bytes):
|
||||||
|
total_v_raw = u16_be(data_bytes[0], data_bytes[1])
|
||||||
|
meas_v_raw = u16_be(data_bytes[2], data_bytes[3])
|
||||||
|
current_raw = u16_be(data_bytes[4], data_bytes[5])
|
||||||
|
soc_raw = u16_be(data_bytes[6], data_bytes[7])
|
||||||
|
|
||||||
|
total_v = total_v_raw / 10.0
|
||||||
|
meas_v = meas_v_raw / 10.0
|
||||||
|
current = (current_raw - 30000) / 10.0
|
||||||
|
soc = soc_raw / 10.0
|
||||||
|
|
||||||
|
print(f"🔋 Tensão total : {total_v:.1f} V (raw={total_v_raw})")
|
||||||
|
print(f" Tensão medida: {meas_v:.1f} V (raw={meas_v_raw})")
|
||||||
|
print(f" Corrente : {current:.1f} A (raw={current_raw})")
|
||||||
|
print(f" SOC : {soc:.1f} % (raw={soc_raw})")
|
||||||
|
|
||||||
|
# -------- init device --------
|
||||||
|
ret = dll.VCI_OpenDevice(DEVICE_TYPE, DEVICE_INDEX, 0)
|
||||||
|
if ret != 1:
|
||||||
|
print("❌ Falha ao abrir dispositivo.")
|
||||||
|
raise SystemExit
|
||||||
|
print("✅ Dispositivo aberto.")
|
||||||
|
|
||||||
|
# baud 250 kbps
|
||||||
|
baud_250k = ctypes.c_uint(0x1C0008)
|
||||||
|
ret = dll.VCI_SetReference(DEVICE_TYPE, DEVICE_INDEX, CHANNEL_INDEX, 0, ctypes.byref(baud_250k))
|
||||||
|
if ret != 1:
|
||||||
|
print("❌ Falha ao configurar baudrate.")
|
||||||
|
raise SystemExit
|
||||||
|
print("🔧 Baudrate configurado para 250 kbps.")
|
||||||
|
|
||||||
|
config = VCI_INIT_CONFIG()
|
||||||
|
config.AccCode = 0
|
||||||
|
config.AccMask = 0xFFFFFFFF
|
||||||
|
config.Reserved = 0
|
||||||
|
config.Filter = 0
|
||||||
|
config.Timing0 = 0
|
||||||
|
config.Timing1 = 0
|
||||||
|
config.Mode = 0
|
||||||
|
|
||||||
|
ret = dll.VCI_InitCAN(DEVICE_TYPE, DEVICE_INDEX, CHANNEL_INDEX, ctypes.byref(config))
|
||||||
|
if ret != 1:
|
||||||
|
print("❌ Falha ao inicializar CAN.")
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
ret = dll.VCI_StartCAN(DEVICE_TYPE, DEVICE_INDEX, CHANNEL_INDEX)
|
||||||
|
if ret != 1:
|
||||||
|
print("❌ Falha ao iniciar CAN.")
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
dll.VCI_ClearBuffer(DEVICE_TYPE, DEVICE_INDEX, CHANNEL_INDEX)
|
||||||
|
print("🚀 Canal iniciado.\n")
|
||||||
|
|
||||||
|
recv_buffer = (VCI_CAN_OBJ * 100)()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# monta request 0x90
|
||||||
|
send_obj = VCI_CAN_OBJ()
|
||||||
|
send_obj.ID = 0x18900140 # PC -> BMS, DataID 0x90
|
||||||
|
send_obj.SendType = 0
|
||||||
|
send_obj.RemoteFlag = 0
|
||||||
|
send_obj.ExternFlag = 1 # extended
|
||||||
|
send_obj.DataLen = 8
|
||||||
|
for i in range(8):
|
||||||
|
send_obj.Data[i] = 0x00
|
||||||
|
|
||||||
|
dll.VCI_Transmit(DEVICE_TYPE, DEVICE_INDEX, CHANNEL_INDEX,
|
||||||
|
ctypes.byref(send_obj), 1)
|
||||||
|
|
||||||
|
# espera resposta rápida
|
||||||
|
start = time.time()
|
||||||
|
resp_ok = False
|
||||||
|
while time.time() - start < 0.5:
|
||||||
|
count = dll.VCI_Receive(
|
||||||
|
DEVICE_TYPE,
|
||||||
|
DEVICE_INDEX,
|
||||||
|
CHANNEL_INDEX,
|
||||||
|
ctypes.byref(recv_buffer),
|
||||||
|
100,
|
||||||
|
50
|
||||||
|
)
|
||||||
|
if count > 0:
|
||||||
|
for i in range(count):
|
||||||
|
obj = recv_buffer[i]
|
||||||
|
if obj.ID == 0x18904001 and obj.DataLen == 8:
|
||||||
|
data = [obj.Data[j] for j in range(8)]
|
||||||
|
print(f"\n📥 RX ID=0x{obj.ID:08X} Data=" +
|
||||||
|
" ".join(f"{b:02X}" for b in data))
|
||||||
|
parse_0x90(data)
|
||||||
|
resp_ok = True
|
||||||
|
break
|
||||||
|
if resp_ok:
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
if not resp_ok:
|
||||||
|
print("\n⚠️ Sem resposta do BMS para 0x90 nesse ciclo.")
|
||||||
|
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n🧹 Encerrando monitor...")
|
||||||
|
|
@ -0,0 +1,371 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Parser simples para frames CAN do Daly Smart BMS (protocolo V1.0).
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
python daly_can_parser.py
|
||||||
|
|
||||||
|
Depois ir colando linhas assim (exemplos):
|
||||||
|
|
||||||
|
0x18904001 01 94 00 00 75 30 03 B6
|
||||||
|
0x18914001 0F A0 01 0F 72 02
|
||||||
|
0x18924001 3C 01 32 02
|
||||||
|
0x18934001 00 01 02 05 00 01 86 A0
|
||||||
|
0x18944001 10 03 01 01 03 00 00 00
|
||||||
|
0x18984001 00 00 00 00 00 00 00 00
|
||||||
|
|
||||||
|
Enter vazio ou “q” sai.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
# ------------- helpers básicos ----------------
|
||||||
|
|
||||||
|
def parse_hex_token(tok: str) -> int:
|
||||||
|
tok = tok.strip()
|
||||||
|
if tok.lower().startswith("0x"):
|
||||||
|
tok = tok[2:]
|
||||||
|
return int(tok, 16)
|
||||||
|
|
||||||
|
|
||||||
|
def split_id_and_data(line: str) -> Tuple[int, List[int]]:
|
||||||
|
"""
|
||||||
|
Aceita formatos como:
|
||||||
|
0x18904001 01 94 00 00 75 30 03 B6
|
||||||
|
18904001 01 94 00 00 75 30 03 B6
|
||||||
|
Ignora lixo no final.
|
||||||
|
"""
|
||||||
|
parts = [p for p in line.replace(",", " ").split() if p]
|
||||||
|
|
||||||
|
if len(parts) < 9:
|
||||||
|
raise ValueError("Esperava ID + 8 bytes de dados (mínimo 9 tokens).")
|
||||||
|
|
||||||
|
can_id = parse_hex_token(parts[0])
|
||||||
|
data_bytes = [parse_hex_token(tok) & 0xFF for tok in parts[1:9]]
|
||||||
|
|
||||||
|
return can_id, data_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def get_data_id(can_id: int) -> int:
|
||||||
|
"""
|
||||||
|
No protocolo Daly:
|
||||||
|
ID = [Priority][DataID][AddrA][AddrB]
|
||||||
|
DataID = segundo byte (bits 16..23)
|
||||||
|
"""
|
||||||
|
return (can_id >> 16) & 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
def u16_be(b_hi: int, b_lo: int) -> int:
|
||||||
|
return ((b_hi & 0xFF) << 8) | (b_lo & 0xFF)
|
||||||
|
|
||||||
|
|
||||||
|
def bits_set(byte_val: int) -> List[int]:
|
||||||
|
"""Retorna lista com índices de bits = 1 (0..7)."""
|
||||||
|
return [i for i in range(8) if (byte_val >> i) & 1]
|
||||||
|
|
||||||
|
|
||||||
|
# ------------- parsers por DataID ----------------
|
||||||
|
|
||||||
|
def parse_0x90(data: List[int]) -> None:
|
||||||
|
"""SOC / total voltage / current."""
|
||||||
|
total_v_raw = u16_be(data[0], data[1])
|
||||||
|
meas_v_raw = u16_be(data[2], data[3])
|
||||||
|
current_raw = u16_be(data[4], data[5])
|
||||||
|
soc_raw = u16_be(data[6], data[7])
|
||||||
|
|
||||||
|
total_v = total_v_raw / 10.0
|
||||||
|
meas_v = meas_v_raw / 10.0
|
||||||
|
current = (current_raw - 30000) / 10.0
|
||||||
|
soc = soc_raw / 10.0
|
||||||
|
|
||||||
|
print("== 0x90: Tensão / Corrente / SOC ==")
|
||||||
|
print(f" Tensão total (cumulative): {total_v:.1f} V (raw={total_v_raw})")
|
||||||
|
print(f" Tensão medida (gather) : {meas_v:.1f} V (raw={meas_v_raw})")
|
||||||
|
print(f" Corrente : {current:.1f} A (raw={current_raw})")
|
||||||
|
print(f" SOC : {soc:.1f} % (raw={soc_raw})")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x91(data: List[int]) -> None:
|
||||||
|
"""Maximum & minimum cell voltage."""
|
||||||
|
vmax_raw = u16_be(data[0], data[1])
|
||||||
|
vmax_idx = data[2]
|
||||||
|
vmin_raw = u16_be(data[3], data[4])
|
||||||
|
vmin_idx = data[5]
|
||||||
|
|
||||||
|
print("== 0x91: Tensão máx/mín de célula ==")
|
||||||
|
print(f" Vmax: {vmax_raw/1000.0:.3f} V (raw={vmax_raw} mV) célula #{vmax_idx}")
|
||||||
|
print(f" Vmin: {vmin_raw/1000.0:.3f} V (raw={vmin_raw} mV) célula #{vmin_idx}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x92(data: List[int]) -> None:
|
||||||
|
"""Maximum & minimum temperature."""
|
||||||
|
tmax = data[0] - 40
|
||||||
|
tmax_i = data[1]
|
||||||
|
tmin = data[2] - 40
|
||||||
|
tmin_i = data[3]
|
||||||
|
|
||||||
|
print("== 0x92: Temperatura máx/mín ==")
|
||||||
|
print(f" T_max: {tmax} °C (sensor #{tmax_i})")
|
||||||
|
print(f" T_min: {tmin} °C (sensor #{tmin_i})")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x93(data: List[int]) -> None:
|
||||||
|
"""Charge / discharge MOS status + remaining capacity."""
|
||||||
|
state = data[0]
|
||||||
|
charge_mos = data[1]
|
||||||
|
dischg_mos = data[2]
|
||||||
|
life_cycles = data[3]
|
||||||
|
remain_mAh = (
|
||||||
|
(data[4] << 24)
|
||||||
|
| (data[5] << 16)
|
||||||
|
| (data[6] << 8)
|
||||||
|
| data[7]
|
||||||
|
)
|
||||||
|
|
||||||
|
state_map = {0: "parado", 1: "carga", 2: "descarga"}
|
||||||
|
|
||||||
|
print("== 0x93: Estado MOS / capacidade restante ==")
|
||||||
|
print(f" Estado geral : {state} ({state_map.get(state,'desconhecido')})")
|
||||||
|
print(f" Charge MOS state : {charge_mos}")
|
||||||
|
print(f" Discharge MOS state : {dischg_mos}")
|
||||||
|
print(f" Vida BMS (ciclos) : {life_cycles}")
|
||||||
|
print(f" Capacidade restante : {remain_mAh} mAh ({remain_mAh/1000.0:.3f} Ah)")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x94(data: List[int]) -> None:
|
||||||
|
"""Status info: nº células, temps, charger/load, DI/DO bits."""
|
||||||
|
n_series = data[0]
|
||||||
|
n_temps = data[1]
|
||||||
|
charger = data[2]
|
||||||
|
load = data[3]
|
||||||
|
io_byte = data[4]
|
||||||
|
|
||||||
|
di_bits = bits_set(io_byte & 0x0F) # bits 0..3
|
||||||
|
do_bits = bits_set((io_byte >> 4) & 0x0F) # bits 4..7
|
||||||
|
|
||||||
|
print("== 0x94: Status geral / IO ==")
|
||||||
|
print(f" Nº células em série : {n_series}")
|
||||||
|
print(f" Nº sensores de temp : {n_temps}")
|
||||||
|
print(f" Carregador conectado: {'sim' if charger else 'não'}")
|
||||||
|
print(f" Load conectado : {'sim' if load else 'não'}")
|
||||||
|
print(f" DI ativos (0-3) : {['DI'+str(i+1) for i in di_bits] or ['nenhum']}")
|
||||||
|
print(f" DO ativos (0-3) : {['DO'+str(i+1) for i in do_bits] or ['nenhum']}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x95(data: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Cell voltage frames (1-48).
|
||||||
|
Byte0 = frame number (0..15), Byte1..6 = 3 tensões de célula (mV), Byte7 reservado.
|
||||||
|
Cada célula = 2 bytes, 1 mV.
|
||||||
|
"""
|
||||||
|
frame = data[0]
|
||||||
|
cells = []
|
||||||
|
for i in range(3): # 3 células por frame
|
||||||
|
hi = data[1 + 2*i]
|
||||||
|
lo = data[1 + 2*i + 1]
|
||||||
|
raw = u16_be(hi, lo)
|
||||||
|
if raw == 0xFFFF or raw == 0:
|
||||||
|
continue
|
||||||
|
cell_index = frame * 3 + i + 1 # 1-based
|
||||||
|
cells.append((cell_index, raw))
|
||||||
|
|
||||||
|
print("== 0x95: Tensões individuais de célula (frame múltiplo) ==")
|
||||||
|
print(f" Frame: {frame}")
|
||||||
|
if not cells:
|
||||||
|
print(" (sem células válidas neste frame)")
|
||||||
|
else:
|
||||||
|
for idx, raw in cells:
|
||||||
|
print(f" Célula #{idx:2d}: {raw/1000.0:.3f} V (raw={raw} mV)")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x96(data: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Cell temperatures frames.
|
||||||
|
Byte0 = frame number (0..2), Byte1..7 = temps com offset 40.
|
||||||
|
"""
|
||||||
|
frame = data[0]
|
||||||
|
temps = []
|
||||||
|
for i in range(1, 8):
|
||||||
|
raw = data[i]
|
||||||
|
if raw == 0xFF: # vazio
|
||||||
|
continue
|
||||||
|
temp_c = raw - 40
|
||||||
|
sensor_index = frame * 7 + (i - 1) + 1 # 1-based
|
||||||
|
temps.append((sensor_index, temp_c))
|
||||||
|
|
||||||
|
print("== 0x96: Temperaturas individuais (frame múltiplo) ==")
|
||||||
|
print(f" Frame: {frame}")
|
||||||
|
if not temps:
|
||||||
|
print(" (sem temperaturas válidas neste frame)")
|
||||||
|
else:
|
||||||
|
for idx, t in temps:
|
||||||
|
print(f" Temp sensor #{idx:2d}: {t} °C")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x97(data: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Balance state 1-48.
|
||||||
|
0 = off, 1 = on.
|
||||||
|
Bits 0..47 mapeiam células 1..48. 48..63 reservados.
|
||||||
|
"""
|
||||||
|
active_cells = []
|
||||||
|
for byte_index, b in enumerate(data):
|
||||||
|
for bit in range(8):
|
||||||
|
global_bit = byte_index * 8 + bit
|
||||||
|
if global_bit >= 48:
|
||||||
|
continue
|
||||||
|
if (b >> bit) & 1:
|
||||||
|
cell_idx = global_bit + 1
|
||||||
|
active_cells.append(cell_idx)
|
||||||
|
|
||||||
|
print("== 0x97: Estado de balanceamento das células ==")
|
||||||
|
if not active_cells:
|
||||||
|
print(" Nenhuma célula está em balanceamento (todas 0).")
|
||||||
|
else:
|
||||||
|
print(f" Células em balanceamento ({len(active_cells)}): {active_cells}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_0x98(data: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Battery failure status: mapa de flags.
|
||||||
|
Imprime apenas os alarmes ativos.
|
||||||
|
"""
|
||||||
|
alarms = []
|
||||||
|
|
||||||
|
# Byte0
|
||||||
|
b = data[0]
|
||||||
|
if b & (1 << 0): alarms.append("Cell volt high level 1")
|
||||||
|
if b & (1 << 1): alarms.append("Cell volt high level 2")
|
||||||
|
if b & (1 << 2): alarms.append("Cell volt low level 1")
|
||||||
|
if b & (1 << 3): alarms.append("Cell volt low level 2")
|
||||||
|
if b & (1 << 4): alarms.append("Sum volt high level 1")
|
||||||
|
if b & (1 << 5): alarms.append("Sum volt high level 2")
|
||||||
|
if b & (1 << 6): alarms.append("Sum volt low level 1")
|
||||||
|
if b & (1 << 7): alarms.append("Sum volt low level 2")
|
||||||
|
|
||||||
|
# Byte1
|
||||||
|
b = data[1]
|
||||||
|
if b & (1 << 0): alarms.append("Chg temp high level 1")
|
||||||
|
if b & (1 << 1): alarms.append("Chg temp high level 2")
|
||||||
|
if b & (1 << 2): alarms.append("Chg temp low level 1")
|
||||||
|
if b & (1 << 3): alarms.append("Chg temp low level 2")
|
||||||
|
if b & (1 << 4): alarms.append("Dischg temp high level 1")
|
||||||
|
if b & (1 << 5): alarms.append("Dischg temp high level 2")
|
||||||
|
if b & (1 << 6): alarms.append("Dischg temp low level 1")
|
||||||
|
if b & (1 << 7): alarms.append("Dischg temp low level 2")
|
||||||
|
|
||||||
|
# Byte2
|
||||||
|
b = data[2]
|
||||||
|
if b & (1 << 0): alarms.append("Chg overcurrent level 1")
|
||||||
|
if b & (1 << 1): alarms.append("Chg overcurrent level 2")
|
||||||
|
if b & (1 << 2): alarms.append("Dischg overcurrent level 1")
|
||||||
|
if b & (1 << 3): alarms.append("Dischg overcurrent level 2")
|
||||||
|
if b & (1 << 4): alarms.append("SOC high level 1")
|
||||||
|
if b & (1 << 5): alarms.append("SOC high level 2")
|
||||||
|
if b & (1 << 6): alarms.append("SOC low level 1")
|
||||||
|
if b & (1 << 7): alarms.append("SOC low level 2")
|
||||||
|
|
||||||
|
# Byte3
|
||||||
|
b = data[3]
|
||||||
|
if b & (1 << 0): alarms.append("Diff volt level 1")
|
||||||
|
if b & (1 << 1): alarms.append("Diff volt level 2")
|
||||||
|
if b & (1 << 2): alarms.append("Diff temp level 1")
|
||||||
|
if b & (1 << 3): alarms.append("Diff temp level 2")
|
||||||
|
|
||||||
|
# Byte4
|
||||||
|
b = data[4]
|
||||||
|
if b & (1 << 0): alarms.append("Chg MOS temp high alarm")
|
||||||
|
if b & (1 << 1): alarms.append("Dischg MOS temp high alarm")
|
||||||
|
if b & (1 << 2): alarms.append("Chg MOS temp sensor error")
|
||||||
|
if b & (1 << 3): alarms.append("Dischg MOS temp sensor error")
|
||||||
|
if b & (1 << 4): alarms.append("Chg MOS adhesion error")
|
||||||
|
if b & (1 << 5): alarms.append("Dischg MOS adhesion error")
|
||||||
|
if b & (1 << 6): alarms.append("Chg MOS open circuit error")
|
||||||
|
if b & (1 << 7): alarms.append("Dischg MOS open circuit error")
|
||||||
|
|
||||||
|
# Byte5
|
||||||
|
b = data[5]
|
||||||
|
if b & (1 << 0): alarms.append("AFE collect chip error")
|
||||||
|
if b & (1 << 1): alarms.append("Voltage collect dropped")
|
||||||
|
if b & (1 << 2): alarms.append("Cell temp sensor error")
|
||||||
|
if b & (1 << 3): alarms.append("EEPROM error")
|
||||||
|
if b & (1 << 4): alarms.append("RTC error")
|
||||||
|
if b & (1 << 5): alarms.append("Precharge failure")
|
||||||
|
if b & (1 << 6): alarms.append("Communication failure")
|
||||||
|
if b & (1 << 7): alarms.append("Internal communication failure")
|
||||||
|
|
||||||
|
# Byte6
|
||||||
|
b = data[6]
|
||||||
|
if b & (1 << 0): alarms.append("Current module fault")
|
||||||
|
if b & (1 << 1): alarms.append("Sum voltage detect fault")
|
||||||
|
if b & (1 << 2): alarms.append("Short circuit protect fault")
|
||||||
|
if b & (1 << 3): alarms.append("Low volt forbidden chg fault")
|
||||||
|
|
||||||
|
fault_code = data[7]
|
||||||
|
|
||||||
|
print("== 0x98: Status de falhas / alarmes ==")
|
||||||
|
if not alarms and fault_code == 0:
|
||||||
|
print(" Nenhum alarme ativo.")
|
||||||
|
else:
|
||||||
|
print(" Alarmes ativos:")
|
||||||
|
for a in alarms:
|
||||||
|
print(" - " + a)
|
||||||
|
print(f" Fault code bruto: 0x{fault_code:02X} ({fault_code})")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------- roteador principal ----------------
|
||||||
|
|
||||||
|
def parse_daly_frame(can_id: int, data: List[int]) -> None:
|
||||||
|
data_id = get_data_id(can_id)
|
||||||
|
print(f"\n=== Frame recebido ===")
|
||||||
|
print(f" CAN ID : 0x{can_id:08X}")
|
||||||
|
print(f" DataID : 0x{data_id:02X}")
|
||||||
|
print(f" Data : {' '.join(f'{b:02X}' for b in data)}")
|
||||||
|
|
||||||
|
if data_id == 0x90:
|
||||||
|
parse_0x90(data)
|
||||||
|
elif data_id == 0x91:
|
||||||
|
parse_0x91(data)
|
||||||
|
elif data_id == 0x92:
|
||||||
|
parse_0x92(data)
|
||||||
|
elif data_id == 0x93:
|
||||||
|
parse_0x93(data)
|
||||||
|
elif data_id == 0x94:
|
||||||
|
parse_0x94(data)
|
||||||
|
elif data_id == 0x95:
|
||||||
|
parse_0x95(data)
|
||||||
|
elif data_id == 0x96:
|
||||||
|
parse_0x96(data)
|
||||||
|
elif data_id == 0x97:
|
||||||
|
parse_0x97(data)
|
||||||
|
elif data_id == 0x98:
|
||||||
|
parse_0x98(data)
|
||||||
|
else:
|
||||||
|
print(" DataID ainda não implementado neste parser.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Daly BMS CAN parser (teste de bancada)")
|
||||||
|
print("Cole uma linha com ID + 8 bytes (ex.: 0x18904001 01 94 00 00 75 30 03 B6)")
|
||||||
|
print("Enter vazio ou 'q' para sair.\n")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = input("> ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nSaindo.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not line or line.lower() in ("q", "quit", "exit"):
|
||||||
|
print("Saindo.")
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
can_id, data = split_id_and_data(line)
|
||||||
|
parse_daly_frame(can_id, data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao processar linha: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue