Implementado bateria Daly, integrado IA segformer ao visual worker, treinamento raw

This commit is contained in:
Diego Freitas 2026-02-05 16:39:59 -03:00
parent c805f6103b
commit 5e9155c3ed
32 changed files with 2822 additions and 465 deletions

View File

@ -519,6 +519,7 @@
<Compile Include="Models\Components\CustomDialog.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Models\DalyBMSModel.cs" />
<Compile Include="Models\DispositivosModel.cs" />
<Compile Include="Models\GPSModel.cs" />
<Compile Include="Models\GraficoInterativoModel.cs" />
@ -576,6 +577,7 @@
<Compile Include="Services\CANServiceWaveshare.cs" />
<Compile Include="Services\CoolerControlService.cs" />
<Compile Include="Services\CoulombCounterService.cs" />
<Compile Include="Services\DalyBMSService.cs" />
<Compile Include="Services\DispositivosService.cs" />
<Compile Include="Forms\frmPrincipal.cs">
<SubType>Form</SubType>

View File

@ -43,6 +43,7 @@ namespace AgroBase.Forms
System.Windows.Forms.DataVisualization.Charting.Series series4 = new System.Windows.Forms.DataVisualization.Charting.Series();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frmAcompanhamento));
this.pnlFundo = new System.Windows.Forms.Panel();
this.lblSen_Latencia = new System.Windows.Forms.Label();
this.lblB19V = new System.Windows.Forms.Label();
this.lblB24V = 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.lblM_EF = new System.Windows.Forms.Label();
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();
((System.ComponentModel.ISupportInitialize)(this.chartET)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.chartDT)).BeginInit();
@ -143,6 +144,7 @@ namespace AgroBase.Forms
//
this.pnlFundo.AutoScroll = true;
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.lblB19V);
this.pnlFundo.Controls.Add(this.lblB24V);
@ -234,6 +236,17 @@ namespace AgroBase.Forms
this.pnlFundo.Size = new System.Drawing.Size(1473, 811);
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
//
this.lblB19V.AutoSize = true;
@ -1108,16 +1121,16 @@ namespace AgroBase.Forms
this.picEF.TabIndex = 90;
this.picEF.TabStop = false;
//
// lblSen_Latencia
// lblVBat
//
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";
this.lblVBat.AutoSize = true;
this.lblVBat.Font = new System.Drawing.Font("Microsoft Sans Serif", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.lblVBat.Location = new System.Drawing.Point(656, 739);
this.lblVBat.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
this.lblVBat.Name = "lblVBat";
this.lblVBat.Size = new System.Drawing.Size(149, 12);
this.lblVBat.TabIndex = 194;
this.lblVBat.Text = "Bateria: 0,00V 0,00A 0,00% 0,00ºC";
//
// frmAcompanhamento
//
@ -1234,5 +1247,6 @@ namespace AgroBase.Forms
public System.Windows.Forms.Label lblB24V;
public System.Windows.Forms.Label lblB19V;
public System.Windows.Forms.Label lblSen_Latencia;
public System.Windows.Forms.Label lblVBat;
}
}

View File

@ -83,6 +83,12 @@ namespace AgroBase.Forms
lblSen_Latencia.Text = $"Latencia de Loop: {Variaveis.OperacaoEmAndamento.DispSen.Dados?.DadosLeitura?.LatenciaLoop ?? 0} ms";
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)

View File

@ -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 0255 (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;
}
}

View File

@ -2307,6 +2307,7 @@ namespace AgroBase.Models
GPSService.AtualizaDadosRedis();
LivoxManagerProcess.AtualizarLeitura();
MovimentacaoMovimentoCompensadoModel.AtualizarDados();
DalyBMSService.AtualizarDados();
var _Trajetoria = Variaveis.OperacaoEmAndamento.Trajetoria;
var _DispMvd = Variaveis.OperacaoEmAndamento.DispMvd;

View File

@ -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 DateTime Momento { get; set; }
public T_Code Dispositivo { get; set; }
public byte IdTx { get; set; }
public byte IdRx { get; set; }
public uint IdTx { get; set; }
public uint IdRx { get; set; }
public byte funcCodeTx { get; set; }
public byte funcCodeRx { get; set; }
public byte[] DataTx { get; set; }
@ -44,6 +126,7 @@ namespace AgroBase.Services
public DateTime RespondidoEm { get; set; }
public string ChaveInterna { get; set; }
public bool IsExtended => IdRx > 0x7FF || IdTx > 0x7FF;
}
public class CanCommandFreqModel<T>
@ -156,9 +239,9 @@ namespace AgroBase.Services
public interface ICanService : IDisposable
{
bool PortaIsCan(SerialPort Porta = null);
void RegistrarHandler(byte endereco_rx, ICanMessageHandler handler);
void RegistrarHandler(uint endereco_rx, ICanMessageHandler handler);
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();
string _portName { get; set; }
bool Iniciado { get; }

View File

@ -28,9 +28,9 @@ namespace AgroBase.Services
private AsyncTaskTimerModel tmrProcessarCAN;
private ConcurrentQueue<string> FramesRecebidos = new ConcurrentQueue<string>();
private StringBuilder SerialCANBuffer = new StringBuilder();
private readonly ConcurrentDictionary<byte, ICanMessageHandler> _roteadores = new ConcurrentDictionary<byte, ICanMessageHandler>();
private readonly ConcurrentDictionary<byte, bool> _roteadoresRegistrados = new ConcurrentDictionary<byte, bool>();
private byte? _ultimoIdEnviado = null;
private readonly ConcurrentDictionary<uint, ICanMessageHandler> _roteadores = new ConcurrentDictionary<uint, ICanMessageHandler>();
private readonly ConcurrentDictionary<uint, bool> _roteadoresRegistrados = new ConcurrentDictionary<uint, bool>();
private uint? _ultimoIdEnviado = null;
private List<CanMessage> Mensagens = new List<CanMessage>();
private readonly object _LockMensagens = new object();
@ -61,9 +61,9 @@ namespace AgroBase.Services
private DateTime _ultimoErroCritico = DateTime.MinValue;
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}");
}
@ -170,7 +170,7 @@ namespace AgroBase.Services
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);
if (!registrado)
@ -311,109 +311,142 @@ namespace AgroBase.Services
private void ProcessarFrameCAN(string frame)
{
// Ex: t00183301020304
if (!frame.StartsWith("t")) return;
// Ex:
// 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
{
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);
int len = int.Parse(frame.Substring(4, 1));
// precisa ter pelo menos: prefixo + ID + DLC
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];
if (data == null || data.Length == 0) return;
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;
byte fCodeRx = data.Length > 0 ? data[0] : (byte)0x00;
byte parametroRx = data.Length > 1 ? data[1] : (byte)0x00;
string chave = GerarChaveMensagem(dispositivo, (byte)id, data);
bool db = dispositivo == T_Code.Bat;
// 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;
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)
{
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.Respondido = true;
mensagem.RespondidoEm = DateTime.Now;
if ((handler?.Dispositivo ?? T_Code.Vzo) != T_Code.Vzo)
if (dispositivo != T_Code.Vzo)
{
handler.ProcessarMensagem(mensagem);
}
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)
{
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;
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)
dados.AddRange(payload);
if (Dispositivo == T_Code.Mks)
{
byte crc = CalcularChecksum(idTx, dados.ToArray());
byte crc = CanManager.CalcularChecksum(idTx, dados.ToArray());
dados.Add(crc);
}
@ -427,7 +460,7 @@ namespace AgroBase.Services
funcCodeRx = funcCodeRx,
DataTx = dados.ToArray(),
ComResposta = get,
ChaveInterna = GerarChaveMensagem(Dispositivo, idRx, dados.ToArray())
ChaveInterna = CanManager.GerarChaveMensagem(Dispositivo, idRx, dados.ToArray())
};
bool mensagemNaFila = false;
@ -482,7 +515,7 @@ namespace AgroBase.Services
.OrderBy(x => x)
.ToList();
byte proximoId;
uint proximoId;
if (!_ultimoIdEnviado.HasValue || !idsDisponiveis.Contains(_ultimoIdEnviado.Value))
{
@ -541,13 +574,34 @@ namespace AgroBase.Services
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)
frame += b.ToString("X2");
sb.Append(b.ToString("X2"));
}
frame += "\r";
sb.Append("\r");
return frame;
return sb.ToString();
}
private bool EnviarComando(string comando)
@ -659,26 +713,7 @@ namespace AgroBase.Services
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)
private const double TARGET_FPS = 200;

View File

@ -29,9 +29,9 @@ namespace AgroBase.Services
private AsyncTaskTimerModel tmrProcessarCAN;
private StringBuilder SerialCANBuffer = new StringBuilder();
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<byte, bool> _roteadoresRegistrados = new ConcurrentDictionary<byte, bool>();
private byte? _ultimoIdEnviado = null;
private readonly ConcurrentDictionary<uint, ICanMessageHandler> _roteadores = new ConcurrentDictionary<uint, ICanMessageHandler>();
private readonly ConcurrentDictionary<uint, bool> _roteadoresRegistrados = new ConcurrentDictionary<uint, bool>();
private uint? _ultimoIdEnviado = null;
private List<CanMessage> MensagensPendentes = new List<CanMessage>();
private readonly object _LockMensagens = new object();
private bool DebugMode = false;
@ -60,9 +60,9 @@ namespace AgroBase.Services
public double UltimoErroCritico { get; set; } = 0;
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}");
}
@ -73,7 +73,7 @@ namespace AgroBase.Services
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);
if (!registrado)
@ -186,28 +186,52 @@ namespace AgroBase.Services
uint id = frame.Item1;
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
{
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;
byte fCodeRx = data[0];
byte parametroRx = data.Length > 1 ? data[1] : (byte)0x00;
string chave = GerarChaveMensagem(dispositivo, (byte)id, data);
bool db = dispositivo == T_Code.Bat;
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;
string chave = CanManager.GerarChaveMensagem(dispositivo, id, data);
CanMessage mensagem = null;
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)
{
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.Respondido = true;
mensagem.RespondidoEm = DateTime.Now;
@ -218,27 +242,33 @@ namespace AgroBase.Services
}
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)
{
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;
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)
dados.AddRange(payload);
if (Dispositivo == T_Code.Mks)
{
byte crc = CalcularChecksum(idTx, dados.ToArray());
byte crc = CanManager.CalcularChecksum(idTx, dados.ToArray());
dados.Add(crc);
}
@ -252,7 +282,7 @@ namespace AgroBase.Services
funcCodeRx = funcCodeRx,
DataTx = dados.ToArray(),
ComResposta = get,
ChaveInterna = GerarChaveMensagem(Dispositivo, idRx, dados.ToArray())
ChaveInterna = CanManager.GerarChaveMensagem(Dispositivo, idRx, dados.ToArray())
};
bool mensagemNaFila = false;
@ -307,7 +337,7 @@ namespace AgroBase.Services
.OrderBy(x => x)
.ToList();
byte proximoId;
uint proximoId;
if (!_ultimoIdEnviado.HasValue || !idsDisponiveis.Contains(_ultimoIdEnviado.Value))
{
@ -328,7 +358,7 @@ namespace AgroBase.Services
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 bool PodeEnviarAgora(CanMessage m)
@ -382,20 +412,42 @@ namespace AgroBase.Services
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)
frame += b.ToString("X2");
sb.Append(b.ToString("X2"));
}
frame += "\r";
return frame;
sb.Append('\r');
return sb.ToString();
}
private bool EnviarComando(string comando, CanMessage msg = null)
{
if (Connector.IsOpen)
{
bool sucesso = Connector.Send(msg.IdTx, msg.DataTx);
bool sucesso = Connector.Send(msg.IdTx, msg.DataTx, isExtended: msg.IsExtended);
if (sucesso)
{
@ -485,27 +537,6 @@ namespace AgroBase.Services
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)

View File

@ -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();
}
}
}
}

View File

@ -74,7 +74,7 @@ namespace AgroBase.Comum
{
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)

View File

@ -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)
{
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)
@ -283,10 +283,12 @@ namespace AgroBase.Services
}
}
private MKS057DModel ObterOuCriar(byte idTx, byte idRx)
private MKS057DModel ObterOuCriar(uint _idTx, uint _idRx)
{
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);
if (item == null)
{

View File

@ -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)
{
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)
@ -280,10 +280,12 @@ namespace AgroBase.Services
SinalZNaoEncontrado = 19
}
private OIDModel ObterOuCriar(byte idTx, byte idRx)
private OIDModel ObterOuCriar(uint _idTx, uint _idRx)
{
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);
if (item == null)
{

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using static AgroBase.Models.Enums;
using static AgroBase.Models.Operadores.OperadoresModels;
namespace AgroBase.Services.Operadores
@ -744,6 +745,43 @@ namespace AgroBase.Services.Operadores
("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;
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Trj),
("timestamp", agora),
@ -759,6 +797,5 @@ namespace AgroBase.Services.Operadores
}
}

View File

@ -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;
}

View File

@ -156,22 +156,41 @@ public class WaveshareCANFDService
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
{
ID = id,
SendType = 0,
RemoteFlag = (byte)(isRemote ? 1 : 0),
ExternFlag = (byte)(isExtended ? 1 : 0),
SendType = 0, // normal
RemoteFlag = (byte)(isRemote ? 1 : 0), // 0 = data frame, 1 = remote frame
ExternFlag = (byte)(isExtended ? 1 : 0), // 0 = standard, 1 = extended
DataLen = (byte)data.Length,
Data = new byte[8],
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);
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()

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

View File

@ -21,19 +21,19 @@
"id": 3,
"Arquivo": "modelseg",
"Diretorio": "C:\\AgroBaseModels\\Ruas\\",
"Extensao": ".blob",
"Versao": "1_2",
"Extensao": ".pt",
"Versao": "1_3",
"TipoArquivo": 0,
"ArquivoDownload": "models/street_detector_model_seg-1_2.blob",
"ArquivoDownload": "models/street_detector_model_seg-1_3.pt",
},
{
"id": 4,
"Arquivo": "modelseg",
"Diretorio": "C:\\AgroBaseModels\\Ruas\\",
"Extensao": ".txt",
"Versao": "1_2",
"Versao": "1_3",
"TipoArquivo": 0,
"ArquivoDownload": "models/street_detector_labelmap_seg-1_2.txt"
"ArquivoDownload": "models/street_detector_labelmap_seg-1_3.txt"
},
{
"id": 5,

View File

@ -19,6 +19,7 @@ def main():
from health_worker.modulos.ponte_ip import ModuloIPBribge
from health_worker.modulos.can import ModuloCAN
from health_worker.modulos.trajetoria import ModuloTrajetorira
from health_worker.modulos.bateria import ModuloBateria
from health_worker.config import mostrar_log
from shared.contexto_global_redis import ContextoGlobalRedis, CmdKey, CtxKey
@ -35,6 +36,7 @@ def main():
T_Code.Ipb: ModuloIPBribge(),
T_Code.Can: ModuloCAN(),
T_Code.Trj: ModuloTrajetorira(),
T_Code.Bat: ModuloBateria(),
}
def loop_ativo():

View File

@ -514,7 +514,7 @@ class CameraManager:
t1 = time.time()
grid_conf["ultima_chamada"] = self._ultima_analise_matriz_confianca.get("ultima_chamada", t0)
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
ContextoGlobalRedis.atualizar_ctx_dict(

View File

@ -1,6 +1,7 @@
import math
import time
import numpy as np
from shared.enums import StatusCarroMapa
class CostmapFuser:
@ -124,7 +125,9 @@ class CostmapFuser:
det_score_f=None, # (H,W) 0..1
det_label_id=None, # (H,W) int, -1 = none
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:
H, W = custo_f.shape
@ -402,7 +405,7 @@ class CostmapFuser:
"N_on": N_on,
"N_off": N_off
}
blocked_out = blocked_raw
blocked_out = status_seg == StatusCarroMapa.Parado or blocked_raw
if use_persistence:
v = float(max(0.0, velocidade_mps or 0.0))
@ -444,13 +447,14 @@ class CostmapFuser:
elif st["latched"] and (st["off"] >= N_off):
st["latched"] = False
blocked_out = status_seg == StatusCarroMapa.Parado or bool(st["latched"])
decision.update({
"parar": False, # bool(st["latched"]),
"parar": blocked_out,
"v_max_sugerida_mps": v_max_sug,
"frames_on": int(st["on"]),
"frames_off": int(st["off"])
})
blocked_out = bool(st["latched"])
st["reason"] = reason
# guarda também as duas distâncias pra debug
@ -494,7 +498,7 @@ class CostmapFuser:
except Exception as e:
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"
opcional: "z_ref" (m) 2D
@ -598,7 +602,8 @@ class CostmapFuser:
det_score_f = det_score,
det_label_id = det_lbl,
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

View File

@ -1,41 +1,30 @@
#!/usr/bin/env python3
# -*- 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):
MODELO/dataset/original/group/<grupo>/previews
MODELO/dataset/original/group/<grupo>/raws
MODELO/dataset/original/group/<grupo>/masks
Entrada:
MODELO/dataset/original/group/<grupo>/raws (.raw, 4 canais float32)
MODELO/dataset/original/group/<grupo>/masks (.png/.jpg)
(opcional) MODELO/dataset/original/group/<grupo>/masks2
Saída:
MODELO/dataset/augmented/group/<grupo>/previews
MODELO/dataset/augmented/group/<grupo>/raws
Saída:
MODELO/dataset/augmented/group/<grupo>/previews (RGB gerado do RAW blurado)
MODELO/dataset/augmented/group/<grupo>/raws (RAW4 augmentado: geom + blur)
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, images/masks):
MODELO/dataset/original/group/<grupo>/images
MODELO/dataset/original/group/<grupo>/masks
Saída:
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.) em preview RGB.
Regras:
- GEOMETRIA (flip, shift, rotate) aplicada em RAW + masks (+ masks2).
- BLUR / MOTION BLUR aplicado no RAW, canal por canal, com o mesmo kernel.
- Preview é SEMPRE derivado do RAW transformado (geom + blur).
- Máscaras não sofrem blur, apenas geom.
Uso:
python _5_augmentation.py --copies 5
python _5_augmentation.py --copies 5 --groups chao,chao_erva,cana
python _5_augmentation_raw.py --copies 5
python _5_augmentation_raw.py --copies 5 --groups chao,chao_erva,cana
"""
import os
import json
@ -50,6 +39,8 @@ import numpy as np
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
MODELO = config.get("camera", ".")
RESOLUCAO = config.get("raw_size")
USE_MASKS2 = config.get("dual_head", False)
# Pastas base
@ -57,7 +48,7 @@ DATASET_BASE = os.path.join(MODELO, "dataset")
ORIG_GROUP_ROOT = os.path.join(DATASET_BASE, "original", "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_MSK = os.path.join(DATASET_BASE, "original", "masks")
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
IMG_EXTS = (".jpg", ".jpeg", ".png")
MSK_EXTS = (".png", ".jpg", ".jpeg") # prioriza PNG
MSK_EXTS = (".png", ".jpg", ".jpeg")
MSK2_EXTS = (".png", ".jpg", ".jpeg")
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):
os.makedirs(p, exist_ok=True)
@ -86,7 +71,8 @@ def garantir_dir(p):
# 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([
A.HorizontalFlip(p=0.5),
A.ShiftScaleRotate(
@ -99,32 +85,144 @@ train_geo = ReplayCompose([
),
], additional_targets={
'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),
A.GaussianBlur(blur_limit=3, p=1.0),
], p=0.20),
# ===============================
# Funções de blur físico no RAW
# ===============================
A.OneOf([
A.GaussNoise(var_limit=(5.0, 15.0), p=1.0),
A.ImageCompression(quality_lower=50, quality_upper=85, p=1.0),
], p=0.20),
def motion_blur_kernel(ksize=5, angle=0.0):
"""
Gera um kernel de motion blur 2D com tamanho ksize e ângulo em graus.
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),
A.RandomSunFlare(p=0.10),
A.ChannelShuffle(p=0.05),
A.CoarseDropout(max_holes=6, max_height=16, max_width=16, p=0.10),
])
kernel = np.zeros((ksize, ksize), dtype=np.float32)
# linha horizontal no centro
kernel[ksize // 2, :] = 1.0
# 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):
@ -138,40 +236,101 @@ def save_rgb(path, arr_rgb):
Image.fromarray(arr_rgb).save(path)
def load_raw_grayscale(path, like_shape=None):
"""
Carrega RAW como (H, W) em uint8 ou uint16 detectando pelo tamanho do arquivo.
"""
if like_shape is None:
raise RuntimeError("like_shape é obrigatório para RAW (preciso do HxW).")
h, w = like_shape[:2]
npx = h * w
fsize = os.path.getsize(path)
if fsize == npx: # 1 byte por pixel
dtype = np.uint8
elif fsize == npx * 2: # 2 bytes por pixel
dtype = np.uint16
def _load_raw(path: str, raw_hw) -> np.ndarray:
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":
if raw_hw is None:
raise RuntimeError(
f"Para ler .raw preciso de raw_hw=(H,W). "
f"Passe raw_hw ao criar o dataset."
)
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:
raise RuntimeError(
f"Tamanho inesperado para RAW {path}: {fsize} bytes "
f"(esperado {npx} (u8) ou {npx*2} (u16) para {w}x{h})."
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})")
# 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)
if data.size != npx:
raise RuntimeError(f"RAW {path}: size={data.size} != {npx} (HxW)")
return data.reshape((h, w))
def _scale_to_float01(raw: np.ndarray, max_value = None) -> np.ndarray:
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:
# 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):
np.asarray(arr).tofile(path)
def save_raw_any(path, arr):
arr = np.asarray(arr)
arr.tofile(path)
# ===============================
# Helpers de diretório / mapeamento
# ===============================
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):
return []
grupos = []
@ -180,8 +339,8 @@ def list_groups(root):
if not os.path.isdir(gdir):
continue
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"))
if has_masks and has_imgs:
has_raws = os.path.isdir(os.path.join(gdir, "raws"))
if has_masks and has_raws:
grupos.append(name)
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
# ===============================
# Núcleo: augmentação de um sample
# ===============================
def augment_sample(
img_path,
raw_path,
msk_path,
img_out_dir,
msk_out_dir,
copies,
msk2_path=None,
msk2_out_dir=None,
raw_path=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))
msk2_ext = os.path.splitext(os.path.basename(msk2_path))[1] if msk2_path else None
raw_ext = os.path.splitext(os.path.basename(raw_path))[1] if raw_path else None
if base_msk != base:
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)
msk2 = load_rgb(msk2_path) if msk2_path else None
if raw_path is not None and raw_out_dir is not None:
# usa shape do preview pra validar RAW
raw = load_raw_grayscale(raw_path, like_shape=img.shape)
else:
raw = None
# 2) RAW4
raw = _load_raw(raw_path, raw_hw=(RESOLUCAO[1], RESOLUCAO[0])) # (H,W,4) uint8
raw = _scale_to_float01(raw) # float32 0..1
# 3) Preview inicial derivado do RAW (antes de geom/blur, só referência)
img = raw_to_preview_rgb(raw)
gen = 0
for i in range(copies):
# 1) GEOM: mesma transformação em img, mask, mask2 e raw
if raw is not None:
# 4) GEOMETRIA: mesma transformação em img, mask, mask2 e raw
if msk2 is not None and msk2_out_dir:
aug_geo = train_geo(image=img, mask=msk, mask2=msk2, raw=raw)
else:
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"]
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
# 2) FOTO: só preview RGB
img_p = train_photo(image=img_g)["image"]
# 5) BLUR FÍSICO: aplica (ou não) o MESMO blur no RAW geométrico
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}")
save_rgb(out_img, img_p)
save_rgb(out_img, img_b)
save_rgb(out_msk, msk_g)
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}")
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}")
save_raw_grayscale(out_raw, raw_g)
save_raw_any(out_raw, raw_b)
gen += 1
return gen
# ===============================
# Processamento por grupo
# ===============================
def process_group(group_name, copies):
"""
Processa um grupo único.
Suporta:
- group/<g>/previews + raws + masks (+ masks2)
- group/<g>/images + masks (+ masks2) [modo antigo]
Processa um grupo único usando APENAS:
- group/<g>/raws
- group/<g>/masks (+ masks2 opcional)
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)
# 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")
msk_dir = os.path.join(gdir, "masks")
msk2_dir = os.path.join(gdir, "masks2")
use_preview_mode = os.path.isdir(img_dir_previews)
img_dir = img_dir_previews if use_preview_mode else img_dir_images
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.")
if not (os.path.isdir(raw_dir) and os.path.isdir(msk_dir)):
print(f"[WARN] Grupo '{group_name}' inválido (precisa de 'raws' e 'masks'). Pulando.")
return 0
use_raw = os.path.isdir(raw_dir)
use_raw = True
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)
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 {}
img_out_dir, msk_out_dir, msk2_out_dir, raw_out_dir = ensure_aug_dirs(
group_name,
use_masks2=use_masks2,
use_raw=use_raw,
is_preview_mode=use_preview_mode
is_preview_mode=True
)
count = 0
for img_file in sorted(imgs):
base, _ = os.path.splitext(img_file)
for base, raw_file in sorted(raw_map.items()):
msk_file = msk_map.get(base)
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
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
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:
count += augment_sample(
os.path.join(img_dir, img_file),
msk_file,
img_out_dir,
msk_out_dir,
raw_path=raw_file,
msk_path=msk_file,
img_out_dir=img_out_dir,
msk_out_dir=msk_out_dir,
copies=copies,
msk2_path=msk2_file,
msk2_out_dir=msk2_out_dir,
raw_path=raw_file,
raw_out_dir=raw_out_dir,
)
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.")
return count
# ===============================
# Fallback legacy (sem RAW)
# ===============================
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)):
print("[WARN] Modo legacy não encontrado. Nada a fazer.")
return 0
@ -412,7 +590,7 @@ def process_legacy(copies):
count = 0
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)
if not msk_file:
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.")
try:
count += augment_sample(
os.path.join(ORIG_OLD_IMG, img_file),
msk_file,
img_out_dir,
msk_out_dir,
copies=copies,
msk2_path=msk2_file,
msk2_out_dir=msk2_out_dir,
raw_path=None,
raw_out_dir=None,
)
img_path = os.path.join(ORIG_OLD_IMG, img_file)
img = load_rgb(img_path)
base_msk, msk_ext = os.path.splitext(os.path.basename(msk_file))
msk = load_rgb(msk_file)
msk2 = load_rgb(msk2_file) if msk2_file else None
for i in range(copies):
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"]
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:
print(f"[ERRO] (legacy) {img_file}: {e}")
print(f"[OK] Legacy → {count} amostras geradas.")
return count
# ===============================
# main
# ===============================
def main(copies=5, groups_csv=None):
total = 0
if os.path.isdir(ORIG_GROUP_ROOT):
@ -462,7 +661,7 @@ def main(copies=5, groups_csv=None):
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("--groups", type=str, default=None, help="Lista de grupos separados por vírgula (ex: chao,erva_cana).")
args = ap.parse_args()

View File

@ -35,8 +35,11 @@ with open("config.json", "r", encoding="utf-8") as f:
MODELO = config["camera"]
USE_MASKS2 = config["dual_head"]
RESOLUCAO = tuple(config["resolucao"]) # [W,H]
MODEL_NAME = config["model_name"]
CHANNELS = int(config.get("channels", 4))
pasta_base = os.path.join(MODELO, "dataset")
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])}
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)
return by_base
def load_raw_gray_autodtype(path: str, src_hw: Tuple[int,int]) -> np.ndarray:
"""
Carrega RAW como (H,W) detectando uint8/uint16 pelo tamanho do arquivo.
"""
def load_raw4_float(path: str, src_hw: Tuple[int,int]) -> np.ndarray:
h, w = src_hw
npx = h * w
fsize = os.path.getsize(path)
if fsize == npx:
dtype = np.uint8
elif fsize == npx * 2:
dtype = np.uint16
else:
num_floats = fsize // 4
if num_floats != 4 * npx:
raise RuntimeError(
f"Tamanho inesperado para RAW {path}: {fsize} bytes "
f"(esperado {npx} (u8) ou {npx*2} (u16) para {w}x{h})."
f"RAW {path}: esperado 4 canais float32, "
f"mas num_floats={num_floats}, H*W={npx}"
)
data = np.fromfile(path, dtype=dtype)
if data.size != npx:
raise RuntimeError(f"RAW {path}: size={data.size} != {npx} (HxW)")
return data.reshape((h, w))
data = np.fromfile(path, dtype=np.float32)
return data.reshape(4, h, w) # (C,H,W)
def save_raw(path: str, arr: np.ndarray):
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 raw_path:
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 = mosaic_to_raw4_resized_buf(mosaic, out_h, out_w) # float32, shape (4,H,W) em 0..1
raw4 = load_raw4_float(raw_path, (src_h, src_w)) # (4, Hsrc, Wsrc)
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
# raw4: (C,H,W) -> (C,N)
@ -302,7 +306,8 @@ def main(args):
"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:
json.dump(stats, f, indent=2, ensure_ascii=False)

View File

@ -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
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
# (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:
norm_stats_path = args.norm_stats

View File

@ -5,19 +5,18 @@ Teste/visualização do SegFormer RAW (4 ou 5 canais) treinado no _8_train_segfo
Modos:
1) Dataset (padrão):
- os RAWs + masks da pasta dataset/split/<split_folder>
- Mostra: [preview RAW (RGB)] | [mask GT] | [mask predita]
- Navegação:
D -> próxima
A -> anterior
Q -> sair
1) Dataset (com GT):
- Usa RawSegDataset (raws + masks) a partir de split (train/val/test)
- Mostra: [preview RAW (RGB)] | [mask GT] | [overlay predito]
2) Câmera ao vivo (--camera):
2) Pasta sem máscara (--test_folder sem masks):
- Faz inferência 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)
- Monta entrada com 4 ou 5 canais (IR bruto / NDVI / IR+NDVI) conforme config
- Faz inferência com SegFormer
- Mostra RGB + overlay da segmentação em tempo real
- Monta os 4/5 canais conforme config
- Mostra overlay em tempo real
"""
import os
@ -30,29 +29,189 @@ import cv2
import torch
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
# ============================================================
# 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):
"""
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():
parser = argparse.ArgumentParser()
# Config / 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("--root_override", type=str, default=None, help="Se quiser apontar 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("--raw_max", type=float, default=None, help="Mesmo raw_max do treino (se usou). Usado internamente no dataset.")
parser.add_argument("--root_override", type=str, default=None, help="Aponta direto para uma pasta split (sobrepõe split_folder)")
parser.add_argument(
"--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
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("--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("--resize_h", type=int, default=None, help="Altura para inferência (velocidade)")
parser.add_argument("--resize_w", type=int, default=None, help="Largura 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 (override)")
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("--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)
MODELO = config["camera"] # ex: "gal5000"
MODEL_NAME = config["model_name"] # ex: "segformer_b3"
modelo_folder = config["modelo"] # ex: "dif"
#MODEL_NAME = config["model_name"] # ex: "segformer_b0"
MODEL_NAME = "pulv2_1008x800"
modelo_folder = config["modelo"] # ex: "weed_1008x800"
USE_NDVI = bool(config.get("use_ndvi", False))
CHANNELS = int(config.get("channels", 4))
RESOLUCAO = config["resolucao"]
W, H = RESOLUCAO[0], RESOLUCAO[1]
if (args.resize_h is not None):
if args.resize_h is not None:
H = args.resize_h
if (args.resize_w is not None):
if args.resize_w is not None:
W = args.resize_w
print(f"[cfg] resolucao nominal RAW: {W}x{H}")
@ -86,12 +246,12 @@ def main():
labelmap_path = os.path.join(dataset_path, "labelmap.txt")
# ==========================
# Normalização fixa (como no treino)
# Normalização fixa (igual treino)
# ==========================
norm_mean = 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:
norm_stats_path = args.norm_stats
if os.path.isfile(norm_stats_path):
@ -107,10 +267,8 @@ def main():
print(f"[NORM] mean={stats_mean}")
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)}
# Sempre montamos na ordem interna base [R,G,B,IR]
m_R = stats_mean[idx_by_name["R"]]
m_G = stats_mean[idx_by_name["G"]]
m_IR = stats_mean[idx_by_name["IR"]]
@ -121,20 +279,13 @@ def main():
s_IR = stats_std[idx_by_name["IR"]]
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:
# [R,G,B,IR]
norm_mean = [m_R, m_G, m_B, m_IR]
norm_std = [s_R, s_G, s_B, s_IR]
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_std = [s_R, s_G, s_B, 1.0]
elif CHANNELS == 5 and USE_NDVI:
# [R,G,B,IR,NDVI]
norm_mean = [m_R, m_G, m_B, m_IR, 0.0]
norm_std = [s_R, s_G, s_B, s_IR, 1.0]
else:
@ -150,8 +301,6 @@ def main():
if args.ckpt is not None:
ckpt_path = args.ckpt
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}")
ckpt_path = os.path.join(save_path, "best_miou.pt")
@ -160,7 +309,7 @@ def main():
print(f"[model] ckpt = {ckpt_path}")
# ---- Instancia service do modelo RAW (4 ou 5 canais) ----
# ---- Instancia service do modelo RAW ----
model_svc = RawSegformerService(
config_path=args.config,
ckpt_path=ckpt_path,
@ -182,7 +331,13 @@ def main():
# ======================================================================
if args.camera:
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)"
cv2.namedWindow(win, cv2.WINDOW_NORMAL)
@ -194,22 +349,17 @@ def main():
cam.start_streaming()
while True:
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)
t1 = time.time()
# Separa canais básicos
r = raw4_base[0]
g = raw4_base[1]
ir = raw4_base[2]
b = raw4_base[3]
# Deixa o service montar a entrada final com 4 ou 5 canais
# (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
raw_input = model_svc.build_raw_input(r, g, ir, b)
t2 = time.time()
# Inferência + previews via service
pred_ids = model_svc.infer_raw(raw_input)
t3 = time.time()
_, _, overlay, _, t_pvw = model_svc.preview_infer_cached(raw_input, pred_ids, alpha=args.alpha)
@ -234,7 +384,8 @@ def main():
]
y = 24
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
cv2.imshow(win, overlay)
@ -246,18 +397,24 @@ def main():
return
# ======================================================================
# MODO DATASET (A/D)
# MODO DATASET / TEST_FOLDER
# ======================================================================
# ---- Define raiz do split ----
if args.root_override is not None:
# ---- Define raiz do split/test ----
if args.test_folder is not None:
raw_root = args.test_folder
elif args.root_override is not None:
raw_root = args.root_override
else:
raw_root = os.path.join(dataset_path, "split", args.split_folder)
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(
raw_root,
labelmap_path=labelmap_path,
@ -267,60 +424,95 @@ def main():
use_ndvi=USE_NDVI,
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
n = len(ds)
print(f"[data] total de amostras: {n}")
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:
if has_gt:
# ------- Caminho com GT -------
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
# Converte para numpy (C,H,W) float32 0..1
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)
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
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)
# Legenda embaixo
resultado = np.concatenate([preview_rgb, gt_resized, overlay_resized], axis=1)
legenda = desenhar_legenda_horizontal(colormap_rgb, classes)
legenda_resized = cv2.resize(legenda, (resultado.shape[1], legenda.shape[0]), interpolation=cv2.INTER_NEAREST)
resultado_completo = np.concatenate([resultado, legenda_resized], axis=0)
# Índice + info de canais
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)
header_txt = f"idx {idx + 1}/{n} | C={CHANNELS} NDVI={int(USE_NDVI)}"
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:
x, y, win_w, win_h = cv2.getWindowImageRect(win_name)
except:
_x, _y, win_w, win_h = cv2.getWindowImageRect(win_name)
except Exception:
win_w, win_h = 0, 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)
else:
display = resultado_completo
# OpenCV usa BGR
cv2.imshow(win_name, cv2.cvtColor(display, cv2.COLOR_RGB2BGR))
cv2.imshow(win_name, display)
key = cv2.waitKey(0) & 0xFF
if key in (ord("q"), ord("Q"), 27):

View File

@ -1,17 +1,17 @@
{
"camera": "oak-d",
"camera": "gal5000",
"modelo": "segformer_b0",
"model_name": "nav",
"model_name": "pulv_n_1008x800",
"dual_head": false,
"main_class_name": "navegavel",
"main_class_name": "pulverizar",
"es_classes": "",
"model_to_use": "geral",
"raw_size": [1296, 1028],
"resolucao": [1024, 576],
"resolucao": [1008, 800],
"roi_inicio": 0.0,
"roi_tamanho": 1.0,
"shaves": 3,
"channels": 3,
"channels": 5,
"use_ndvi": true,
"backbone": "nvidia/segformer-b0-finetuned-ade-512-512"
}

View File

@ -1,17 +1,17 @@
{
"camera": "gal5000",
"camera": "oak-d",
"modelo": "segformer_b0",
"model_name": "ndvi_big",
"model_name": "nav",
"dual_head": false,
"main_class_name": "erva",
"main_class_name": "navegavel",
"es_classes": "",
"model_to_use": "geral",
"raw_size": [1296, 1028],
"resolucao": [1008, 800],
"resolucao": [1024, 576],
"roi_inicio": 0.0,
"roi_tamanho": 1.0,
"shaves": 3,
"channels": 5,
"channels": 3,
"use_ndvi": true,
"backbone": "nvidia/segformer-b0-finetuned-ade-512-512"
}

BIN
Python/dalybms/ControlCANFD.dll (Stored with Git LFS) Normal file

Binary file not shown.

465
Python/dalybms/console.py Normal file
View File

@ -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()

View File

@ -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...")

371
Python/dalybms/tradutor.py Normal file
View File

@ -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()