novo core da camera multiespectral versao produto, saude do atuador nao penaliza autonomia quando nao precisa, saude do movimentacao avalia travamento da roda, saude do sensoriamento avalia comando e leitura dos servos, reles e sinaleiros, com persistencia temporal, regras taticas permite parar o carro ao detectar travamento das rodas, travamento das rodas aciona parada de emergencia, ajustado contrato do weed worker para a versao produto, adicionado offset de referenciamento direcional, ajustado calculo de erro angular no direcional entre comando e leitura com persistencia temporal, ajustado limites dos sensores, incluido parada de emergencia mediante a liberacao humana apos travamento das rodas
This commit is contained in:
parent
21decdbbe9
commit
c9ad8d2aff
|
|
@ -36,6 +36,7 @@ namespace AgroBase.Forms.Direcional
|
|||
this.lblTipoMovimento = new System.Windows.Forms.Label();
|
||||
this.btnSalvarSentido = new System.Windows.Forms.Button();
|
||||
this.gpbDF = new System.Windows.Forms.GroupBox();
|
||||
this.chbDFUsar = new System.Windows.Forms.CheckBox();
|
||||
this.label5 = new System.Windows.Forms.Label();
|
||||
this.txtDFOffsetAngulo = new System.Windows.Forms.TextBox();
|
||||
this.label2 = new System.Windows.Forms.Label();
|
||||
|
|
@ -47,6 +48,7 @@ namespace AgroBase.Forms.Direcional
|
|||
this.lblDFEsquerda = new System.Windows.Forms.Label();
|
||||
this.lblDFDireita = new System.Windows.Forms.Label();
|
||||
this.gpbEF = new System.Windows.Forms.GroupBox();
|
||||
this.chbEFUsar = new System.Windows.Forms.CheckBox();
|
||||
this.label6 = new System.Windows.Forms.Label();
|
||||
this.txtEFOffsetAngulo = new System.Windows.Forms.TextBox();
|
||||
this.label4 = new System.Windows.Forms.Label();
|
||||
|
|
@ -58,6 +60,7 @@ namespace AgroBase.Forms.Direcional
|
|||
this.lblEFEsquerda = new System.Windows.Forms.Label();
|
||||
this.lblEFDireita = new System.Windows.Forms.Label();
|
||||
this.gpbDT = new System.Windows.Forms.GroupBox();
|
||||
this.chbDTUsar = new System.Windows.Forms.CheckBox();
|
||||
this.label8 = new System.Windows.Forms.Label();
|
||||
this.txtDTOffsetAngulo = new System.Windows.Forms.TextBox();
|
||||
this.label3 = new System.Windows.Forms.Label();
|
||||
|
|
@ -69,6 +72,7 @@ namespace AgroBase.Forms.Direcional
|
|||
this.lblDTEsquerda = new System.Windows.Forms.Label();
|
||||
this.lblDTDireita = new System.Windows.Forms.Label();
|
||||
this.gpbET = new System.Windows.Forms.GroupBox();
|
||||
this.chbETUsar = new System.Windows.Forms.CheckBox();
|
||||
this.label7 = new System.Windows.Forms.Label();
|
||||
this.txtETOffsetAngulo = new System.Windows.Forms.TextBox();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
|
|
@ -79,10 +83,14 @@ namespace AgroBase.Forms.Direcional
|
|||
this.cmbETEsquerda = new System.Windows.Forms.ComboBox();
|
||||
this.lblETEsquerda = new System.Windows.Forms.Label();
|
||||
this.lblETDireita = new System.Windows.Forms.Label();
|
||||
this.chbDFUsar = new System.Windows.Forms.CheckBox();
|
||||
this.chbDTUsar = new System.Windows.Forms.CheckBox();
|
||||
this.chbEFUsar = new System.Windows.Forms.CheckBox();
|
||||
this.chbETUsar = new System.Windows.Forms.CheckBox();
|
||||
this.txtDTOffsetRef = new System.Windows.Forms.TextBox();
|
||||
this.label9 = new System.Windows.Forms.Label();
|
||||
this.label10 = new System.Windows.Forms.Label();
|
||||
this.txtETOffsetRef = new System.Windows.Forms.TextBox();
|
||||
this.label11 = new System.Windows.Forms.Label();
|
||||
this.txtEFOffsetRef = new System.Windows.Forms.TextBox();
|
||||
this.label12 = new System.Windows.Forms.Label();
|
||||
this.txtDFOffsetRef = new System.Windows.Forms.TextBox();
|
||||
this.gpbSentido.SuspendLayout();
|
||||
this.gpbDF.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudDFOffset)).BeginInit();
|
||||
|
|
@ -109,7 +117,7 @@ namespace AgroBase.Forms.Direcional
|
|||
this.gpbSentido.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.gpbSentido.Name = "gpbSentido";
|
||||
this.gpbSentido.Padding = new System.Windows.Forms.Padding(2);
|
||||
this.gpbSentido.Size = new System.Drawing.Size(376, 427);
|
||||
this.gpbSentido.Size = new System.Drawing.Size(376, 463);
|
||||
this.gpbSentido.TabIndex = 80;
|
||||
this.gpbSentido.TabStop = false;
|
||||
this.gpbSentido.Text = "Sentido de giro";
|
||||
|
|
@ -117,7 +125,7 @@ namespace AgroBase.Forms.Direcional
|
|||
// lblIP
|
||||
//
|
||||
this.lblIP.AutoSize = true;
|
||||
this.lblIP.Location = new System.Drawing.Point(9, 378);
|
||||
this.lblIP.Location = new System.Drawing.Point(9, 422);
|
||||
this.lblIP.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lblIP.Name = "lblIP";
|
||||
this.lblIP.Size = new System.Drawing.Size(17, 13);
|
||||
|
|
@ -126,7 +134,7 @@ namespace AgroBase.Forms.Direcional
|
|||
//
|
||||
// txtIP
|
||||
//
|
||||
this.txtIP.Location = new System.Drawing.Point(12, 395);
|
||||
this.txtIP.Location = new System.Drawing.Point(12, 439);
|
||||
this.txtIP.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.txtIP.Name = "txtIP";
|
||||
this.txtIP.Size = new System.Drawing.Size(85, 20);
|
||||
|
|
@ -154,7 +162,7 @@ namespace AgroBase.Forms.Direcional
|
|||
//
|
||||
// btnSalvarSentido
|
||||
//
|
||||
this.btnSalvarSentido.Location = new System.Drawing.Point(275, 391);
|
||||
this.btnSalvarSentido.Location = new System.Drawing.Point(275, 435);
|
||||
this.btnSalvarSentido.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.btnSalvarSentido.Name = "btnSalvarSentido";
|
||||
this.btnSalvarSentido.Size = new System.Drawing.Size(88, 24);
|
||||
|
|
@ -165,6 +173,8 @@ namespace AgroBase.Forms.Direcional
|
|||
//
|
||||
// gpbDF
|
||||
//
|
||||
this.gpbDF.Controls.Add(this.label12);
|
||||
this.gpbDF.Controls.Add(this.txtDFOffsetRef);
|
||||
this.gpbDF.Controls.Add(this.chbDFUsar);
|
||||
this.gpbDF.Controls.Add(this.label5);
|
||||
this.gpbDF.Controls.Add(this.txtDFOffsetAngulo);
|
||||
|
|
@ -180,11 +190,21 @@ namespace AgroBase.Forms.Direcional
|
|||
this.gpbDF.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.gpbDF.Name = "gpbDF";
|
||||
this.gpbDF.Padding = new System.Windows.Forms.Padding(2);
|
||||
this.gpbDF.Size = new System.Drawing.Size(168, 152);
|
||||
this.gpbDF.Size = new System.Drawing.Size(168, 175);
|
||||
this.gpbDF.TabIndex = 9;
|
||||
this.gpbDF.TabStop = false;
|
||||
this.gpbDF.Text = "M4 - Direito Frente (DF)";
|
||||
//
|
||||
// chbDFUsar
|
||||
//
|
||||
this.chbDFUsar.AutoSize = true;
|
||||
this.chbDFUsar.Location = new System.Drawing.Point(127, 124);
|
||||
this.chbDFUsar.Name = "chbDFUsar";
|
||||
this.chbDFUsar.Size = new System.Drawing.Size(32, 17);
|
||||
this.chbDFUsar.TabIndex = 19;
|
||||
this.chbDFUsar.Text = "L";
|
||||
this.chbDFUsar.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// label5
|
||||
//
|
||||
this.label5.AutoSize = true;
|
||||
|
|
@ -294,6 +314,8 @@ namespace AgroBase.Forms.Direcional
|
|||
//
|
||||
// gpbEF
|
||||
//
|
||||
this.gpbEF.Controls.Add(this.label11);
|
||||
this.gpbEF.Controls.Add(this.txtEFOffsetRef);
|
||||
this.gpbEF.Controls.Add(this.chbEFUsar);
|
||||
this.gpbEF.Controls.Add(this.label6);
|
||||
this.gpbEF.Controls.Add(this.txtEFOffsetAngulo);
|
||||
|
|
@ -309,11 +331,21 @@ namespace AgroBase.Forms.Direcional
|
|||
this.gpbEF.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.gpbEF.Name = "gpbEF";
|
||||
this.gpbEF.Padding = new System.Windows.Forms.Padding(2);
|
||||
this.gpbEF.Size = new System.Drawing.Size(168, 152);
|
||||
this.gpbEF.Size = new System.Drawing.Size(168, 175);
|
||||
this.gpbEF.TabIndex = 9;
|
||||
this.gpbEF.TabStop = false;
|
||||
this.gpbEF.Text = "M2 - Esquerdo Frente (EF)";
|
||||
//
|
||||
// chbEFUsar
|
||||
//
|
||||
this.chbEFUsar.AutoSize = true;
|
||||
this.chbEFUsar.Location = new System.Drawing.Point(127, 124);
|
||||
this.chbEFUsar.Name = "chbEFUsar";
|
||||
this.chbEFUsar.Size = new System.Drawing.Size(32, 17);
|
||||
this.chbEFUsar.TabIndex = 21;
|
||||
this.chbEFUsar.Text = "L";
|
||||
this.chbEFUsar.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// label6
|
||||
//
|
||||
this.label6.AutoSize = true;
|
||||
|
|
@ -423,6 +455,8 @@ namespace AgroBase.Forms.Direcional
|
|||
//
|
||||
// gpbDT
|
||||
//
|
||||
this.gpbDT.Controls.Add(this.label9);
|
||||
this.gpbDT.Controls.Add(this.txtDTOffsetRef);
|
||||
this.gpbDT.Controls.Add(this.chbDTUsar);
|
||||
this.gpbDT.Controls.Add(this.label8);
|
||||
this.gpbDT.Controls.Add(this.txtDTOffsetAngulo);
|
||||
|
|
@ -434,15 +468,25 @@ namespace AgroBase.Forms.Direcional
|
|||
this.gpbDT.Controls.Add(this.cmbDTEsquerda);
|
||||
this.gpbDT.Controls.Add(this.lblDTEsquerda);
|
||||
this.gpbDT.Controls.Add(this.lblDTDireita);
|
||||
this.gpbDT.Location = new System.Drawing.Point(195, 216);
|
||||
this.gpbDT.Location = new System.Drawing.Point(195, 239);
|
||||
this.gpbDT.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.gpbDT.Name = "gpbDT";
|
||||
this.gpbDT.Padding = new System.Windows.Forms.Padding(2);
|
||||
this.gpbDT.Size = new System.Drawing.Size(168, 152);
|
||||
this.gpbDT.Size = new System.Drawing.Size(168, 175);
|
||||
this.gpbDT.TabIndex = 9;
|
||||
this.gpbDT.TabStop = false;
|
||||
this.gpbDT.Text = "M3 - Direito Trás (DT)";
|
||||
//
|
||||
// chbDTUsar
|
||||
//
|
||||
this.chbDTUsar.AutoSize = true;
|
||||
this.chbDTUsar.Location = new System.Drawing.Point(125, 124);
|
||||
this.chbDTUsar.Name = "chbDTUsar";
|
||||
this.chbDTUsar.Size = new System.Drawing.Size(32, 17);
|
||||
this.chbDTUsar.TabIndex = 23;
|
||||
this.chbDTUsar.Text = "L";
|
||||
this.chbDTUsar.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// label8
|
||||
//
|
||||
this.label8.AutoSize = true;
|
||||
|
|
@ -552,6 +596,8 @@ namespace AgroBase.Forms.Direcional
|
|||
//
|
||||
// gpbET
|
||||
//
|
||||
this.gpbET.Controls.Add(this.label10);
|
||||
this.gpbET.Controls.Add(this.txtETOffsetRef);
|
||||
this.gpbET.Controls.Add(this.chbETUsar);
|
||||
this.gpbET.Controls.Add(this.label7);
|
||||
this.gpbET.Controls.Add(this.txtETOffsetAngulo);
|
||||
|
|
@ -563,15 +609,25 @@ namespace AgroBase.Forms.Direcional
|
|||
this.gpbET.Controls.Add(this.cmbETEsquerda);
|
||||
this.gpbET.Controls.Add(this.lblETEsquerda);
|
||||
this.gpbET.Controls.Add(this.lblETDireita);
|
||||
this.gpbET.Location = new System.Drawing.Point(13, 216);
|
||||
this.gpbET.Location = new System.Drawing.Point(13, 239);
|
||||
this.gpbET.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.gpbET.Name = "gpbET";
|
||||
this.gpbET.Padding = new System.Windows.Forms.Padding(2);
|
||||
this.gpbET.Size = new System.Drawing.Size(168, 152);
|
||||
this.gpbET.Size = new System.Drawing.Size(168, 175);
|
||||
this.gpbET.TabIndex = 0;
|
||||
this.gpbET.TabStop = false;
|
||||
this.gpbET.Text = "M1 - Esquerdo Trás (ET)";
|
||||
//
|
||||
// chbETUsar
|
||||
//
|
||||
this.chbETUsar.AutoSize = true;
|
||||
this.chbETUsar.Location = new System.Drawing.Point(127, 124);
|
||||
this.chbETUsar.Name = "chbETUsar";
|
||||
this.chbETUsar.Size = new System.Drawing.Size(32, 17);
|
||||
this.chbETUsar.TabIndex = 23;
|
||||
this.chbETUsar.Text = "L";
|
||||
this.chbETUsar.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// label7
|
||||
//
|
||||
this.label7.AutoSize = true;
|
||||
|
|
@ -679,52 +735,88 @@ namespace AgroBase.Forms.Direcional
|
|||
this.lblETDireita.TabIndex = 2;
|
||||
this.lblETDireita.Text = "Direita";
|
||||
//
|
||||
// chbDFUsar
|
||||
// txtDTOffsetRef
|
||||
//
|
||||
this.chbDFUsar.AutoSize = true;
|
||||
this.chbDFUsar.Location = new System.Drawing.Point(127, 124);
|
||||
this.chbDFUsar.Name = "chbDFUsar";
|
||||
this.chbDFUsar.Size = new System.Drawing.Size(32, 17);
|
||||
this.chbDFUsar.TabIndex = 19;
|
||||
this.chbDFUsar.Text = "L";
|
||||
this.chbDFUsar.UseVisualStyleBackColor = true;
|
||||
this.txtDTOffsetRef.Location = new System.Drawing.Point(65, 146);
|
||||
this.txtDTOffsetRef.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.txtDTOffsetRef.Name = "txtDTOffsetRef";
|
||||
this.txtDTOffsetRef.Size = new System.Drawing.Size(57, 20);
|
||||
this.txtDTOffsetRef.TabIndex = 24;
|
||||
this.txtDTOffsetRef.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
|
||||
//
|
||||
// chbDTUsar
|
||||
// label9
|
||||
//
|
||||
this.chbDTUsar.AutoSize = true;
|
||||
this.chbDTUsar.Location = new System.Drawing.Point(125, 124);
|
||||
this.chbDTUsar.Name = "chbDTUsar";
|
||||
this.chbDTUsar.Size = new System.Drawing.Size(32, 17);
|
||||
this.chbDTUsar.TabIndex = 23;
|
||||
this.chbDTUsar.Text = "L";
|
||||
this.chbDTUsar.UseVisualStyleBackColor = true;
|
||||
this.label9.AutoSize = true;
|
||||
this.label9.Location = new System.Drawing.Point(10, 149);
|
||||
this.label9.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.label9.Name = "label9";
|
||||
this.label9.Size = new System.Drawing.Size(55, 13);
|
||||
this.label9.TabIndex = 25;
|
||||
this.label9.Text = "Offset Ref";
|
||||
//
|
||||
// chbEFUsar
|
||||
// label10
|
||||
//
|
||||
this.chbEFUsar.AutoSize = true;
|
||||
this.chbEFUsar.Location = new System.Drawing.Point(127, 124);
|
||||
this.chbEFUsar.Name = "chbEFUsar";
|
||||
this.chbEFUsar.Size = new System.Drawing.Size(32, 17);
|
||||
this.chbEFUsar.TabIndex = 21;
|
||||
this.chbEFUsar.Text = "L";
|
||||
this.chbEFUsar.UseVisualStyleBackColor = true;
|
||||
this.label10.AutoSize = true;
|
||||
this.label10.Location = new System.Drawing.Point(10, 149);
|
||||
this.label10.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.label10.Name = "label10";
|
||||
this.label10.Size = new System.Drawing.Size(55, 13);
|
||||
this.label10.TabIndex = 27;
|
||||
this.label10.Text = "Offset Ref";
|
||||
//
|
||||
// chbETUsar
|
||||
// txtETOffsetRef
|
||||
//
|
||||
this.chbETUsar.AutoSize = true;
|
||||
this.chbETUsar.Location = new System.Drawing.Point(127, 124);
|
||||
this.chbETUsar.Name = "chbETUsar";
|
||||
this.chbETUsar.Size = new System.Drawing.Size(32, 17);
|
||||
this.chbETUsar.TabIndex = 23;
|
||||
this.chbETUsar.Text = "L";
|
||||
this.chbETUsar.UseVisualStyleBackColor = true;
|
||||
this.txtETOffsetRef.Location = new System.Drawing.Point(65, 146);
|
||||
this.txtETOffsetRef.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.txtETOffsetRef.Name = "txtETOffsetRef";
|
||||
this.txtETOffsetRef.Size = new System.Drawing.Size(57, 20);
|
||||
this.txtETOffsetRef.TabIndex = 26;
|
||||
this.txtETOffsetRef.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
|
||||
//
|
||||
// label11
|
||||
//
|
||||
this.label11.AutoSize = true;
|
||||
this.label11.Location = new System.Drawing.Point(11, 149);
|
||||
this.label11.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.label11.Name = "label11";
|
||||
this.label11.Size = new System.Drawing.Size(55, 13);
|
||||
this.label11.TabIndex = 29;
|
||||
this.label11.Text = "Offset Ref";
|
||||
//
|
||||
// txtEFOffsetRef
|
||||
//
|
||||
this.txtEFOffsetRef.Location = new System.Drawing.Point(66, 146);
|
||||
this.txtEFOffsetRef.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.txtEFOffsetRef.Name = "txtEFOffsetRef";
|
||||
this.txtEFOffsetRef.Size = new System.Drawing.Size(57, 20);
|
||||
this.txtEFOffsetRef.TabIndex = 28;
|
||||
this.txtEFOffsetRef.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
|
||||
//
|
||||
// label12
|
||||
//
|
||||
this.label12.AutoSize = true;
|
||||
this.label12.Location = new System.Drawing.Point(10, 149);
|
||||
this.label12.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.label12.Name = "label12";
|
||||
this.label12.Size = new System.Drawing.Size(55, 13);
|
||||
this.label12.TabIndex = 31;
|
||||
this.label12.Text = "Offset Ref";
|
||||
//
|
||||
// txtDFOffsetRef
|
||||
//
|
||||
this.txtDFOffsetRef.Location = new System.Drawing.Point(65, 146);
|
||||
this.txtDFOffsetRef.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.txtDFOffsetRef.Name = "txtDFOffsetRef";
|
||||
this.txtDFOffsetRef.Size = new System.Drawing.Size(57, 20);
|
||||
this.txtDFOffsetRef.TabIndex = 30;
|
||||
this.txtDFOffsetRef.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
|
||||
//
|
||||
// frmDirConfig
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.BackColor = System.Drawing.Color.White;
|
||||
this.ClientSize = new System.Drawing.Size(398, 447);
|
||||
this.ClientSize = new System.Drawing.Size(398, 485);
|
||||
this.Controls.Add(this.gpbSentido);
|
||||
this.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.Name = "frmDirConfig";
|
||||
|
|
@ -805,5 +897,13 @@ namespace AgroBase.Forms.Direcional
|
|||
private System.Windows.Forms.CheckBox chbEFUsar;
|
||||
private System.Windows.Forms.CheckBox chbDTUsar;
|
||||
private System.Windows.Forms.CheckBox chbETUsar;
|
||||
private System.Windows.Forms.Label label12;
|
||||
private System.Windows.Forms.TextBox txtDFOffsetRef;
|
||||
private System.Windows.Forms.Label label11;
|
||||
private System.Windows.Forms.TextBox txtEFOffsetRef;
|
||||
private System.Windows.Forms.Label label9;
|
||||
private System.Windows.Forms.TextBox txtDTOffsetRef;
|
||||
private System.Windows.Forms.Label label10;
|
||||
private System.Windows.Forms.TextBox txtETOffsetRef;
|
||||
}
|
||||
}
|
||||
|
|
@ -72,6 +72,9 @@ namespace AgroBase.Forms.Direcional
|
|||
TextBox txtOffsetAngulo = FuncoesGlobais.FindControlRecursive<TextBox>(gpbSentido, "txt" + Modulo.Modulo_ID + "OffsetAngulo");
|
||||
txtOffsetAngulo.Text = Modulo.DirMotor.OffsetAnguloReal.ToString();
|
||||
|
||||
TextBox txtOffsetRef = FuncoesGlobais.FindControlRecursive<TextBox>(gpbSentido, "txt" + Modulo.Modulo_ID + "OffsetRef");
|
||||
txtOffsetRef.Text = Modulo.DirMotor.OffsetReferenciamento.ToString();
|
||||
|
||||
CheckBox chbUsar = FuncoesGlobais.FindControlRecursive<CheckBox>(gpbSentido, "chb" + Modulo.Modulo_ID + "Usar");
|
||||
chbUsar.Checked = Modulo.DirMotor.UsarAnguloSensor;
|
||||
}
|
||||
|
|
@ -119,6 +122,9 @@ namespace AgroBase.Forms.Direcional
|
|||
TextBox txtOffsetAngulo = FuncoesGlobais.FindControlRecursive<TextBox>(gpbSentido, "txt" + Modulo.Modulo_ID + "OffsetAngulo");
|
||||
Modulo.DirMotor.OffsetAnguloReal = Convert.ToDouble(txtOffsetAngulo.Text);
|
||||
|
||||
TextBox txtOffsetRef = FuncoesGlobais.FindControlRecursive<TextBox>(gpbSentido, "txt" + Modulo.Modulo_ID + "OffsetRef");
|
||||
Modulo.DirMotor.OffsetReferenciamento = Convert.ToDouble(txtOffsetRef.Text);
|
||||
|
||||
CheckBox chbUsar = FuncoesGlobais.FindControlRecursive<CheckBox>(gpbSentido, "chb" + Modulo.Modulo_ID + "Usar");
|
||||
Modulo.DirMotor.UsarAnguloSensor = chbUsar.Checked;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -339,13 +339,28 @@ namespace AgroBase.Models.Modules
|
|||
{
|
||||
var op = Variaveis.OperacaoEmAndamento;
|
||||
|
||||
if (op.CalibrandoRuntime)
|
||||
if (op == null)
|
||||
return;
|
||||
|
||||
long tickSolicitado = Stopwatch.GetTimestamp();
|
||||
|
||||
lock (_metricasLock)
|
||||
{
|
||||
long agoraTicks = Stopwatch.GetTimestamp();
|
||||
DateTime agora = DateTime.Now;
|
||||
double esperaLockSeg = (agoraTicks - tickSolicitado) / (double)Stopwatch.Frequency;
|
||||
double tempoMovimentoAtualSeg = op.Sensoriamento?.Movimentacao?.TempoMovimentoSegs ?? 0.0;
|
||||
|
||||
if (op.CalibrandoRuntime)
|
||||
{
|
||||
/*
|
||||
* Durante a calibragem não integra, mas mantém
|
||||
* o relógio ancorado no instante atual.
|
||||
*/
|
||||
ReancorarMetricas(agoraTicks, tempoMovimentoAtualSeg, encerrarTrechos: true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool operacaoConcluida = op.Sensoriamento?.Operacao?.StatusOperacaoAtual == StatusOperacao.Concluido;
|
||||
bool operacaoAtiva = op.Sensoriamento?.Operacao?.OperacaoIniciada == true && !operacaoConcluida;
|
||||
|
|
@ -411,10 +426,11 @@ namespace AgroBase.Models.Modules
|
|||
: new HashSet<int>();
|
||||
|
||||
double vazaoAtualMLs = operacaoAtiva ? ObterVazaoAtualConfiavelMLs(agora) : 0.0;
|
||||
double tempoMovimentoAtualSeg = op.Sensoriamento?.Movimentacao?.TempoMovimentoSegs ?? 0.0;
|
||||
|
||||
if (_ultimoTickMetricas != 0)
|
||||
{
|
||||
bool haviaAtuacaoNoIntervaloAnterior = _bicosAtivosIntervaloAnterior.Count > 0 || _bombasAtivasIntervaloAnterior.Count > 0 || _vazaoIntervaloAnteriorMLs > 0.01;
|
||||
|
||||
double dt = (agoraTicks - _ultimoTickMetricas) / (double)Stopwatch.Frequency;
|
||||
|
||||
if (dt > 0 && dt <= IntervaloMaximoIntegracaoSeg)
|
||||
|
|
@ -443,8 +459,26 @@ namespace AgroBase.Models.Modules
|
|||
|
||||
if (dt > IntervaloMaximoIntegracaoSeg)
|
||||
{
|
||||
Variaveis.MostrarLog("[ATU/METRICAS] Integração ignorada por dt alto: " + dt.ToString("0.000") + "s");
|
||||
op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Alerta, 50, "[ATU/METRICAS] Integração ignorada por dt alto: " + dt.ToString("0.000") + "s");
|
||||
string mensagem =
|
||||
"[ATU/METRICAS] Integração ignorada por dt alto: " +
|
||||
$"{dt:0.000}s; " +
|
||||
$"opAtiva={operacaoAtiva}; " +
|
||||
$"atuacaoAnterior={haviaAtuacaoNoIntervaloAnterior}; " +
|
||||
$"bicosAnterior={_bicosAtivosIntervaloAnterior.Count}; " +
|
||||
$"bombasAnterior={_bombasAtivasIntervaloAnterior.Count}; " +
|
||||
$"vazaoAnterior={_vazaoIntervaloAnteriorMLs:0.00}mL/s; " +
|
||||
$"esperaLock={esperaLockSeg * 1000.0:0.0}ms";
|
||||
|
||||
Variaveis.MostrarLog(mensagem);
|
||||
|
||||
/*
|
||||
* Só vira evento operacional se efetivamente
|
||||
* perdemos um intervalo de pulverização.
|
||||
*/
|
||||
if (operacaoAtiva && haviaAtuacaoNoIntervaloAnterior)
|
||||
{
|
||||
op.Sensoriamento?.InserirLog(Dispositivo, StatusModulo.Alerta, 80, mensagem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -494,6 +528,24 @@ namespace AgroBase.Models.Modules
|
|||
}
|
||||
}
|
||||
|
||||
private void ReancorarMetricas(long agoraTicks, double tempoMovimentoAtualSeg, bool encerrarTrechos)
|
||||
{
|
||||
if (encerrarTrechos)
|
||||
{
|
||||
foreach (var bico in BicosPulverizadores ?? new List<AtuadorBicoModel>())
|
||||
{
|
||||
bico?.AtualizarEstadoTrecho(false);
|
||||
}
|
||||
}
|
||||
_ultimoTickMetricas = agoraTicks;
|
||||
_tempoMovimentoAnteriorSeg = tempoMovimentoAtualSeg;
|
||||
_bicosAtivosIntervaloAnterior = new HashSet<int>();
|
||||
_bombasAtivasIntervaloAnterior = new HashSet<int>();
|
||||
_potenciaBombasIntervaloAnterior = new Dictionary<int, double>();
|
||||
_vazaoIntervaloAnteriorMLs = 0.0;
|
||||
ZerarVazoesInstantaneas();
|
||||
}
|
||||
|
||||
private void IntegrarIntervaloAnterior(double dt, double tempoMovimentoIntervaloSeg)
|
||||
{
|
||||
var bicosDoIntervalo = (BicosPulverizadores ??
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ namespace AgroBase.Models.Modules
|
|||
public Sentido Sentido_SP { get; set; } = Sentido.Parado;
|
||||
public double Angulo_SP { get; set; } = 0;
|
||||
public double FolgaMecanica { get; set; } = 0;
|
||||
public double OffsetReferenciamento { get; set; } = 0;
|
||||
public double Velocidade_Max { get; set; } = 600;
|
||||
public int Velocidade
|
||||
{
|
||||
|
|
@ -149,11 +150,16 @@ namespace AgroBase.Models.Modules
|
|||
return !Comandar || Sentido_SP == Sentido.Parado ? 0 : Angulo_SP;
|
||||
}
|
||||
}
|
||||
public double ErroAnguloSP
|
||||
public double ErroAngulo
|
||||
{
|
||||
get
|
||||
{
|
||||
return Math.Abs(AnguloLeitura - AnguloSPAtual);
|
||||
double anguloSp =
|
||||
!Comandar || Sentido_SP == Sentido.Parado
|
||||
? 0
|
||||
: Angulo_SP;
|
||||
|
||||
return anguloSp - AnguloLeitura;
|
||||
}
|
||||
}
|
||||
public bool RetornandoAzero
|
||||
|
|
@ -165,13 +171,7 @@ namespace AgroBase.Models.Modules
|
|||
return foraZero && parar;
|
||||
}
|
||||
}
|
||||
public bool EmMovimento
|
||||
{
|
||||
get
|
||||
{
|
||||
return Math.Abs(RPM) > 1;
|
||||
}
|
||||
}
|
||||
public bool EmMovimento { get; set; }
|
||||
public bool AtingiuAnguloSP
|
||||
{
|
||||
get
|
||||
|
|
@ -207,6 +207,109 @@ namespace AgroBase.Models.Modules
|
|||
}
|
||||
|
||||
|
||||
private DateTime _erroAngularDesde = DateTime.MinValue;
|
||||
private DateTime _recuperacaoAngularDesde = DateTime.MinValue;
|
||||
|
||||
public DateTime UltimaMudancaAnguloSP { get; private set; }
|
||||
public DateTime InicioMovimentoAngular { get; private set; }
|
||||
|
||||
public bool ErroSeguimentoAngularPersistente { get; private set; }
|
||||
|
||||
public void AtualizarSaudeSeguimentoAngular(DateTime agora)
|
||||
{
|
||||
const double toleranciaFalhaGraus = 3.0;
|
||||
const double toleranciaRecuperacaoGraus = 2.0;
|
||||
|
||||
TimeSpan tempoGracaSP = TimeSpan.FromMilliseconds(750);
|
||||
TimeSpan persistenciaFalha = TimeSpan.FromSeconds(2);
|
||||
TimeSpan persistenciaRecuperacao = TimeSpan.FromMilliseconds(500);
|
||||
TimeSpan timeoutMovimento = TimeSpan.FromSeconds(15);
|
||||
|
||||
if (!Comandar)
|
||||
{
|
||||
ResetarSaudeSeguimentoAngular();
|
||||
return;
|
||||
}
|
||||
|
||||
bool emGracaSP =
|
||||
UltimaMudancaAnguloSP != DateTime.MinValue &&
|
||||
agora - UltimaMudancaAnguloSP <= tempoGracaSP;
|
||||
|
||||
bool movimentoDentroDoPrazo =
|
||||
EmMovimento &&
|
||||
InicioMovimentoAngular != DateTime.MinValue &&
|
||||
agora - InicioMovimentoAngular <= timeoutMovimento;
|
||||
|
||||
bool movimentoExcedeuTimeout =
|
||||
EmMovimento &&
|
||||
InicioMovimentoAngular != DateTime.MinValue &&
|
||||
agora - InicioMovimentoAngular > timeoutMovimento;
|
||||
|
||||
double ErroAnguloAbs = Math.Abs(ErroAngulo);
|
||||
|
||||
bool candidatoFalha =
|
||||
ErroAnguloAbs > toleranciaFalhaGraus &&
|
||||
!emGracaSP &&
|
||||
(!EmMovimento || movimentoExcedeuTimeout);
|
||||
|
||||
if (candidatoFalha)
|
||||
{
|
||||
_recuperacaoAngularDesde = DateTime.MinValue;
|
||||
|
||||
if (_erroAngularDesde == DateTime.MinValue)
|
||||
_erroAngularDesde = agora;
|
||||
|
||||
if (agora - _erroAngularDesde >= persistenciaFalha)
|
||||
ErroSeguimentoAngularPersistente = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_erroAngularDesde = DateTime.MinValue;
|
||||
|
||||
bool recuperado =
|
||||
ErroAnguloAbs <= toleranciaRecuperacaoGraus ||
|
||||
movimentoDentroDoPrazo ||
|
||||
emGracaSP;
|
||||
|
||||
if (!recuperado)
|
||||
{
|
||||
_recuperacaoAngularDesde = DateTime.MinValue;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_recuperacaoAngularDesde == DateTime.MinValue)
|
||||
_recuperacaoAngularDesde = agora;
|
||||
|
||||
if (agora - _recuperacaoAngularDesde >= persistenciaRecuperacao)
|
||||
ErroSeguimentoAngularPersistente = false;
|
||||
}
|
||||
|
||||
private void ResetarSaudeSeguimentoAngular()
|
||||
{
|
||||
_erroAngularDesde = DateTime.MinValue;
|
||||
_recuperacaoAngularDesde = DateTime.MinValue;
|
||||
ErroSeguimentoAngularPersistente = false;
|
||||
}
|
||||
|
||||
public void AtualizarEmMovimento(bool emMovimentoAgora)
|
||||
{
|
||||
DateTime agora = DateTime.Now;
|
||||
|
||||
if (emMovimentoAgora && !EmMovimento)
|
||||
{
|
||||
InicioMovimentoAngular = agora;
|
||||
}
|
||||
|
||||
if (!emMovimentoAgora)
|
||||
{
|
||||
InicioMovimentoAngular = DateTime.MinValue;
|
||||
}
|
||||
|
||||
EmMovimento = emMovimentoAgora;
|
||||
}
|
||||
|
||||
|
||||
public Dictionary<TipoMovimentoDirecional, ConfiguracaoSentidoMotor> ConfigSentidos { get; set; }
|
||||
public List<FuncoesPinout> Funcoes { get; set; }
|
||||
|
||||
|
|
@ -313,38 +416,41 @@ namespace AgroBase.Models.Modules
|
|||
var _Controle = op.Controle;
|
||||
int AnguloTipoMovimento = VariaveisEquipamento.AnguloMovimento[_Controle.TipoMovimento];
|
||||
|
||||
Sentido novoSentido = Sentido.Parado;
|
||||
double novoAngulo = 0;
|
||||
|
||||
if (!Comandar)
|
||||
{
|
||||
Sentido_SP = Sentido.Parado;
|
||||
novoSentido = Sentido.Parado;
|
||||
}
|
||||
else if (Direcao == Direcao.Direita)
|
||||
{
|
||||
Sentido_SP = ConfigSentidos[_Controle.TipoMovimento].Direita;
|
||||
novoSentido = ConfigSentidos[_Controle.TipoMovimento].Direita;
|
||||
}
|
||||
else if (Direcao == Direcao.Esquerda)
|
||||
{
|
||||
Sentido_SP = ConfigSentidos[_Controle.TipoMovimento].Esquerda;
|
||||
novoSentido = ConfigSentidos[_Controle.TipoMovimento].Esquerda;
|
||||
}
|
||||
else
|
||||
{
|
||||
Sentido_SP = Sentido.Parado;
|
||||
novoSentido = Sentido.Parado;
|
||||
}
|
||||
|
||||
UltimaDirecao = Direcao;
|
||||
int mx = Sentido_SP == Sentido.Parado ? 0 : Sentido_SP == Sentido.Antihorario ? -1 : 1;
|
||||
int mx = novoSentido == Sentido.Parado ? 0 : novoSentido == Sentido.Antihorario ? -1 : 1;
|
||||
|
||||
if (Sentido_SP == Sentido.Parado)
|
||||
if (novoSentido == Sentido.Parado)
|
||||
{
|
||||
Angulo_SP = 0;
|
||||
novoAngulo = 0;
|
||||
UltimaDirecao = Direcao.Parado;
|
||||
}
|
||||
else if (AnguloTipoMovimento != -1)
|
||||
{
|
||||
Angulo_SP = AnguloTipoMovimento;
|
||||
novoAngulo = AnguloTipoMovimento;
|
||||
}
|
||||
else
|
||||
{
|
||||
Angulo_SP = _Controle.Angulo;
|
||||
novoAngulo = _Controle.Angulo;
|
||||
}
|
||||
|
||||
if (Comandar)
|
||||
|
|
@ -353,8 +459,16 @@ namespace AgroBase.Models.Modules
|
|||
//Console.WriteLine($"[{Mod_ID}] Angulo Atualizado para {Variaveis.OperacaoEmAndamento.SimulacaoAnguloControle}");
|
||||
}
|
||||
|
||||
Angulo_SP = Math.Abs(Angulo_SP) * mx;
|
||||
Angulo_SP += AnguloFolgaCompensar;
|
||||
novoAngulo = Math.Abs(novoAngulo) * mx;
|
||||
novoAngulo += AnguloFolgaCompensar;
|
||||
|
||||
if (Math.Abs(novoAngulo - Angulo_SP) >= 0.05)
|
||||
{
|
||||
UltimaMudancaAnguloSP = DateTime.Now;
|
||||
}
|
||||
|
||||
Angulo_SP = novoAngulo;
|
||||
Sentido_SP = novoSentido;
|
||||
|
||||
if (_Simulando)
|
||||
{
|
||||
|
|
@ -473,9 +587,7 @@ namespace AgroBase.Models.Modules
|
|||
_sentido = _sentido == Sentido.Horario ? Sentido.Antihorario : Sentido.Horario;
|
||||
Variaveis.MostrarLog($"[DirecionalModel] [ReferenciaMotorRelativo] [DIR {Mod_ID}] - Invertendo Sentido de Giro para {_sentido.ToString()}");
|
||||
|
||||
double offset = Mod_ID == "DT" ? -2 : 0;
|
||||
|
||||
_anguloSP = CalcularCentroCompensado(AnguloEntrada, AnguloSaida, _sentido, offset);
|
||||
_anguloSP = CalcularCentroCompensado(AnguloEntrada, AnguloSaida, _sentido, OffsetReferenciamento);
|
||||
}
|
||||
|
||||
verificacoesAngulo++;
|
||||
|
|
|
|||
|
|
@ -494,6 +494,8 @@ namespace AgroBase.Models.Modules
|
|||
modulo.DirMotor.EnviarComandoControle(controleDir.UltimaDirecao);
|
||||
enviouControle = true;
|
||||
}
|
||||
|
||||
modulo.DirMotor.AtualizarSaudeSeguimentoAngular(agora);
|
||||
}
|
||||
|
||||
if (enviouControle)
|
||||
|
|
|
|||
|
|
@ -625,9 +625,9 @@ namespace AgroBase.Models.Modules
|
|||
posicao = CanMessagePosicaoDados.Dados1,
|
||||
indice = 0,
|
||||
analise_health = true,
|
||||
maximo = 5.2,
|
||||
minimo = 4.9,
|
||||
nominal = 5.0,
|
||||
maximo = 5.3,
|
||||
minimo = 4.75,
|
||||
nominal = 5.1,
|
||||
unidade_medida = "V"
|
||||
},
|
||||
new SensorValoresLeituraModel()
|
||||
|
|
@ -636,9 +636,9 @@ namespace AgroBase.Models.Modules
|
|||
posicao = CanMessagePosicaoDados.Dados1,
|
||||
indice = 1,
|
||||
analise_health = true,
|
||||
maximo = 1500,
|
||||
maximo = 800,
|
||||
minimo = 100,
|
||||
nominal = 280,
|
||||
nominal = 430,
|
||||
unidade_medida = "mA"
|
||||
},
|
||||
new SensorValoresLeituraModel()
|
||||
|
|
|
|||
|
|
@ -4094,6 +4094,21 @@ namespace AgroBase.Models
|
|||
|
||||
op.TempoIniciarOperacao = Math.Max(0, Convert.ToInt32(Math.Round(tempoAguardarInicio)));
|
||||
|
||||
|
||||
JObject emergenciaSistema = RedisService.GetField<JObject>(CtxKey.DadosOperacao, "emergencia_sistema");
|
||||
bool emergenciaSistemicaSolicitada = emergenciaSistema?["solicitada"]?.Value<bool>() ?? RedisService.GetField<bool>(CtxKey.DadosOperacao, "emergencia_sistema_solicitada", false);
|
||||
|
||||
bool emergenciaAnterior = Operacao.Emergencia;
|
||||
bool emergencia = emergenciaAnterior || emergenciaSistemicaSolicitada;
|
||||
|
||||
if (emergenciaSistemicaSolicitada && !emergenciaAnterior)
|
||||
{
|
||||
string codigo = emergenciaSistema?["codigo"]?.Value<string>() ?? "INTERTRAVAMENTO_SISTEMA";
|
||||
string motivo = emergenciaSistema?["motivo"]?.Value<string>() ?? "Intertravamento sistêmico solicitado";
|
||||
int severidade = emergenciaSistema?["severidade"]?.Value<int>() ?? 90;
|
||||
InserirLog(T_Code.Mov, StatusModulo.Falha, Math.Max(0, 100 - severidade), $"Emergência sistêmica acionada [{codigo}]: {motivo}");
|
||||
}
|
||||
|
||||
var operacaoAtualizada = new OperacaoSensoriamentoLogModel()
|
||||
{
|
||||
Momento = agora,
|
||||
|
|
@ -4102,7 +4117,7 @@ namespace AgroBase.Models
|
|||
Status = op.Status,
|
||||
StatusOperacaoAnterior = Operacao.StatusOperacaoAnterior,
|
||||
OperacaoIniciada = Operacao.OperacaoIniciada,
|
||||
Emergencia = Operacao.Emergencia,
|
||||
Emergencia = emergencia,
|
||||
Pausa = Operacao.Pausa,
|
||||
Calibrando = Operacao.Calibrando,
|
||||
Finalizando = Operacao.Finalizando,
|
||||
|
|
@ -4889,7 +4904,7 @@ namespace AgroBase.Models
|
|||
? (velocidade / velocidadeMax) * 100.0
|
||||
: 0;
|
||||
|
||||
double erroAngulo = dir != null ? CalcularErroAngulo(dir) : 0;
|
||||
double erroAngulo = dir?.ErroAngulo ?? 0;
|
||||
|
||||
return new DirecionalModuloLogModel
|
||||
{
|
||||
|
|
@ -4919,6 +4934,7 @@ namespace AgroBase.Models
|
|||
|
||||
ErroAngulo = Math.Round(erroAngulo, 2),
|
||||
ErroAnguloAbs = Math.Round(Math.Abs(erroAngulo), 2),
|
||||
ErroSeguimentoAngularPersistente = dir?.ErroSeguimentoAngularPersistente ?? false,
|
||||
AtingiuAnguloSP = dir?.AtingiuAnguloSP ?? false,
|
||||
RetornandoAzero = dir?.RetornandoAzero ?? false,
|
||||
|
||||
|
|
@ -4943,19 +4959,6 @@ namespace AgroBase.Models
|
|||
};
|
||||
}
|
||||
|
||||
double CalcularErroAngulo(DirecionalModel dir)
|
||||
{
|
||||
if (dir == null)
|
||||
return 0;
|
||||
|
||||
double anguloSp =
|
||||
!dir.Comandar || dir.Sentido_SP == Sentido.Parado
|
||||
? 0
|
||||
: dir.Angulo_SP;
|
||||
|
||||
return anguloSp - dir.AnguloLeitura;
|
||||
}
|
||||
|
||||
DirecionalStatusLogModel CriarStatus((bool, List<string>) status)
|
||||
{
|
||||
return new DirecionalStatusLogModel
|
||||
|
|
@ -5021,7 +5024,7 @@ namespace AgroBase.Models
|
|||
|
||||
model.AnguloMedioAbs = MediaOuZero(controlando, x => Math.Abs(x.DirMotor.AnguloLeitura));
|
||||
model.AnguloSPMedioAbs = MediaOuZero(controlando, x => Math.Abs(x.DirMotor.Angulo_SP));
|
||||
model.ErroAnguloMedioAbs = MediaOuZero(controlando, x => Math.Abs(CalcularErroAngulo(x.DirMotor)));
|
||||
model.ErroAnguloMedioAbs = MediaOuZero(controlando, x => Math.Abs(x.DirMotor.ErroAngulo));
|
||||
|
||||
model.Status =
|
||||
dados != null && controle != null
|
||||
|
|
@ -5999,11 +6002,9 @@ namespace AgroBase.Models
|
|||
|
||||
public DirecionalStatusLogModel Status { get; set; } = new DirecionalStatusLogModel();
|
||||
|
||||
public bool StatusGeralOk =>
|
||||
Disponivel &&
|
||||
Status.Ok &&
|
||||
ModulosInicializados > 0 &&
|
||||
!Modulos.Any(x => x.ErroAnguloAbs > 3.0 && x.Controlar);
|
||||
public bool StatusEstruturalOk => Disponivel && Status.Ok && ModulosInicializados > 0;
|
||||
|
||||
public bool StatusGeralOk => StatusEstruturalOk && !Modulos.Any(x => x.Controlar && x.ErroSeguimentoAngularPersistente);
|
||||
|
||||
public List<DirecionalModuloLogModel> Modulos { get; set; } = new List<DirecionalModuloLogModel>();
|
||||
|
||||
|
|
@ -6078,6 +6079,7 @@ namespace AgroBase.Models
|
|||
// Diagnóstico geométrico
|
||||
public double ErroAngulo { get; set; }
|
||||
public double ErroAnguloAbs { get; set; }
|
||||
public bool ErroSeguimentoAngularPersistente { get; set; }
|
||||
public bool AtingiuAnguloSP { get; set; }
|
||||
public bool RetornandoAzero { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -1228,6 +1228,8 @@ namespace AgroBase.Services
|
|||
short rpm = DecodeInt16BE(data, 1);
|
||||
|
||||
item.RPM.valor = rpm;
|
||||
|
||||
Variaveis.OperacaoEmAndamento?.DispMvd?.Dados?.Modulos?.FirstOrDefault(x => x.DirMotor._EnderecoCAN_Tx == item.EnderecoTx)?.DirMotor?.AtualizarEmMovimento(Math.Abs(rpm) > 1);
|
||||
}
|
||||
|
||||
private void ProcessarPulsosRecebidos(MKS057DModel item, byte[] data)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AgroBase.Models;
|
||||
using AgroBase.Models.Modules;
|
||||
using AgroBase.Models.Operadores;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
|
@ -788,15 +789,30 @@ namespace AgroBase.Services.Operadores
|
|||
})
|
||||
.ToList();
|
||||
|
||||
bool possuiComandoServo = servo.UltimoComandoEnviado != DateTime.MinValue;
|
||||
|
||||
DateTime ultimaRespostaAnguloServo =
|
||||
servo.ValoresLeituras?
|
||||
.Where(x =>x.funcao == FuncoesPinout.ServoAnguloLeitura)
|
||||
.OrderByDescending(x =>x.atual?.lidoEm ?? DateTime.MinValue)
|
||||
.FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue;
|
||||
|
||||
bool possuiRespostaServo = ultimaRespostaAnguloServo != DateTime.MinValue;
|
||||
|
||||
double idadeUltimoComandoServoMs =
|
||||
servo.UltimoComandoEnviado == DateTime.MinValue
|
||||
!possuiComandoServo
|
||||
? 999999
|
||||
: (agora_dt - servo.UltimoComandoEnviado).TotalMilliseconds;
|
||||
: Math.Max(0, (agora_dt - servo.UltimoComandoEnviado).TotalMilliseconds);
|
||||
|
||||
double idadeUltimaRespostaServoMs =
|
||||
servo.UltimaLeitura == DateTime.MinValue
|
||||
!possuiRespostaServo
|
||||
? 999999
|
||||
: (agora_dt - servo.UltimaLeitura).TotalMilliseconds;
|
||||
: Math.Max(0, (agora_dt - ultimaRespostaAnguloServo).TotalMilliseconds);
|
||||
|
||||
bool telemetriaPosComandoServo =
|
||||
possuiComandoServo &&
|
||||
possuiRespostaServo &&
|
||||
ultimaRespostaAnguloServo >= servo.UltimoComandoEnviado;
|
||||
|
||||
var servoObj = new
|
||||
{
|
||||
|
|
@ -806,15 +822,21 @@ namespace AgroBase.Services.Operadores
|
|||
tipo,
|
||||
controlar = servo.Controlar,
|
||||
mandatorio = servo.Mandatorio,
|
||||
|
||||
angulo_sp = servo._AnguloControle,
|
||||
angulo_desejado = servo.UltimoAnguloDesejado,
|
||||
angulo_leitura = servo._AnguloLeiutra,
|
||||
|
||||
possui_comando = possuiComandoServo,
|
||||
telemetria_pos_comando = telemetriaPosComandoServo,
|
||||
|
||||
ultimo_comando_ms = idadeUltimoComandoServoMs,
|
||||
|
||||
ultima_resposta_ms = idadeUltimaRespostaServoMs,
|
||||
|
||||
dados,
|
||||
timestamp = agora,
|
||||
saude = saude ?? new { },
|
||||
|
||||
angulo_desejado = servo.UltimoAnguloDesejado,
|
||||
angulo_leitura = servo._AnguloLeiutra,
|
||||
ultimo_comando_ms = idadeUltimoComandoServoMs,
|
||||
ultima_resposta_ms = idadeUltimaRespostaServoMs,
|
||||
};
|
||||
|
||||
senServosAtivos.Add($"{tipo}.{id}");
|
||||
|
|
@ -843,15 +865,30 @@ namespace AgroBase.Services.Operadores
|
|||
})
|
||||
.ToList();
|
||||
|
||||
bool possuiComandoRele = rele.UltimoComandoEnviado != DateTime.MinValue;
|
||||
bool possuiRespostaRele = rele.UltimaLeitura != DateTime.MinValue;
|
||||
double idadeUltimoComandoReleMs = !possuiComandoRele ? 999999 : Math.Max(0, (agora_dt - rele.UltimoComandoEnviado).TotalMilliseconds);
|
||||
double idadeUltimaRespostaReleMs = !possuiRespostaRele ? 999999 : Math.Max(0, (agora_dt - rele.UltimaLeitura).TotalMilliseconds);
|
||||
DateTime ultimaRespostaEstadoRele =
|
||||
rele.ValoresLeituras?
|
||||
.Where(x =>x.funcao == FuncoesPinout.EstadoLeitura)
|
||||
.OrderByDescending(x =>x.atual?.lidoEm ?? DateTime.MinValue)
|
||||
.FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue;
|
||||
|
||||
bool telemetriaPosComando =
|
||||
bool possuiComandoRele = rele.UltimoComandoEnviado != DateTime.MinValue;
|
||||
|
||||
bool possuiRespostaRele = ultimaRespostaEstadoRele != DateTime.MinValue;
|
||||
|
||||
double idadeUltimoComandoReleMs =
|
||||
!possuiComandoRele
|
||||
? 999999
|
||||
: Math.Max(0, (agora_dt - rele.UltimoComandoEnviado).TotalMilliseconds);
|
||||
|
||||
double idadeUltimaRespostaReleMs =
|
||||
!possuiRespostaRele
|
||||
? 999999
|
||||
: Math.Max(0, (agora_dt - ultimaRespostaEstadoRele).TotalMilliseconds);
|
||||
|
||||
bool telemetriaPosComandoRele =
|
||||
possuiComandoRele &&
|
||||
possuiRespostaRele &&
|
||||
rele.UltimaLeitura >= rele.UltimoComandoEnviado;
|
||||
ultimaRespostaEstadoRele >= rele.UltimoComandoEnviado;
|
||||
|
||||
var releObj = new
|
||||
{
|
||||
|
|
@ -870,7 +907,7 @@ namespace AgroBase.Services.Operadores
|
|||
estado_leitura = rele._EstadoLeitura,
|
||||
|
||||
possui_comando = possuiComandoRele,
|
||||
telemetria_pos_comando = telemetriaPosComando,
|
||||
telemetria_pos_comando = telemetriaPosComandoRele,
|
||||
|
||||
ultimo_comando_ms = idadeUltimoComandoReleMs,
|
||||
ultima_resposta_ms = idadeUltimaRespostaReleMs,
|
||||
|
|
@ -902,15 +939,50 @@ namespace AgroBase.Services.Operadores
|
|||
})
|
||||
.ToList();
|
||||
|
||||
DateTime ultimaRespostaLedId =
|
||||
led.ValoresLeituras?
|
||||
.Where(x =>x.funcao == FuncoesPinout.StatusLedID)
|
||||
.OrderByDescending(x => x.atual?.lidoEm ?? DateTime.MinValue)
|
||||
.FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue;
|
||||
|
||||
DateTime ultimaRespostaLedStatus =
|
||||
led.ValoresLeituras?
|
||||
.Where(x =>x.funcao == FuncoesPinout.StatusLedLeitura)
|
||||
.OrderByDescending(x =>x.atual?.lidoEm ?? DateTime.MinValue)
|
||||
.FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue;
|
||||
|
||||
bool possuiComandoLed = led.UltimoComandoEnviado != DateTime.MinValue;
|
||||
|
||||
bool possuiRespostaLed =
|
||||
ultimaRespostaLedId != DateTime.MinValue &&
|
||||
ultimaRespostaLedStatus != DateTime.MinValue;
|
||||
|
||||
/*
|
||||
* Usa a mais antiga das duas respostas.
|
||||
* Assim ultima_resposta_ms representa a idade do conjunto completo.
|
||||
*/
|
||||
DateTime ultimaRespostaLed =
|
||||
possuiRespostaLed
|
||||
? (ultimaRespostaLedId <= ultimaRespostaLedStatus ? ultimaRespostaLedId : ultimaRespostaLedStatus)
|
||||
: DateTime.MinValue;
|
||||
|
||||
double idadeUltimoComandoLedMs =
|
||||
led.UltimoComandoEnviado == DateTime.MinValue
|
||||
!possuiComandoLed
|
||||
? 999999
|
||||
: (agora_dt - led.UltimoComandoEnviado).TotalMilliseconds;
|
||||
: Math.Max(0, (agora_dt - led.UltimoComandoEnviado).TotalMilliseconds);
|
||||
|
||||
double idadeUltimaRespostaLedMs =
|
||||
led.UltimaLeitura == DateTime.MinValue
|
||||
!possuiRespostaLed
|
||||
? 999999
|
||||
: (agora_dt - led.UltimaLeitura).TotalMilliseconds;
|
||||
: Math.Max(0, (agora_dt - ultimaRespostaLed).TotalMilliseconds);
|
||||
|
||||
bool telemetriaPosComandoLed =
|
||||
possuiComandoLed &&
|
||||
possuiRespostaLed &&
|
||||
ultimaRespostaLedId >= led.UltimoComandoEnviado &&
|
||||
ultimaRespostaLedStatus >= led.UltimoComandoEnviado;
|
||||
|
||||
int statusLedSp = led.UltimoComportamentoDesejado != null ? (int)led.UltimoComportamentoDesejado.statusLED : -1;
|
||||
|
||||
var ledObj = new
|
||||
{
|
||||
|
|
@ -920,16 +992,24 @@ namespace AgroBase.Services.Operadores
|
|||
tipo,
|
||||
controlar = led.Controlar,
|
||||
mandatorio = led.Mandatorio,
|
||||
|
||||
status_led_id_sp = led.UltimoComportamentoDesejado?.id ?? -1,
|
||||
|
||||
status_led_sp = statusLedSp,
|
||||
|
||||
status_led_id = (int)led._StatusLedId,
|
||||
status_led_leitura = (int)led._StatusLed,
|
||||
|
||||
possui_comando = possuiComandoLed,
|
||||
telemetria_pos_comando = telemetriaPosComandoLed,
|
||||
|
||||
ultimo_comando_ms = idadeUltimoComandoLedMs,
|
||||
|
||||
ultima_resposta_ms = idadeUltimaRespostaLedMs,
|
||||
|
||||
dados,
|
||||
timestamp = agora,
|
||||
saude = saude ?? new { },
|
||||
|
||||
status_led_id_sp = led.UltimoComportamentoDesejado?.id ?? -1,
|
||||
status_led_sp = led.UltimoComportamentoDesejado?.statusLED,
|
||||
status_led_id = led._StatusLedId,
|
||||
status_led_leitura = led._StatusLed,
|
||||
ultimo_comando_ms = idadeUltimoComandoLedMs,
|
||||
ultima_resposta_ms = idadeUltimaRespostaLedMs,
|
||||
};
|
||||
|
||||
senLedsAtivos.Add($"{tipo}.{id}");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import json
|
|||
import os
|
||||
import time
|
||||
import threading
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
|
@ -15,6 +16,9 @@ from camera_worker.oak_fcc3_core.oak_fcc3_client import OakFcc3Client
|
|||
from camera_worker.camera_imu import IMUCamera
|
||||
|
||||
|
||||
CAMERA_MULTISPECTRAL_VERSION = "production_v1_2026_08_24"
|
||||
|
||||
|
||||
class CameraMultispectral:
|
||||
"""
|
||||
Adaptador do módulo OAK-FFC-3 multiespectral para o ecossistema do robô.
|
||||
|
|
@ -28,6 +32,9 @@ class CameraMultispectral:
|
|||
|
||||
A classe NÃO expõe lógica radiométrica/fusão/flatfield para o worker.
|
||||
Isso fica dentro do core OAK-FCC-3.
|
||||
|
||||
Em modo produto, hardware/resoluções/Bayer/target_size vêm exclusivamente
|
||||
do module_params homologado e do hardware validado pelo Manager/Client.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -41,6 +48,7 @@ class CameraMultispectral:
|
|||
target_size=None,
|
||||
cache_max_age_s=0.15,
|
||||
timeout_s=1.0,
|
||||
require_product_contract=False,
|
||||
):
|
||||
self.mostrar_log = mostrar_log
|
||||
self.mx_id = str(mx_id) if mx_id else None
|
||||
|
|
@ -61,11 +69,42 @@ class CameraMultispectral:
|
|||
self.rodando = False
|
||||
self.ultima_saude = {}
|
||||
|
||||
self.width = int(width)
|
||||
self.height = int(height)
|
||||
# width/height recebidos do caller são somente compatibilidade legado.
|
||||
# Em produto, resolução nativa vem do module_params/hardware real.
|
||||
self.legacy_width = int(width)
|
||||
self.legacy_height = int(height)
|
||||
self.requested_target_size = (
|
||||
None
|
||||
if target_size is None
|
||||
else [int(target_size[0]), int(target_size[1])]
|
||||
)
|
||||
|
||||
# Aliases históricos. São sobrescritos pelo contrato do Client e passam
|
||||
# a significar exclusivamente a resolução RGB nativa de referência.
|
||||
self.width = self.legacy_width
|
||||
self.height = self.legacy_height
|
||||
|
||||
self.fps = int(fps)
|
||||
self.target_size = target_size
|
||||
self.target_size = self.requested_target_size
|
||||
self.tensor_size = self.requested_target_size
|
||||
self.timeout_s = float(timeout_s)
|
||||
self.require_product_contract = bool(require_product_contract)
|
||||
|
||||
self.product_contract = False
|
||||
self.client_contract = {}
|
||||
self.sensor_size_by_role = {
|
||||
"rgb": [self.legacy_width, self.legacy_height],
|
||||
"re": [self.legacy_width, self.legacy_height],
|
||||
"nir": [self.legacy_width, self.legacy_height],
|
||||
}
|
||||
self.camera_hardware = {}
|
||||
self.rgb_native_size = [self.legacy_width, self.legacy_height]
|
||||
self.spectral_native_size = {
|
||||
"re": [self.legacy_width, self.legacy_height],
|
||||
"nir": [self.legacy_width, self.legacy_height],
|
||||
}
|
||||
self.bayer_pattern = None
|
||||
self.module_params_sha256 = None
|
||||
|
||||
self._cache_max_age_s = float(cache_max_age_s)
|
||||
self._lock = threading.Lock()
|
||||
|
|
@ -115,6 +154,9 @@ class CameraMultispectral:
|
|||
module_calibration_json = self._resolver_module_params_default()
|
||||
|
||||
self.module_calibration_json = module_calibration_json
|
||||
self.module_params_sha256 = self._sha256_file_safe(
|
||||
self.module_calibration_json
|
||||
)
|
||||
|
||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||
ContextoGlobalRedis.CamKey(self.mx_id),
|
||||
|
|
@ -137,8 +179,10 @@ class CameraMultispectral:
|
|||
try:
|
||||
self.client = OakFcc3Client(
|
||||
mx_id=self.mx_id,
|
||||
width=self.width,
|
||||
height=self.height,
|
||||
# Fallbacks legado. O Client produto ignora estes valores como
|
||||
# autoridade física e usa o module_params homologado.
|
||||
width=self.legacy_width,
|
||||
height=self.legacy_height,
|
||||
bayer="BGGR",
|
||||
fps=self.fps,
|
||||
frame_type="RAW_BRUTO",
|
||||
|
|
@ -146,15 +190,31 @@ class CameraMultispectral:
|
|||
capture_mode="TRIPLE",
|
||||
raw_policy="require_triple",
|
||||
module_calibration_json=self.module_calibration_json,
|
||||
sync_mode="best",
|
||||
sync_tolerance_ms=25.0,
|
||||
sync_mode="strict",
|
||||
hardware_sync_enabled=False,
|
||||
frame_sync_master="CAM_A",
|
||||
sync_tolerance_ms=15.0,
|
||||
|
||||
imu_modo=self.imu_modo,
|
||||
imu_freq_hz=self.imu_freq_hz,
|
||||
|
||||
# Runtime de campo: não executa a auditoria estatística
|
||||
# evaluate_frame_quality() em todo frame.
|
||||
# O tensor continua passando por todo o processamento científico
|
||||
# necessário (decode, radiometria, flat-field, fusão e resize).
|
||||
evaluate_quality=False,
|
||||
require_product_contract=self.require_product_contract,
|
||||
)
|
||||
|
||||
# O contrato já está disponível antes do start porque o Client
|
||||
# carrega/valida module_params no construtor.
|
||||
self._adotar_contrato_client()
|
||||
|
||||
resp = self.client.start(print_debug=False)
|
||||
|
||||
# Reconfirma depois do Manager abrir e validar o hardware físico.
|
||||
self._adotar_contrato_client()
|
||||
|
||||
# Se mx_id veio None, pega o ID real aberto pelo core antes de criar IMU.
|
||||
try:
|
||||
self.mx_id = str(getattr(self.client, "mx_id", None) or self.mx_id)
|
||||
|
|
@ -235,6 +295,11 @@ class CameraMultispectral:
|
|||
|
||||
self.mostrar_log(f"[CameraMultispectral] Erro ao iniciar módulo: {e}")
|
||||
|
||||
# Produto é fail-closed: o CameraManager precisa receber o motivo
|
||||
# real da falha, e não um objeto parcialmente inicializado.
|
||||
if self.require_product_contract:
|
||||
raise
|
||||
|
||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||
ContextoGlobalRedis.CamKey(self.mx_id),
|
||||
mx_id=self.mx_id,
|
||||
|
|
@ -267,40 +332,204 @@ class CameraMultispectral:
|
|||
# Devolve o default mesmo se não existir, para o erro ficar claro no core.
|
||||
return "calibration/module_params.json"
|
||||
|
||||
def _sha256_file_safe(self, path):
|
||||
try:
|
||||
if not path or not os.path.isfile(path):
|
||||
return None
|
||||
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
while True:
|
||||
chunk = f.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
h.update(chunk)
|
||||
|
||||
return h.hexdigest()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _normalize_size(value, field_name):
|
||||
if not isinstance(value, (list, tuple)) or len(value) != 2:
|
||||
raise RuntimeError(f"{field_name} inválido: {value!r}")
|
||||
|
||||
w = int(value[0])
|
||||
h = int(value[1])
|
||||
|
||||
if w <= 0 or h <= 0:
|
||||
raise RuntimeError(f"{field_name} inválido: {value!r}")
|
||||
|
||||
return [w, h]
|
||||
|
||||
def _adotar_contrato_client(self):
|
||||
if self.client is None:
|
||||
raise RuntimeError("OakFcc3Client não inicializado")
|
||||
|
||||
getter = getattr(self.client, "get_contract", None)
|
||||
if not callable(getter):
|
||||
if self.require_product_contract:
|
||||
raise RuntimeError(
|
||||
"OakFcc3Client sem get_contract(); versão produto obrigatória."
|
||||
)
|
||||
return
|
||||
|
||||
contract = getter() or {}
|
||||
if not isinstance(contract, dict):
|
||||
raise RuntimeError(
|
||||
f"Contrato do OakFcc3Client inválido: {type(contract)}"
|
||||
)
|
||||
|
||||
product = bool(contract.get("product_contract", False))
|
||||
|
||||
if self.require_product_contract and not product:
|
||||
raise RuntimeError(
|
||||
"CameraMultispectral exige module_params de produção homologado."
|
||||
)
|
||||
|
||||
sizes_raw = contract.get("sensor_size_by_role") or {}
|
||||
sizes = {}
|
||||
|
||||
for role in ("rgb", "re", "nir"):
|
||||
if role not in sizes_raw:
|
||||
if self.require_product_contract:
|
||||
raise RuntimeError(
|
||||
f"Contrato do Client sem sensor_size_by_role.{role}"
|
||||
)
|
||||
continue
|
||||
|
||||
sizes[role] = self._normalize_size(
|
||||
sizes_raw[role],
|
||||
f"sensor_size_by_role.{role}",
|
||||
)
|
||||
|
||||
if "rgb" in sizes:
|
||||
self.rgb_native_size = list(sizes["rgb"])
|
||||
self.width = int(self.rgb_native_size[0])
|
||||
self.height = int(self.rgb_native_size[1])
|
||||
|
||||
if "re" in sizes:
|
||||
self.spectral_native_size["re"] = list(sizes["re"])
|
||||
|
||||
if "nir" in sizes:
|
||||
self.spectral_native_size["nir"] = list(sizes["nir"])
|
||||
|
||||
if sizes:
|
||||
self.sensor_size_by_role = {
|
||||
role: list(size)
|
||||
for role, size in sizes.items()
|
||||
}
|
||||
|
||||
camera_hardware = contract.get("camera_hardware") or {}
|
||||
if isinstance(camera_hardware, dict):
|
||||
self.camera_hardware = json.loads(
|
||||
json.dumps(camera_hardware, default=str)
|
||||
)
|
||||
|
||||
bayer = contract.get("bayer_pattern")
|
||||
if bayer:
|
||||
bayer = str(bayer).upper()
|
||||
if bayer not in ("RGGB", "BGGR", "GRBG", "GBRG"):
|
||||
raise RuntimeError(f"Bayer inválido no contrato: {bayer!r}")
|
||||
self.bayer_pattern = bayer
|
||||
|
||||
default_target = contract.get("default_target_size")
|
||||
if default_target is not None:
|
||||
default_target = self._normalize_size(
|
||||
default_target,
|
||||
"client.default_target_size",
|
||||
)
|
||||
|
||||
if (
|
||||
self.requested_target_size is not None
|
||||
and default_target is not None
|
||||
and list(self.requested_target_size) != list(default_target)
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Resolução IA divergente do module_params: "
|
||||
f"caller={self.requested_target_size}, "
|
||||
f"module_params={default_target}"
|
||||
)
|
||||
|
||||
self.target_size = (
|
||||
list(default_target)
|
||||
if default_target is not None
|
||||
else (
|
||||
list(self.requested_target_size)
|
||||
if self.requested_target_size is not None
|
||||
else list(self.rgb_native_size)
|
||||
)
|
||||
)
|
||||
self.tensor_size = list(self.target_size)
|
||||
self.product_contract = product
|
||||
self.client_contract = json.loads(
|
||||
json.dumps(contract, default=str)
|
||||
)
|
||||
|
||||
def _montar_parametros(self, status=None, start_resp=None):
|
||||
status = status or {}
|
||||
|
||||
altura_camera = 111.0
|
||||
|
||||
# Valores medidos por você anteriormente:
|
||||
# FOV prático: 96 x 157 cm a 111 cm de altura.
|
||||
# FOV prático histórico medido no conjunto mecânico.
|
||||
# É mantido como referência operacional, mas não como calibração óptica.
|
||||
largura_real = 157.0
|
||||
altura_real = 96.0
|
||||
|
||||
cm_por_px_x = largura_real / float(self.width)
|
||||
cm_por_px_y = altura_real / float(self.height)
|
||||
tensor_w = int(self.tensor_size[0])
|
||||
tensor_h = int(self.tensor_size[1])
|
||||
|
||||
# Para o worker, pixel operacional significa pixel do tensor entregue à IA,
|
||||
# não pixel do sensor bruto. A homografia/crop pode reduzir discretamente
|
||||
# a FOV real, então estes valores continuam sendo NOMINAIS.
|
||||
cm_por_px_x = largura_real / float(tensor_w)
|
||||
cm_por_px_y = altura_real / float(tensor_h)
|
||||
|
||||
parametros = {
|
||||
"tipo": "multispectral",
|
||||
"adapter_version": CAMERA_MULTISPECTRAL_VERSION,
|
||||
"product_contract": bool(self.product_contract),
|
||||
"module_calibration_json": self.module_calibration_json,
|
||||
"module_params_sha256": self.module_params_sha256,
|
||||
"frame_type": "RAW_BRUTO",
|
||||
"channels": ["R", "G", "B", "RE", "NIR"],
|
||||
"output_layout": "CHW",
|
||||
"dtype": "float32",
|
||||
"tensor_min": 0.0,
|
||||
"tensor_max": 1.0,
|
||||
"rgb_width": self.width,
|
||||
"rgb_height": self.height,
|
||||
"tensor_width": self.width if self.target_size is None else int(self.target_size[0]),
|
||||
"tensor_height": self.height if self.target_size is None else int(self.target_size[1]),
|
||||
|
||||
"reference_camera": "rgb",
|
||||
"rgb_native_size": list(self.rgb_native_size),
|
||||
"sensor_size_by_role": {
|
||||
role: list(size)
|
||||
for role, size in self.sensor_size_by_role.items()
|
||||
},
|
||||
"camera_hardware": json.loads(
|
||||
json.dumps(self.camera_hardware, default=str)
|
||||
),
|
||||
"bayer_pattern": self.bayer_pattern,
|
||||
|
||||
# Aliases compatíveis, agora explicitamente RGB nativo.
|
||||
"rgb_width": int(self.rgb_native_size[0]),
|
||||
"rgb_height": int(self.rgb_native_size[1]),
|
||||
|
||||
"tensor_size": list(self.tensor_size),
|
||||
"tensor_width": tensor_w,
|
||||
"tensor_height": tensor_h,
|
||||
"fps": self.fps,
|
||||
|
||||
"altura_camera": altura_camera,
|
||||
"largura_real": largura_real,
|
||||
"altura_real": altura_real,
|
||||
"cm_por_px_x": cm_por_px_x,
|
||||
"cm_por_px_y": cm_por_px_y,
|
||||
"cm_por_px_nominal": True,
|
||||
|
||||
"status": status,
|
||||
"start_resp": start_resp,
|
||||
"client_contract": json.loads(
|
||||
json.dumps(self.client_contract, default=str)
|
||||
),
|
||||
}
|
||||
|
||||
return parametros
|
||||
|
|
@ -557,6 +786,15 @@ class CameraMultispectral:
|
|||
f"Tensor multiespectral deveria ter 5 canais, veio shape={tensor.shape}"
|
||||
)
|
||||
|
||||
if self.target_size is not None:
|
||||
expected_w = int(self.target_size[0])
|
||||
expected_h = int(self.target_size[1])
|
||||
if tuple(tensor.shape[1:]) != (expected_h, expected_w):
|
||||
raise RuntimeError(
|
||||
"Tensor final diverge do contrato de resolução: "
|
||||
f"shape={tensor.shape}, esperado=(5,{expected_h},{expected_w})"
|
||||
)
|
||||
|
||||
t_validate_ms = (time.perf_counter() - t0) * 1000.0
|
||||
|
||||
# ============================================================
|
||||
|
|
@ -595,6 +833,13 @@ class CameraMultispectral:
|
|||
"shape": list(tensor.shape),
|
||||
"dtype": str(tensor.dtype),
|
||||
"channels": ["R", "G", "B", "RE", "NIR"],
|
||||
"product_contract": bool(self.product_contract),
|
||||
"tensor_size": list(self.tensor_size),
|
||||
"sensor_size_by_role": {
|
||||
role: list(size)
|
||||
for role, size in self.sensor_size_by_role.items()
|
||||
},
|
||||
"bayer_pattern": self.bayer_pattern,
|
||||
|
||||
"frame_id": int(meta.get("frame_id", 0) or 0),
|
||||
"async_packet_seq": int(
|
||||
|
|
@ -615,6 +860,22 @@ class CameraMultispectral:
|
|||
},
|
||||
}
|
||||
|
||||
#capture_perf = dict(meta.get("capture_perf", {}) or {})
|
||||
#agora_log = time.monotonic()
|
||||
#if not hasattr(self, "_ultimo_log_sync"):
|
||||
# self._ultimo_log_sync = 0.0
|
||||
#if agora_log - self._ultimo_log_sync >= 1.0:
|
||||
# print(
|
||||
# "[OAK_SYNC] "
|
||||
# f"frame={meta.get('frame_id')} | "
|
||||
# f"dt={float(meta.get('sync_dt_ms', 0.0) or 0.0):.3f}ms | "
|
||||
# f"ok={bool(meta.get('sync_ok', False))} | "
|
||||
# f"tol={float(meta.get('sync_tolerance_ms', 0.0) or 0.0):.3f}ms | "
|
||||
# f"seq={capture_perf.get('selected_seq_by_cam')} | "
|
||||
# f"ts={capture_perf.get('selected_ts_by_cam')}"
|
||||
# )
|
||||
# self._ultimo_log_sync = agora_log
|
||||
|
||||
with self._lock:
|
||||
self.ultimo_tensor_multispec = tensor
|
||||
self.ultimo_meta = meta
|
||||
|
|
@ -674,7 +935,10 @@ class CameraMultispectral:
|
|||
|
||||
def requisitar_frame_rgb(self, force: bool = False, max_age_s: float = None):
|
||||
"""
|
||||
Retorna preview BGR uint8 do tensor multiespectral.
|
||||
Retorna preview BGR uint8 gerado sob demanda a partir do Raw5 cacheado.
|
||||
|
||||
O hot path de inferência não paga custo de preview. O preview só é
|
||||
construído quando stream/debug realmente pede um frame RGB.
|
||||
"""
|
||||
try:
|
||||
agora = time.time()
|
||||
|
|
@ -682,27 +946,58 @@ class CameraMultispectral:
|
|||
max_age_s = self._cache_max_age_s
|
||||
|
||||
with self._lock:
|
||||
cache_ok = (
|
||||
preview_ok = (
|
||||
self.ultimo_frame_rgb is not None
|
||||
and self.timestamp_ultimo_frame_rgb is not None
|
||||
and (agora - self.timestamp_ultimo_frame_rgb) < max_age_s
|
||||
)
|
||||
|
||||
if cache_ok and not force:
|
||||
if preview_ok and not force:
|
||||
return self.ultimo_frame_rgb, dict(self._ultimo_resultado_rgb)
|
||||
|
||||
# Atualiza tensor, que por consequência atualiza preview RGB.
|
||||
tensor, res = self.requisitar_tensor_multispec(force=force, max_age_s=max_age_s)
|
||||
tensor_cache = self.ultimo_tensor_multispec
|
||||
ts_tensor = self.timestamp_ultimo_tensor
|
||||
|
||||
tensor_fresco = (
|
||||
tensor_cache is not None
|
||||
and ts_tensor is not None
|
||||
and (agora - ts_tensor) < max_age_s
|
||||
)
|
||||
|
||||
if not tensor_fresco or force:
|
||||
tensor_cache, res = self.requisitar_tensor_multispec(
|
||||
force=force,
|
||||
max_age_s=max_age_s,
|
||||
)
|
||||
|
||||
if tensor_cache is None:
|
||||
return None, {
|
||||
"erro": res.get("erro") or "tensor indisponível para preview",
|
||||
"duracao": res.get("duracao", 0.0),
|
||||
"frame_valido": False,
|
||||
}
|
||||
|
||||
t0 = time.perf_counter()
|
||||
preview = self._tensor_to_preview_bgr(tensor_cache)
|
||||
dur = time.perf_counter() - t0
|
||||
|
||||
if preview is None or preview.size <= 0:
|
||||
raise RuntimeError("Falha ao gerar preview RGB a partir do Raw5.")
|
||||
|
||||
resultado = {
|
||||
"erro": None,
|
||||
"duracao": dur,
|
||||
"frame_valido": True,
|
||||
"origem": "raw5_cache_on_demand",
|
||||
"shape": list(preview.shape),
|
||||
}
|
||||
|
||||
with self._lock:
|
||||
if self.ultimo_frame_rgb is not None:
|
||||
return self.ultimo_frame_rgb, dict(self._ultimo_resultado_rgb)
|
||||
self.ultimo_frame_rgb = preview
|
||||
self.timestamp_ultimo_frame_rgb = time.time()
|
||||
self._ultimo_resultado_rgb = resultado
|
||||
|
||||
return None, {
|
||||
"erro": res.get("erro") or "preview RGB indisponível",
|
||||
"duracao": res.get("duracao", 0.0),
|
||||
"frame_valido": False,
|
||||
}
|
||||
return preview, resultado
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[CameraMultispectral] Erro ao requisitar RGB: {e}")
|
||||
|
|
@ -1341,8 +1636,12 @@ class CameraMultispectral:
|
|||
core = getattr(self.client, "core", None)
|
||||
if core is not None and hasattr(core, "fuse_multispec_cameras"):
|
||||
t0 = time.perf_counter()
|
||||
out = core.fuse_multispec_cameras(decoded_fixed, meta, 5)
|
||||
out = core.resize_tensor_chw(out, target_size=self.target_size)
|
||||
out = core.fuse_multispec_cameras(
|
||||
decoded_fixed,
|
||||
meta,
|
||||
5,
|
||||
target_size=self.target_size,
|
||||
)
|
||||
|
||||
#t_core_build_ms = (time.perf_counter() - t0) * 1000.0
|
||||
#t_total_ms = (time.perf_counter() - t_total0) * 1000.0
|
||||
|
|
@ -1407,6 +1706,16 @@ class CameraMultispectral:
|
|||
meta_item.setdefault("role", role)
|
||||
meta_item.setdefault("socket", info.get("socket", cam_id))
|
||||
meta_item.setdefault("sensor", info.get("sensor"))
|
||||
meta_item.setdefault("width", info.get("width"))
|
||||
meta_item.setdefault("height", info.get("height"))
|
||||
meta_item.setdefault("native_size", info.get("native_size"))
|
||||
meta_item.setdefault("bit_depth", info.get("bit_depth"))
|
||||
meta_item.setdefault("raw_format", info.get("raw_format"))
|
||||
if role == "rgb":
|
||||
meta_item.setdefault(
|
||||
"bayer_pattern",
|
||||
info.get("bayer_pattern") or self.bayer_pattern,
|
||||
)
|
||||
fixed["meta"] = meta_item
|
||||
|
||||
out[cam_id] = fixed
|
||||
|
|
@ -1423,6 +1732,16 @@ class CameraMultispectral:
|
|||
"role": role,
|
||||
"socket": info.get("socket", cam_id),
|
||||
"sensor": info.get("sensor"),
|
||||
"width": info.get("width"),
|
||||
"height": info.get("height"),
|
||||
"native_size": info.get("native_size"),
|
||||
"bit_depth": info.get("bit_depth"),
|
||||
"raw_format": info.get("raw_format"),
|
||||
"bayer_pattern": (
|
||||
info.get("bayer_pattern") or self.bayer_pattern
|
||||
if role == "rgb"
|
||||
else None
|
||||
),
|
||||
"timestamp": (meta.get("timestamps") or {}).get(cam_id) if isinstance(meta, dict) else None,
|
||||
"shape": list(item.shape),
|
||||
"dtype": str(item.dtype),
|
||||
|
|
@ -1441,6 +1760,28 @@ class CameraMultispectral:
|
|||
# Salvamento científico / pós-processamento
|
||||
# ============================================================
|
||||
|
||||
def _processing_diagnostics_json_safe(self):
|
||||
core = getattr(self.client, "core", None) if self.client is not None else None
|
||||
|
||||
if core is None:
|
||||
return None
|
||||
|
||||
payload = {
|
||||
"fusion_result": getattr(core, "last_fusion_result", None),
|
||||
"radiometric_normalization_result": getattr(
|
||||
core,
|
||||
"last_radiometric_normalization_result",
|
||||
None,
|
||||
),
|
||||
"flatfield_result": getattr(core, "last_flatfield_result", None),
|
||||
"frame_quality": getattr(core, "last_frame_quality_result", None),
|
||||
}
|
||||
|
||||
try:
|
||||
return json.loads(json.dumps(payload, default=str))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _ts_name(self) -> str:
|
||||
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
|
||||
|
||||
|
|
@ -1466,6 +1807,20 @@ class CameraMultispectral:
|
|||
with self._lock:
|
||||
raw_meta = dict(self.ultimo_meta or {})
|
||||
|
||||
# Reforça o contrato científico no próprio stream_meta salvo.
|
||||
raw_meta.setdefault("frame_type", "RAW_BRUTO")
|
||||
raw_meta.setdefault("reference_camera", "rgb")
|
||||
raw_meta.setdefault("bayer_pattern", self.bayer_pattern)
|
||||
raw_meta.setdefault(
|
||||
"sensor_size_by_role",
|
||||
{role: list(size) for role, size in self.sensor_size_by_role.items()},
|
||||
)
|
||||
raw_meta.setdefault(
|
||||
"camera_hardware",
|
||||
json.loads(json.dumps(self.camera_hardware, default=str)),
|
||||
)
|
||||
raw_meta.setdefault("product_contract", bool(self.product_contract))
|
||||
|
||||
required = {"CAM_A", "CAM_B", "CAM_C"}
|
||||
presentes = set(raw_frame.keys())
|
||||
faltando = sorted(required - presentes)
|
||||
|
|
@ -1526,9 +1881,9 @@ class CameraMultispectral:
|
|||
preview = self.client.build_save_preview_from_cam_a(
|
||||
packed_raw_by_camera=raw_frame,
|
||||
meta_stream=raw_meta,
|
||||
sensor_width=self.width,
|
||||
sensor_height=self.height,
|
||||
bayer_pattern="BGGR",
|
||||
sensor_width=int(self.rgb_native_size[0]),
|
||||
sensor_height=int(self.rgb_native_size[1]),
|
||||
bayer_pattern=self.bayer_pattern,
|
||||
)
|
||||
|
||||
if preview is not None:
|
||||
|
|
@ -1641,9 +1996,22 @@ class CameraMultispectral:
|
|||
"mx_id": self.mx_id,
|
||||
"module_calibration_json": self.module_calibration_json,
|
||||
|
||||
"sensor_width": self.width,
|
||||
"sensor_height": self.height,
|
||||
"bayer_pattern": "BGGR",
|
||||
# Compatibilidade root: sensor_width/height representam CAM_A/RGB.
|
||||
"reference_camera": "rgb",
|
||||
"sensor_width": int(self.rgb_native_size[0]),
|
||||
"sensor_height": int(self.rgb_native_size[1]),
|
||||
"sensor_size_by_role": {
|
||||
role: list(size)
|
||||
for role, size in self.sensor_size_by_role.items()
|
||||
},
|
||||
"camera_hardware": json.loads(
|
||||
json.dumps(self.camera_hardware, default=str)
|
||||
),
|
||||
"bayer_pattern": self.bayer_pattern,
|
||||
"tensor_size": list(self.tensor_size),
|
||||
"product_contract": bool(self.product_contract),
|
||||
"camera_adapter_version": CAMERA_MULTISPECTRAL_VERSION,
|
||||
"module_params_sha256": self.module_params_sha256,
|
||||
"fps_target": self.fps,
|
||||
|
||||
"frame_type": "RAW_BRUTO",
|
||||
|
|
@ -1662,6 +2030,10 @@ class CameraMultispectral:
|
|||
"stream_meta": raw_meta,
|
||||
"actual_camera_controls": self.client.get_current_camera_controls() if self.client else None,
|
||||
"radiometric_last_result": self.client.get_radiometric_last_result() if self.client else None,
|
||||
"client_contract": json.loads(
|
||||
json.dumps(self.client_contract, default=str)
|
||||
),
|
||||
"processing_diagnostics": self._processing_diagnostics_json_safe(),
|
||||
|
||||
"resultado": resultado,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ from .radiometric_controller import RadiometricController
|
|||
|
||||
PHYSICAL_CHANNEL_NAMES = ("R", "G", "B", "RE", "NIR")
|
||||
PHYSICAL_CHANNEL_COUNT = len(PHYSICAL_CHANNEL_NAMES)
|
||||
OAK_FCC3_CLIENT_VERSION = "production_v1_2026_08_24"
|
||||
PRODUCT_SCHEMA = "multispec_module_params_v3"
|
||||
ASSEMBLY_SCHEMA = "multispec_module_params_assembly_v1"
|
||||
|
||||
|
||||
class OakFcc3Client:
|
||||
|
|
@ -46,58 +49,105 @@ class OakFcc3Client:
|
|||
mx_id=None,
|
||||
imu_modo="rotation_vector",
|
||||
imu_freq_hz=200,
|
||||
evaluate_quality=True,
|
||||
require_product_contract=False,
|
||||
**kwargs,
|
||||
):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bayer = bayer
|
||||
self.fps = fps
|
||||
self.frame_type = frame_type
|
||||
self.output_dtype = output_dtype
|
||||
self.capture_mode = capture_mode
|
||||
self.raw_policy = raw_policy
|
||||
# width/height/bayer recebidos do caller ficam apenas como fallback legado.
|
||||
# Em module_params de produção, hardware real + MP são a autoridade.
|
||||
self.legacy_width = int(width)
|
||||
self.legacy_height = int(height)
|
||||
self.legacy_bayer = str(bayer or "BGGR").upper()
|
||||
|
||||
self.fps = float(fps)
|
||||
self.frame_type = str(frame_type).upper()
|
||||
self.output_dtype = str(output_dtype).lower()
|
||||
self.capture_mode = str(capture_mode).upper()
|
||||
self.raw_policy = str(raw_policy).lower()
|
||||
self.module_calibration_json = module_calibration_json
|
||||
self.module_params = self._load_module_params(module_calibration_json)
|
||||
self.require_product_contract = bool(require_product_contract)
|
||||
|
||||
self.module_params = self._load_module_params(
|
||||
module_calibration_json,
|
||||
required=self.require_product_contract,
|
||||
)
|
||||
self.product_contract = self._is_product_module_params(self.module_params)
|
||||
|
||||
if self.require_product_contract and not self.product_contract:
|
||||
raise RuntimeError(
|
||||
"Contrato de produção obrigatório, mas module_params não foi "
|
||||
"gerado pelo assembler oficial."
|
||||
)
|
||||
|
||||
self.fusion_config = self.module_params.get("fusion_config", {}) or {}
|
||||
self.camera_hardware = self._resolve_camera_hardware()
|
||||
self.sensor_size_by_role = self._resolve_sensor_size_by_role()
|
||||
|
||||
rgb_size = self.sensor_size_by_role.get(
|
||||
"rgb",
|
||||
[self.legacy_width, self.legacy_height],
|
||||
)
|
||||
self.rgb_native_width = int(rgb_size[0])
|
||||
self.rgb_native_height = int(rgb_size[1])
|
||||
|
||||
# Mantidos por compatibilidade, mas agora significam RGB de referência.
|
||||
self.width = self.rgb_native_width
|
||||
self.height = self.rgb_native_height
|
||||
|
||||
self.bayer = self._resolve_bayer_pattern()
|
||||
self.default_target_size = self._resolve_default_target_size()
|
||||
|
||||
self.imu_modo = str(imu_modo).strip().lower()
|
||||
self.imu_freq_hz = int(imu_freq_hz)
|
||||
self.evaluate_quality = bool(evaluate_quality)
|
||||
|
||||
self.mx_id = str(mx_id) if mx_id else None
|
||||
|
||||
# Validação antecipada do modo produto. O Manager também repete estes
|
||||
# checks antes de abrir o hardware, de propósito.
|
||||
self._validate_static_product_contract()
|
||||
|
||||
self.svc = OakFcc3Service(
|
||||
timeout=10,
|
||||
fps=fps,
|
||||
width=width,
|
||||
height=height,
|
||||
frame_type=frame_type,
|
||||
output_dtype=output_dtype,
|
||||
capture_mode=capture_mode,
|
||||
raw_policy=raw_policy,
|
||||
fps=self.fps,
|
||||
width=self.width,
|
||||
height=self.height,
|
||||
frame_type=self.frame_type,
|
||||
output_dtype=self.output_dtype,
|
||||
capture_mode=self.capture_mode,
|
||||
raw_policy=self.raw_policy,
|
||||
sync_mode=sync_mode,
|
||||
sync_tolerance_ms=sync_tolerance_ms,
|
||||
mx_id=self.mx_id,
|
||||
module_calibration_json=module_calibration_json,
|
||||
|
||||
module_params=self.module_params,
|
||||
require_product_contract=self.require_product_contract,
|
||||
imu_modo=self.imu_modo,
|
||||
imu_freq_hz=self.imu_freq_hz,
|
||||
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.applied_camera_controls = {}
|
||||
self.radiometric_controller = None
|
||||
self.radiometric_controller_enabled = False
|
||||
|
||||
self.core = RawProcessorCore(
|
||||
sensor_width=width,
|
||||
sensor_height=height,
|
||||
bayer_pattern=bayer,
|
||||
sensor_width=self.rgb_native_width,
|
||||
sensor_height=self.rgb_native_height,
|
||||
bayer_pattern=self.bayer,
|
||||
calibration_json_path=module_calibration_json,
|
||||
)
|
||||
|
||||
# Preview é apenas visual, porém também precisa usar o Bayer/raster
|
||||
# reais do RGB para não mentir sobre a AR0234.
|
||||
self.preview = RawProcessorPreview(
|
||||
sensor_width=width,
|
||||
sensor_height=height,
|
||||
bayer_pattern=bayer,
|
||||
sensor_width=self.rgb_native_width,
|
||||
sensor_height=self.rgb_native_height,
|
||||
bayer_pattern=self.bayer,
|
||||
)
|
||||
self._preview_cache = {
|
||||
(self.rgb_native_width, self.rgb_native_height, self.bayer): self.preview,
|
||||
}
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
|
|
@ -106,19 +156,247 @@ class OakFcc3Client:
|
|||
def __exit__(self, exc_type, exc, tb):
|
||||
self.stop()
|
||||
|
||||
def _load_module_params(self, path):
|
||||
if not path or not os.path.isfile(path):
|
||||
def _load_module_params(self, path, required=False):
|
||||
if not path:
|
||||
if required:
|
||||
raise FileNotFoundError(
|
||||
"module_params obrigatório no contrato de produção."
|
||||
)
|
||||
return {}
|
||||
|
||||
if not os.path.isfile(path):
|
||||
if required:
|
||||
raise FileNotFoundError(
|
||||
f"module_params não encontrado: {path}"
|
||||
)
|
||||
return {}
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
data = json.load(f)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError(
|
||||
f"module_params root deve ser dict/object: {path}"
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _is_product_module_params(module_params):
|
||||
mp = module_params or {}
|
||||
assembly = mp.get("assembly_metadata", {}) or {}
|
||||
return bool(
|
||||
mp.get("schema") == PRODUCT_SCHEMA
|
||||
and assembly.get("schema") == ASSEMBLY_SCHEMA
|
||||
and isinstance(mp.get("camera_hardware"), dict)
|
||||
and isinstance(mp.get("sensor_size_by_role"), dict)
|
||||
and isinstance(mp.get("calibration_provenance"), dict)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_size(value, label):
|
||||
if not (isinstance(value, (list, tuple)) and len(value) == 2):
|
||||
raise RuntimeError(f"{label} deve ser [W,H], recebido={value!r}")
|
||||
|
||||
w = int(value[0])
|
||||
h = int(value[1])
|
||||
if w <= 0 or h <= 0:
|
||||
raise RuntimeError(f"{label} inválido: {value!r}")
|
||||
return [w, h]
|
||||
|
||||
def _resolve_camera_hardware(self):
|
||||
raw = self.module_params.get("camera_hardware", {}) or {}
|
||||
|
||||
if not self.product_contract:
|
||||
return {}
|
||||
|
||||
out = {}
|
||||
expected = {
|
||||
"rgb": ("CAM_A", ("OV9782", "AR0234")),
|
||||
"re": ("CAM_B", ("OV9282",)),
|
||||
"nir": ("CAM_C", ("OV9282",)),
|
||||
}
|
||||
|
||||
for role, (expected_socket, allowed_sensors) in expected.items():
|
||||
item = raw.get(role)
|
||||
if not isinstance(item, dict):
|
||||
raise RuntimeError(f"camera_hardware sem role={role}")
|
||||
|
||||
socket = str(item.get("socket") or item.get("socket_name") or "").upper()
|
||||
sensor = str(item.get("sensor") or item.get("sensor_name") or "").upper()
|
||||
size = self._normalize_size(item.get("size"), f"camera_hardware.{role}.size")
|
||||
|
||||
if socket != expected_socket:
|
||||
raise RuntimeError(
|
||||
f"camera_hardware.{role}.socket={socket!r}, esperado={expected_socket!r}"
|
||||
)
|
||||
|
||||
if sensor not in allowed_sensors:
|
||||
raise RuntimeError(
|
||||
f"camera_hardware.{role}.sensor={sensor!r}, permitidos={allowed_sensors}"
|
||||
)
|
||||
|
||||
out[role] = {
|
||||
"role": role,
|
||||
"socket": socket,
|
||||
"sensor": sensor,
|
||||
"size": size,
|
||||
}
|
||||
|
||||
return out
|
||||
|
||||
def _resolve_sensor_size_by_role(self):
|
||||
if not self.product_contract:
|
||||
return {
|
||||
"rgb": [self.legacy_width, self.legacy_height],
|
||||
"re": [self.legacy_width, self.legacy_height],
|
||||
"nir": [self.legacy_width, self.legacy_height],
|
||||
}
|
||||
|
||||
raw = self.module_params.get("sensor_size_by_role", {}) or {}
|
||||
out = {}
|
||||
|
||||
for role in ("rgb", "re", "nir"):
|
||||
size = self._normalize_size(
|
||||
raw.get(role),
|
||||
f"sensor_size_by_role.{role}",
|
||||
)
|
||||
expected = self.camera_hardware[role]["size"]
|
||||
if size != expected:
|
||||
raise RuntimeError(
|
||||
f"sensor_size_by_role.{role}={size} != camera_hardware={expected}"
|
||||
)
|
||||
out[role] = size
|
||||
|
||||
return out
|
||||
|
||||
def _resolve_bayer_pattern(self):
|
||||
value = self.module_params.get("bayer_pattern") if self.product_contract else None
|
||||
bayer = str(value or self.legacy_bayer).upper()
|
||||
|
||||
if bayer not in ("RGGB", "BGGR", "GRBG", "GBRG"):
|
||||
raise RuntimeError(f"bayer_pattern inválido: {bayer!r}")
|
||||
|
||||
return bayer
|
||||
|
||||
def _resolve_default_target_size(self):
|
||||
value = (self.fusion_config or {}).get("target_size")
|
||||
if value is None:
|
||||
return None
|
||||
return self._normalize_size(value, "fusion_config.target_size")
|
||||
|
||||
def _resolve_target_size(self, target_size):
|
||||
value = target_size if target_size is not None else self.default_target_size
|
||||
if value is None:
|
||||
return None
|
||||
size = self._normalize_size(value, "target_size")
|
||||
|
||||
if self.product_contract and self.default_target_size is not None:
|
||||
if size != self.default_target_size:
|
||||
raise RuntimeError(
|
||||
"target_size solicitado diverge do module_params homologado: "
|
||||
f"requested={size}, calibrated_runtime={self.default_target_size}"
|
||||
)
|
||||
|
||||
return size
|
||||
|
||||
def _validate_static_product_contract(self):
|
||||
if not self.product_contract:
|
||||
return
|
||||
|
||||
if self.frame_type != "RAW_BRUTO":
|
||||
raise RuntimeError(
|
||||
"OakFcc3Client de produção aceita somente frame_type='RAW_BRUTO'."
|
||||
)
|
||||
|
||||
if self.raw_policy != "require_triple":
|
||||
raise RuntimeError(
|
||||
"OakFcc3Client de produção exige raw_policy='require_triple'."
|
||||
)
|
||||
|
||||
if self.capture_mode not in ("TRIPLE", "AUTO"):
|
||||
raise RuntimeError(
|
||||
"OakFcc3Client de produção exige capture_mode TRIPLE/AUTO."
|
||||
)
|
||||
|
||||
expected_mx = (
|
||||
(self.module_params.get("calibration_provenance", {}) or {})
|
||||
.get("device_mx_id")
|
||||
)
|
||||
if expected_mx and self.mx_id and str(expected_mx) != self.mx_id:
|
||||
raise RuntimeError(
|
||||
"MX ID solicitado diverge da calibração homologada: "
|
||||
f"requested={self.mx_id}, calibrated={expected_mx}"
|
||||
)
|
||||
|
||||
def _sync_contract_from_manager_status(self, status):
|
||||
if not isinstance(status, dict):
|
||||
return
|
||||
|
||||
if self.product_contract and not bool(status.get("product_contract", False)):
|
||||
raise RuntimeError(
|
||||
"Client carregou module_params produto, mas Manager não reconheceu o contrato."
|
||||
)
|
||||
|
||||
sizes = status.get("sensor_size_by_role")
|
||||
if isinstance(sizes, dict):
|
||||
normalized = {
|
||||
role: self._normalize_size(sizes.get(role), f"manager.sensor_size_by_role.{role}")
|
||||
for role in ("rgb", "re", "nir")
|
||||
}
|
||||
if self.product_contract and normalized != self.sensor_size_by_role:
|
||||
raise RuntimeError(
|
||||
"Manager e Client discordam sobre sensor_size_by_role: "
|
||||
f"manager={normalized}, client={self.sensor_size_by_role}"
|
||||
)
|
||||
self.sensor_size_by_role = normalized
|
||||
|
||||
bayer = status.get("bayer_pattern")
|
||||
if bayer:
|
||||
bayer = str(bayer).upper()
|
||||
if self.product_contract and bayer != self.bayer:
|
||||
raise RuntimeError(
|
||||
f"Manager Bayer={bayer} != Client/module_params={self.bayer}"
|
||||
)
|
||||
|
||||
def get_contract(self):
|
||||
return {
|
||||
"client_version": OAK_FCC3_CLIENT_VERSION,
|
||||
"product_contract": bool(self.product_contract),
|
||||
"require_product_contract": bool(self.require_product_contract),
|
||||
"sensor_size_by_role": {
|
||||
role: list(size)
|
||||
for role, size in self.sensor_size_by_role.items()
|
||||
},
|
||||
"camera_hardware": json.loads(json.dumps(self.camera_hardware)),
|
||||
"bayer_pattern": self.bayer,
|
||||
"default_target_size": (
|
||||
None if self.default_target_size is None
|
||||
else list(self.default_target_size)
|
||||
),
|
||||
"evaluate_quality": bool(self.evaluate_quality),
|
||||
"radiometric_controller_enabled": bool(self.radiometric_controller_enabled),
|
||||
}
|
||||
|
||||
def apply_module_camera_settings(self):
|
||||
camera_settings = self.module_params.get("camera_settings", {}) or {}
|
||||
|
||||
applied = {}
|
||||
if self.product_contract:
|
||||
missing = [
|
||||
role
|
||||
for role in ("rgb", "re", "nir")
|
||||
if not isinstance(camera_settings.get(role), dict)
|
||||
]
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
f"module_params produto sem camera_settings para: {missing}"
|
||||
)
|
||||
|
||||
for role, settings in camera_settings.items():
|
||||
applied = {}
|
||||
roles = ("rgb", "re", "nir") if self.product_contract else tuple(camera_settings.keys())
|
||||
|
||||
for role in roles:
|
||||
settings = camera_settings.get(role)
|
||||
if not isinstance(settings, dict):
|
||||
continue
|
||||
|
||||
|
|
@ -137,17 +415,43 @@ class OakFcc3Client:
|
|||
}
|
||||
|
||||
self.applied_camera_controls = applied
|
||||
|
||||
if self.product_contract:
|
||||
failed = {
|
||||
role: value
|
||||
for role, value in applied.items()
|
||||
if not bool((value or {}).get("ok", False))
|
||||
}
|
||||
if failed:
|
||||
raise RuntimeError(
|
||||
f"Falha reaplicando camera_settings homologado: {failed}"
|
||||
)
|
||||
|
||||
return applied
|
||||
|
||||
def enable_radiometric_controller(self):
|
||||
cfg = self.module_params.get("radiometric_config", {}) or {}
|
||||
enabled = bool(cfg.get("enabled", False))
|
||||
|
||||
# No produto atual este controller de exposição em campo é OFF.
|
||||
# Não instanciamos um segundo piloto para ficar parado dentro do loop.
|
||||
if not enabled:
|
||||
self.radiometric_controller = None
|
||||
self.radiometric_controller_enabled = False
|
||||
return None
|
||||
|
||||
self.radiometric_controller = RadiometricController(
|
||||
client=self,
|
||||
config_json_path=self.module_calibration_json,
|
||||
)
|
||||
|
||||
self.radiometric_controller.sync_from_camera_controls(self.applied_camera_controls)
|
||||
|
||||
self.radiometric_controller.sync_from_camera_controls(
|
||||
self.applied_camera_controls
|
||||
)
|
||||
self.radiometric_controller.sync_from_actual_camera_controls()
|
||||
|
||||
self.radiometric_controller_enabled = bool(
|
||||
getattr(self.radiometric_controller, "enabled", True)
|
||||
)
|
||||
return self.radiometric_controller
|
||||
|
||||
def update_radiometry(self, decoded, meta=None):
|
||||
|
|
@ -187,17 +491,45 @@ class OakFcc3Client:
|
|||
)
|
||||
|
||||
try:
|
||||
self.mx_id = self.svc.manager.mx_id
|
||||
status = self.svc.get_status()
|
||||
self.mx_id = str(status.get("mx_id") or self.mx_id or "") or None
|
||||
self._sync_contract_from_manager_status(status)
|
||||
except Exception:
|
||||
pass
|
||||
# Se a validação de contrato falhar, fecha hardware antes de propagar.
|
||||
try:
|
||||
self.svc.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
# O Manager de produção já aplicou estes controles via initialControl.
|
||||
# Reaplicamos após start como confirmação operacional e para manter
|
||||
# compatibilidade com Managers legados durante a migração.
|
||||
applied = self.apply_module_camera_settings()
|
||||
|
||||
self.enable_radiometric_controller()
|
||||
|
||||
if print_debug:
|
||||
print("[OAK CLIENT] START:", resp)
|
||||
print("[OAK CLIENT] CONTRACT:", self.get_contract())
|
||||
print("[OAK CLIENT] APPLIED CAMERA SETTINGS:", applied)
|
||||
|
||||
self.enable_radiometric_controller()
|
||||
if isinstance(resp, dict):
|
||||
resp = dict(resp)
|
||||
resp["client_version"] = OAK_FCC3_CLIENT_VERSION
|
||||
resp["product_contract"] = bool(self.product_contract)
|
||||
resp["sensor_size_by_role"] = {
|
||||
role: list(size)
|
||||
for role, size in self.sensor_size_by_role.items()
|
||||
}
|
||||
resp["bayer_pattern"] = self.bayer
|
||||
resp["default_target_size"] = (
|
||||
None if self.default_target_size is None
|
||||
else list(self.default_target_size)
|
||||
)
|
||||
resp["radiometric_controller_enabled"] = bool(
|
||||
self.radiometric_controller_enabled
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
|
|
@ -209,7 +541,29 @@ class OakFcc3Client:
|
|||
return self.svc.get_device_metrics()
|
||||
|
||||
def get_status(self):
|
||||
return self.svc.get_status()
|
||||
status = self.svc.get_status()
|
||||
if not isinstance(status, dict):
|
||||
status = {}
|
||||
else:
|
||||
status = dict(status)
|
||||
|
||||
status.update({
|
||||
"client_version": OAK_FCC3_CLIENT_VERSION,
|
||||
"client_product_contract": bool(self.product_contract),
|
||||
"client_require_product_contract": bool(self.require_product_contract),
|
||||
"client_sensor_size_by_role": {
|
||||
role: list(size)
|
||||
for role, size in self.sensor_size_by_role.items()
|
||||
},
|
||||
"client_bayer_pattern": self.bayer,
|
||||
"client_default_target_size": (
|
||||
None if self.default_target_size is None
|
||||
else list(self.default_target_size)
|
||||
),
|
||||
"evaluate_quality": bool(self.evaluate_quality),
|
||||
"radiometric_controller_enabled": bool(self.radiometric_controller_enabled),
|
||||
})
|
||||
return status
|
||||
|
||||
def get_next_raw_frame(self, timeout=1.0):
|
||||
return self.svc.capture_frame(timeout=timeout)
|
||||
|
|
@ -224,6 +578,12 @@ class OakFcc3Client:
|
|||
frame_type = str(raw_meta.get("frame_type", self.frame_type)).upper()
|
||||
meta = dict(raw_meta)
|
||||
|
||||
if self.product_contract and frame_type != "RAW_BRUTO":
|
||||
raise RuntimeError(
|
||||
"OakFcc3Client produto recebeu frame_type não canônico: "
|
||||
f"{frame_type!r}. Esperado='RAW_BRUTO'."
|
||||
)
|
||||
|
||||
if frame_type == "RAW_BRUTO":
|
||||
decoded = self.decode_stream_cameras(raw_frame, raw_meta)
|
||||
|
||||
|
|
@ -320,6 +680,25 @@ class OakFcc3Client:
|
|||
|
||||
def build_infer_tensor(self, frame, meta, channels_expected, target_size=None):
|
||||
channels_expected = self._validate_physical_channel_count(channels_expected)
|
||||
target_size = self._resolve_target_size(target_size)
|
||||
|
||||
frame_type = str(
|
||||
(meta or {}).get("frame_type", self.frame_type)
|
||||
if isinstance(meta, dict)
|
||||
else self.frame_type
|
||||
).upper()
|
||||
|
||||
# No runtime quente evitamos o quality audit pesado. Decodificamos e
|
||||
# usamos exatamente o mesmo caminho do CameraMultispectral.
|
||||
if not self.evaluate_quality and frame_type == "RAW_BRUTO":
|
||||
decoded = self.core.decode_stream_cameras(frame, meta)
|
||||
return self.build_infer_tensor_from_decoded(
|
||||
decoded=decoded,
|
||||
meta=meta,
|
||||
channels_expected=channels_expected,
|
||||
target_size=target_size,
|
||||
)
|
||||
|
||||
return self.core.build_infer_tensor_from_stream(
|
||||
frame,
|
||||
meta,
|
||||
|
|
@ -327,19 +706,57 @@ class OakFcc3Client:
|
|||
target_size=target_size,
|
||||
)
|
||||
|
||||
def build_infer_tensor_from_decoded(self, decoded, meta, channels_expected, target_size=None):
|
||||
def build_infer_tensor_from_decoded(
|
||||
self,
|
||||
decoded,
|
||||
meta,
|
||||
channels_expected,
|
||||
target_size=None,
|
||||
evaluate_quality=None,
|
||||
):
|
||||
channels_expected = self._validate_physical_channel_count(channels_expected)
|
||||
tensor = self.core.fuse_multispec_cameras(decoded, meta, channels_expected)
|
||||
tensor = self.core.resize_tensor_chw(tensor, target_size=target_size)
|
||||
target_size = self._resolve_target_size(target_size)
|
||||
|
||||
if evaluate_quality is None:
|
||||
evaluate_quality = self.evaluate_quality
|
||||
evaluate_quality = bool(evaluate_quality)
|
||||
|
||||
# Core novo faz a geometria source->target em uma única etapa.
|
||||
# NÃO redimensionar novamente depois da fusão.
|
||||
tensor = self.core.fuse_multispec_cameras(
|
||||
decoded,
|
||||
meta,
|
||||
channels_expected,
|
||||
target_size=target_size,
|
||||
)
|
||||
|
||||
patch_cfg = getattr(
|
||||
self.core,
|
||||
"patch_normalization_config",
|
||||
{},
|
||||
) or {}
|
||||
|
||||
# Mantém paridade com build_infer_tensor_from_stream: se a calibração
|
||||
# habilitar patch normalization, ela também vale no caminho decoded.
|
||||
patch_cfg = getattr(self.core, "patch_normalization_config", {}) or {}
|
||||
if bool(patch_cfg.get("enabled", False)):
|
||||
if self.product_contract:
|
||||
raise RuntimeError(
|
||||
"patch_normalization não é permitido no contrato produto."
|
||||
)
|
||||
tensor = self.core.apply_patch_normalization_to_tensor(tensor)
|
||||
|
||||
self.core.last_frame_quality_result = self.core.evaluate_frame_quality(tensor)
|
||||
return tensor
|
||||
if evaluate_quality:
|
||||
self.core.last_frame_quality_result = self.core.evaluate_frame_quality(tensor)
|
||||
else:
|
||||
# Evita deixar resultado antigo no objeto e evita percentis no hot path.
|
||||
self.core.last_frame_quality_result = {
|
||||
"status": "skipped",
|
||||
"usable_for_training": None,
|
||||
"reason": "disabled_by_oak_fcc3_client",
|
||||
"client_version": OAK_FCC3_CLIENT_VERSION,
|
||||
}
|
||||
|
||||
return np.ascontiguousarray(
|
||||
tensor.astype(np.float32, copy=False)
|
||||
)
|
||||
|
||||
def decode_stream_cameras(self, frame, meta):
|
||||
if str(meta.get("frame_type", self.frame_type)).upper() == "PREVIEW":
|
||||
|
|
@ -382,12 +799,19 @@ class OakFcc3Client:
|
|||
tensor = np.transpose(rgb01.astype(np.float32), (2, 0, 1))
|
||||
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
|
||||
|
||||
def build_multispec_tensor(self, decoded, meta=None, target_size=None):
|
||||
def build_multispec_tensor(
|
||||
self,
|
||||
decoded,
|
||||
meta=None,
|
||||
target_size=None,
|
||||
evaluate_quality=None,
|
||||
):
|
||||
tensor = self.build_infer_tensor_from_decoded(
|
||||
decoded=decoded,
|
||||
meta=meta,
|
||||
channels_expected=5,
|
||||
target_size=target_size,
|
||||
evaluate_quality=evaluate_quality,
|
||||
)
|
||||
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
|
||||
|
||||
|
|
@ -448,10 +872,14 @@ class OakFcc3Client:
|
|||
arr = arr[:, :, 0]
|
||||
|
||||
if bit_depth == 10 and arr.ndim == 2:
|
||||
role_size = self.sensor_size_by_role.get(
|
||||
str(role).lower(),
|
||||
[self.rgb_native_width, self.rgb_native_height],
|
||||
)
|
||||
raw16 = self.core.unpack_raw10_packed(
|
||||
arr,
|
||||
sensor_width=int(info.get("width", self.width)),
|
||||
sensor_height=int(info.get("height", self.height)),
|
||||
sensor_width=int(info.get("width", role_size[0])),
|
||||
sensor_height=int(info.get("height", role_size[1])),
|
||||
)
|
||||
|
||||
if role == "rgb":
|
||||
|
|
@ -513,7 +941,7 @@ class OakFcc3Client:
|
|||
or cam_meta.get("bayer")
|
||||
or stream_meta.get("bayer_pattern")
|
||||
or bayer_pattern
|
||||
or "RGGB"
|
||||
or self.bayer
|
||||
)
|
||||
bayer = str(bayer).upper()
|
||||
|
||||
|
|
@ -545,19 +973,17 @@ class OakFcc3Client:
|
|||
real_w = int(cam_meta.get("width", sensor_width))
|
||||
real_h = int(cam_meta.get("height", sensor_height))
|
||||
|
||||
core = RawProcessorCore(
|
||||
sensor_width=real_w,
|
||||
sensor_height=real_h,
|
||||
bayer_pattern=bayer,
|
||||
)
|
||||
cache_key = (real_w, real_h, bayer)
|
||||
preview = self._preview_cache.get(cache_key)
|
||||
if preview is None:
|
||||
preview = RawProcessorPreview(
|
||||
sensor_width=real_w,
|
||||
sensor_height=real_h,
|
||||
bayer_pattern=bayer,
|
||||
)
|
||||
self._preview_cache[cache_key] = preview
|
||||
|
||||
preview = RawProcessorPreview(
|
||||
sensor_width=real_w,
|
||||
sensor_height=real_h,
|
||||
bayer_pattern=bayer,
|
||||
)
|
||||
|
||||
raw16 = core.unpack_raw10_packed(
|
||||
raw16 = self.core.unpack_raw10_packed(
|
||||
packed,
|
||||
sensor_width=real_w,
|
||||
sensor_height=real_h,
|
||||
|
|
@ -624,6 +1050,11 @@ class OakFcc3Client:
|
|||
|
||||
|
||||
def decode_oak_aligned_multispec(self, frame, meta):
|
||||
if self.product_contract:
|
||||
raise RuntimeError(
|
||||
"MULTISPEC alinhado pela OAK é legado e não faz parte do contrato produto."
|
||||
)
|
||||
|
||||
"""
|
||||
Decodifica frames já alinhados pela OAK.
|
||||
|
||||
|
|
@ -672,6 +1103,11 @@ class OakFcc3Client:
|
|||
return decoded
|
||||
|
||||
def build_multispec_tensor_from_oak_aligned(self, decoded, meta=None):
|
||||
if self.product_contract:
|
||||
raise RuntimeError(
|
||||
"Tensor MULTISPEC pré-alinhado pela OAK é legado no contrato produto."
|
||||
)
|
||||
|
||||
"""
|
||||
Monta CHW [R,G,B,RE,NIR] sem reaplicar homografia.
|
||||
"""
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,95 +1,634 @@
|
|||
# camera_worker/oak_fcc3_core/raw_processor_preview.py
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
RawProcessorPreview - Production
|
||||
================================
|
||||
|
||||
Conversor EXCLUSIVAMENTE visual para RAW Bayer da câmera RGB.
|
||||
|
||||
Este módulo:
|
||||
- NÃO participa do tensor científico;
|
||||
- NÃO altera RAW salvo;
|
||||
- NÃO aplica Flat-Field;
|
||||
- NÃO aplica Radiometric Normalization;
|
||||
- NÃO aplica Homography;
|
||||
- NÃO deve ser usado para treino ou inferência.
|
||||
|
||||
Contrato oficial:
|
||||
CAM_A = RGB
|
||||
OV9782 -> 1280x800
|
||||
AR0234 -> 1920x1200
|
||||
|
||||
A resolução e o Bayer devem vir do module_params/Client já validados.
|
||||
|
||||
Pipeline de preview:
|
||||
RAW Bayer uint16
|
||||
-> robust display levels
|
||||
-> uint8
|
||||
-> demosaic BGR
|
||||
-> gray-world opcional
|
||||
-> contraste opcional
|
||||
-> JPEG/preview
|
||||
|
||||
Importante:
|
||||
O resultado desta classe é BGR, compatível com OpenCV.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from typing import Optional
|
||||
|
||||
|
||||
RAW_PROCESSOR_PREVIEW_VERSION = "production_v1_2026_08_24"
|
||||
|
||||
SUPPORTED_BAYER_PATTERNS = (
|
||||
"RGGB",
|
||||
"BGGR",
|
||||
"GRBG",
|
||||
"GBRG",
|
||||
)
|
||||
|
||||
SUPPORTED_BIT_DEPTHS = (
|
||||
8,
|
||||
10,
|
||||
12,
|
||||
16,
|
||||
)
|
||||
|
||||
|
||||
class RawProcessorPreview:
|
||||
def __init__(self, sensor_width: int, sensor_height: int, bayer_pattern: str = "GBRG"):
|
||||
self.sensor_width = sensor_width
|
||||
self.sensor_height = sensor_height
|
||||
self.bayer_pattern = bayer_pattern.upper()
|
||||
"""
|
||||
Preview RAW Bayer para debug/stream/salvamento visual.
|
||||
|
||||
Parâmetros
|
||||
----------
|
||||
sensor_width:
|
||||
Largura NATIVA do RGB de referência.
|
||||
|
||||
sensor_height:
|
||||
Altura NATIVA do RGB de referência.
|
||||
|
||||
bayer_pattern:
|
||||
Bayer real da CAM_A, vindo do contrato homologado.
|
||||
|
||||
sensor_name:
|
||||
Opcional, somente rastreabilidade.
|
||||
|
||||
strict_shape:
|
||||
Se True, RAW recebido deve possuir exatamente HxW nativo.
|
||||
Recomendado e default no produto.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sensor_width: int,
|
||||
sensor_height: int,
|
||||
bayer_pattern: str = "GBRG",
|
||||
sensor_name: Optional[str] = None,
|
||||
strict_shape: bool = True,
|
||||
):
|
||||
self.sensor_width = int(sensor_width)
|
||||
self.sensor_height = int(sensor_height)
|
||||
|
||||
if self.sensor_width <= 0 or self.sensor_height <= 0:
|
||||
raise ValueError(
|
||||
"Resolução inválida para preview: "
|
||||
f"{self.sensor_width}x{self.sensor_height}"
|
||||
)
|
||||
|
||||
self.bayer_pattern = str(
|
||||
bayer_pattern
|
||||
).strip().upper()
|
||||
|
||||
if self.bayer_pattern not in SUPPORTED_BAYER_PATTERNS:
|
||||
raise ValueError(
|
||||
f"Padrão Bayer não suportado: {self.bayer_pattern}. "
|
||||
f"Suportados={SUPPORTED_BAYER_PATTERNS}"
|
||||
)
|
||||
|
||||
self.sensor_name = (
|
||||
str(sensor_name).strip().upper()
|
||||
if sensor_name
|
||||
else None
|
||||
)
|
||||
|
||||
self.strict_shape = bool(
|
||||
strict_shape
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Contrato
|
||||
# ============================================================
|
||||
|
||||
def get_contract(self) -> dict:
|
||||
return {
|
||||
"preview_version": RAW_PROCESSOR_PREVIEW_VERSION,
|
||||
"sensor_name": self.sensor_name,
|
||||
"sensor_width": int(
|
||||
self.sensor_width
|
||||
),
|
||||
"sensor_height": int(
|
||||
self.sensor_height
|
||||
),
|
||||
"sensor_size": [
|
||||
int(
|
||||
self.sensor_width
|
||||
),
|
||||
int(
|
||||
self.sensor_height
|
||||
),
|
||||
],
|
||||
"bayer_pattern": (
|
||||
self.bayer_pattern
|
||||
),
|
||||
"strict_shape": bool(
|
||||
self.strict_shape
|
||||
),
|
||||
"output_color_order": "BGR",
|
||||
"scientific_effect": "none",
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Validação
|
||||
# ============================================================
|
||||
|
||||
def _validate_raw(
|
||||
self,
|
||||
raw16: np.ndarray,
|
||||
bit_depth: int,
|
||||
) -> np.ndarray:
|
||||
if raw16 is None:
|
||||
raise ValueError(
|
||||
"RAW de preview é None."
|
||||
)
|
||||
|
||||
raw = np.asarray(
|
||||
raw16
|
||||
)
|
||||
|
||||
if raw.ndim != 2:
|
||||
raise ValueError(
|
||||
"RAW Bayer deve ser 2D HxW; "
|
||||
f"shape={raw.shape}"
|
||||
)
|
||||
|
||||
if self.strict_shape:
|
||||
expected = (
|
||||
self.sensor_height,
|
||||
self.sensor_width,
|
||||
)
|
||||
|
||||
if raw.shape != expected:
|
||||
raise ValueError(
|
||||
"RAW Bayer com shape incompatível: "
|
||||
f"recebido={raw.shape}, esperado={expected}"
|
||||
)
|
||||
|
||||
bit_depth = int(
|
||||
bit_depth
|
||||
)
|
||||
|
||||
if bit_depth not in SUPPORTED_BIT_DEPTHS:
|
||||
raise ValueError(
|
||||
f"bit_depth não suportado: {bit_depth}. "
|
||||
f"Suportados={SUPPORTED_BIT_DEPTHS}"
|
||||
)
|
||||
|
||||
if not np.issubdtype(
|
||||
raw.dtype,
|
||||
np.integer,
|
||||
):
|
||||
raise ValueError(
|
||||
"RAW Bayer para preview deve ser inteiro; "
|
||||
f"dtype={raw.dtype}"
|
||||
)
|
||||
|
||||
return raw
|
||||
|
||||
@staticmethod
|
||||
def _validate_levels(
|
||||
black_level,
|
||||
white_level,
|
||||
max_val,
|
||||
):
|
||||
black = (
|
||||
None
|
||||
if black_level is None
|
||||
else float(
|
||||
black_level
|
||||
)
|
||||
)
|
||||
|
||||
white = (
|
||||
None
|
||||
if white_level is None
|
||||
else float(
|
||||
white_level
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
black is not None
|
||||
and not np.isfinite(
|
||||
black
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
f"black_level inválido: {black_level}"
|
||||
)
|
||||
|
||||
if (
|
||||
white is not None
|
||||
and not np.isfinite(
|
||||
white
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
f"white_level inválido: {white_level}"
|
||||
)
|
||||
|
||||
if black is not None:
|
||||
black = float(
|
||||
np.clip(
|
||||
black,
|
||||
0.0,
|
||||
max_val,
|
||||
)
|
||||
)
|
||||
|
||||
if white is not None:
|
||||
white = float(
|
||||
np.clip(
|
||||
white,
|
||||
0.0,
|
||||
max_val,
|
||||
)
|
||||
)
|
||||
|
||||
return black, white
|
||||
|
||||
# ============================================================
|
||||
# RAW -> display uint8
|
||||
# ============================================================
|
||||
|
||||
def raw16_to_vis8(
|
||||
self, raw16: np.ndarray,
|
||||
black_level: Optional[int] = None,
|
||||
white_level: Optional[int] = None,
|
||||
gamma: float = 2.2,
|
||||
bit_depth: int = 10
|
||||
self,
|
||||
raw16: np.ndarray,
|
||||
black_level: Optional[float] = None,
|
||||
white_level: Optional[float] = None,
|
||||
gamma: float = 2.2,
|
||||
bit_depth: int = 10,
|
||||
auto_low_percentile: float = 0.5,
|
||||
auto_high_percentile: float = 99.5,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Conversão para visualização:
|
||||
- auto-level
|
||||
- gamma
|
||||
Converte RAW Bayer para uint8 de DISPLAY.
|
||||
|
||||
Quando níveis não são informados, usa percentis robustos em vez de
|
||||
min/max para evitar que hot pixels ou pequenos pontos saturados lavem
|
||||
todo o preview.
|
||||
|
||||
Isso é somente estética visual.
|
||||
"""
|
||||
max_val = float((1 << bit_depth) - 1)
|
||||
raw = self._validate_raw(
|
||||
raw16,
|
||||
bit_depth,
|
||||
)
|
||||
|
||||
raw = raw16.astype(np.float32)
|
||||
max_val = float(
|
||||
(1 << int(bit_depth))
|
||||
- 1
|
||||
)
|
||||
|
||||
if black_level is None:
|
||||
black_level = float(raw.min())
|
||||
if white_level is None:
|
||||
white_level = float(raw.max())
|
||||
black, white = (
|
||||
self._validate_levels(
|
||||
black_level,
|
||||
white_level,
|
||||
max_val,
|
||||
)
|
||||
)
|
||||
|
||||
if white_level <= black_level:
|
||||
norm = raw / max_val
|
||||
low_p = float(
|
||||
auto_low_percentile
|
||||
)
|
||||
|
||||
high_p = float(
|
||||
auto_high_percentile
|
||||
)
|
||||
|
||||
if not (
|
||||
0.0 <= low_p
|
||||
< high_p
|
||||
<= 100.0
|
||||
):
|
||||
raise ValueError(
|
||||
"Percentis automáticos inválidos: "
|
||||
f"{low_p}, {high_p}"
|
||||
)
|
||||
|
||||
raw_f = raw.astype(
|
||||
np.float32,
|
||||
copy=False,
|
||||
)
|
||||
|
||||
# Amostragem determinística para previews grandes.
|
||||
# Evita percentil full-frame de 2.3M pixels em todo snapshot.
|
||||
total = raw_f.size
|
||||
max_samples = 250_000
|
||||
|
||||
if total > max_samples:
|
||||
step = max(
|
||||
1,
|
||||
total // max_samples,
|
||||
)
|
||||
|
||||
sample = raw_f.reshape(
|
||||
-1
|
||||
)[::step]
|
||||
else:
|
||||
norm = (raw - black_level) / (white_level - black_level)
|
||||
sample = raw_f.reshape(
|
||||
-1
|
||||
)
|
||||
|
||||
norm = np.clip(norm, 0.0, 1.0)
|
||||
if black is None:
|
||||
black = float(
|
||||
np.percentile(
|
||||
sample,
|
||||
low_p,
|
||||
)
|
||||
)
|
||||
|
||||
if gamma is not None and gamma > 0:
|
||||
norm = np.power(norm, 1.0 / gamma)
|
||||
if white is None:
|
||||
white = float(
|
||||
np.percentile(
|
||||
sample,
|
||||
high_p,
|
||||
)
|
||||
)
|
||||
|
||||
return (norm * 255.0).clip(0, 255).astype(np.uint8)
|
||||
if white <= black:
|
||||
# Caso frame praticamente constante.
|
||||
if max_val <= 0.0:
|
||||
norm = np.zeros_like(
|
||||
raw_f,
|
||||
dtype=np.float32,
|
||||
)
|
||||
else:
|
||||
norm = raw_f / max_val
|
||||
|
||||
else:
|
||||
norm = (
|
||||
raw_f - black
|
||||
) / (
|
||||
white - black
|
||||
)
|
||||
|
||||
norm = np.clip(
|
||||
norm,
|
||||
0.0,
|
||||
1.0,
|
||||
)
|
||||
|
||||
if gamma is not None:
|
||||
gamma = float(
|
||||
gamma
|
||||
)
|
||||
|
||||
if not np.isfinite(
|
||||
gamma
|
||||
) or gamma <= 0.0:
|
||||
raise ValueError(
|
||||
f"gamma inválido: {gamma}"
|
||||
)
|
||||
|
||||
norm = np.power(
|
||||
norm,
|
||||
1.0 / gamma,
|
||||
)
|
||||
|
||||
return np.clip(
|
||||
norm * 255.0,
|
||||
0.0,
|
||||
255.0,
|
||||
).astype(
|
||||
np.uint8
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Bayer
|
||||
# ============================================================
|
||||
|
||||
def _debayer_code(self):
|
||||
"""
|
||||
Retorna código OpenCV que produz BGR.
|
||||
|
||||
A versão antiga usava COLOR_Bayer*2RGB_EA e em seguida tratava o
|
||||
resultado como BGR, podendo trocar vermelho/azul no preview.
|
||||
"""
|
||||
# IMPORTANTE:
|
||||
# Os aliases Bayer do OpenCV são contraintuitivos em relação ao
|
||||
# nome físico 2x2 que usamos no produto. O mapeamento abaixo foi
|
||||
# validado com mosaicos sintéticos de cor conhecida e produz BGR:
|
||||
#
|
||||
# físico RGGB -> OpenCV BayerBG2BGR
|
||||
# físico BGGR -> OpenCV BayerRG2BGR
|
||||
# físico GRBG -> OpenCV BayerGB2BGR
|
||||
# físico GBRG -> OpenCV BayerGR2BGR
|
||||
#
|
||||
# Não "simplifique" este mapa pela semelhança dos nomes.
|
||||
mapping = {
|
||||
"RGGB": cv2.COLOR_BayerRG2RGB_EA,
|
||||
"BGGR": cv2.COLOR_BayerBG2RGB_EA,
|
||||
"GRBG": cv2.COLOR_BayerGR2RGB_EA,
|
||||
"GBRG": cv2.COLOR_BayerGB2RGB_EA,
|
||||
}
|
||||
"RGGB": (
|
||||
cv2.COLOR_BayerBG2BGR_EA
|
||||
),
|
||||
"BGGR": (
|
||||
cv2.COLOR_BayerRG2BGR_EA
|
||||
),
|
||||
"GRBG": (
|
||||
cv2.COLOR_BayerGB2BGR_EA
|
||||
),
|
||||
"GBRG": (
|
||||
cv2.COLOR_BayerGR2BGR_EA
|
||||
),
|
||||
}
|
||||
|
||||
if self.bayer_pattern not in mapping:
|
||||
raise ValueError(f"Padrão Bayer não suportado: {self.bayer_pattern}")
|
||||
return mapping[
|
||||
self.bayer_pattern
|
||||
]
|
||||
|
||||
return mapping[self.bayer_pattern]
|
||||
# ============================================================
|
||||
# Ajustes VISUAIS
|
||||
# ============================================================
|
||||
|
||||
def apply_preview_white_balance(self, bgr: np.ndarray, strength: float = 1.0) -> np.ndarray:
|
||||
@staticmethod
|
||||
def apply_preview_white_balance(
|
||||
bgr: np.ndarray,
|
||||
strength: float = 1.0,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Gray-world simples para deixar o preview mais agradável.
|
||||
Não usar no raw de treino.
|
||||
Gray-world simples somente para deixar o preview legível.
|
||||
|
||||
Não usar no RAW científico.
|
||||
"""
|
||||
img = bgr.astype(np.float32)
|
||||
img = np.asarray(
|
||||
bgr
|
||||
)
|
||||
|
||||
mean_b = float(img[:, :, 0].mean())
|
||||
mean_g = float(img[:, :, 1].mean())
|
||||
mean_r = float(img[:, :, 2].mean())
|
||||
if (
|
||||
img.ndim != 3
|
||||
or img.shape[2] != 3
|
||||
):
|
||||
raise ValueError(
|
||||
"Preview WB exige BGR HxWx3; "
|
||||
f"shape={img.shape}"
|
||||
)
|
||||
|
||||
mean_gray = (mean_b + mean_g + mean_r) / 3.0
|
||||
strength = float(
|
||||
strength
|
||||
)
|
||||
|
||||
if not np.isfinite(
|
||||
strength
|
||||
):
|
||||
raise ValueError(
|
||||
f"strength inválido: {strength}"
|
||||
)
|
||||
|
||||
strength = float(
|
||||
np.clip(
|
||||
strength,
|
||||
0.0,
|
||||
1.0,
|
||||
)
|
||||
)
|
||||
|
||||
work = img.astype(
|
||||
np.float32,
|
||||
copy=True,
|
||||
)
|
||||
|
||||
# Amostragem reduz custo em 1920x1200.
|
||||
h, w = work.shape[:2]
|
||||
sample_step = max(
|
||||
1,
|
||||
int(
|
||||
np.sqrt(
|
||||
(h * w)
|
||||
/ 200_000.0
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
sample = work[
|
||||
::sample_step,
|
||||
::sample_step,
|
||||
]
|
||||
|
||||
mean_b = float(
|
||||
sample[:, :, 0].mean()
|
||||
)
|
||||
|
||||
mean_g = float(
|
||||
sample[:, :, 1].mean()
|
||||
)
|
||||
|
||||
mean_r = float(
|
||||
sample[:, :, 2].mean()
|
||||
)
|
||||
|
||||
mean_gray = (
|
||||
mean_b
|
||||
+ mean_g
|
||||
+ mean_r
|
||||
) / 3.0
|
||||
|
||||
eps = 1e-6
|
||||
gain_b = mean_gray / max(mean_b, eps)
|
||||
gain_g = mean_gray / max(mean_g, eps)
|
||||
gain_r = mean_gray / max(mean_r, eps)
|
||||
|
||||
# strength=1 aplica total, strength=0 não aplica
|
||||
gain_b = 1.0 + (gain_b - 1.0) * strength
|
||||
gain_g = 1.0 + (gain_g - 1.0) * strength
|
||||
gain_r = 1.0 + (gain_r - 1.0) * strength
|
||||
gain_b = mean_gray / max(
|
||||
mean_b,
|
||||
eps,
|
||||
)
|
||||
|
||||
img[:, :, 0] *= gain_b
|
||||
img[:, :, 1] *= gain_g
|
||||
img[:, :, 2] *= gain_r
|
||||
gain_g = mean_gray / max(
|
||||
mean_g,
|
||||
eps,
|
||||
)
|
||||
|
||||
return np.clip(img, 0, 255).astype(np.uint8)
|
||||
gain_r = mean_gray / max(
|
||||
mean_r,
|
||||
eps,
|
||||
)
|
||||
|
||||
def apply_preview_contrast(self, bgr: np.ndarray, alpha: float = 1.08, beta: float = 0.0) -> np.ndarray:
|
||||
gain_b = 1.0 + (
|
||||
gain_b - 1.0
|
||||
) * strength
|
||||
|
||||
gain_g = 1.0 + (
|
||||
gain_g - 1.0
|
||||
) * strength
|
||||
|
||||
gain_r = 1.0 + (
|
||||
gain_r - 1.0
|
||||
) * strength
|
||||
|
||||
work[:, :, 0] *= gain_b
|
||||
work[:, :, 1] *= gain_g
|
||||
work[:, :, 2] *= gain_r
|
||||
|
||||
return np.clip(
|
||||
work,
|
||||
0.0,
|
||||
255.0,
|
||||
).astype(
|
||||
np.uint8
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def apply_preview_contrast(
|
||||
bgr: np.ndarray,
|
||||
alpha: float = 1.08,
|
||||
beta: float = 0.0,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Ajuste leve de contraste/brilho para preview.
|
||||
Contraste/brilho de DISPLAY.
|
||||
"""
|
||||
out = cv2.convertScaleAbs(bgr, alpha=alpha, beta=beta)
|
||||
return out
|
||||
alpha = float(
|
||||
alpha
|
||||
)
|
||||
|
||||
beta = float(
|
||||
beta
|
||||
)
|
||||
|
||||
if (
|
||||
not np.isfinite(
|
||||
alpha
|
||||
)
|
||||
or alpha <= 0.0
|
||||
):
|
||||
raise ValueError(
|
||||
f"alpha inválido: {alpha}"
|
||||
)
|
||||
|
||||
if not np.isfinite(
|
||||
beta
|
||||
):
|
||||
raise ValueError(
|
||||
f"beta inválido: {beta}"
|
||||
)
|
||||
|
||||
return cv2.convertScaleAbs(
|
||||
bgr,
|
||||
alpha=alpha,
|
||||
beta=beta,
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Preview completo
|
||||
# ============================================================
|
||||
|
||||
def raw16_to_preview_bgr(
|
||||
self,
|
||||
|
|
@ -99,28 +638,89 @@ class RawProcessorPreview:
|
|||
apply_wb: bool = True,
|
||||
apply_contrast: bool = True,
|
||||
bit_depth: int = 10,
|
||||
black_level: Optional[float] = None,
|
||||
white_level: Optional[float] = None,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Pipeline de preview bonito:
|
||||
1. auto-level + gamma no mosaico
|
||||
2. demosaic
|
||||
3. white balance simples
|
||||
4. leve contraste final
|
||||
Pipeline visual:
|
||||
1. robust levels + gamma no mosaico
|
||||
2. demosaic BGR
|
||||
3. gray-world opcional
|
||||
4. contraste opcional
|
||||
"""
|
||||
vis8 = self.raw16_to_vis8(raw16, gamma=gamma, bit_depth=bit_depth)
|
||||
bgr = cv2.cvtColor(vis8, self._debayer_code())
|
||||
vis8 = self.raw16_to_vis8(
|
||||
raw16,
|
||||
black_level=black_level,
|
||||
white_level=white_level,
|
||||
gamma=gamma,
|
||||
bit_depth=bit_depth,
|
||||
)
|
||||
|
||||
bgr = cv2.cvtColor(
|
||||
vis8,
|
||||
self._debayer_code(),
|
||||
)
|
||||
|
||||
if apply_wb:
|
||||
bgr = self.apply_preview_white_balance(bgr, strength=wb_strength)
|
||||
bgr = (
|
||||
self.apply_preview_white_balance(
|
||||
bgr,
|
||||
strength=wb_strength,
|
||||
)
|
||||
)
|
||||
|
||||
if apply_contrast:
|
||||
bgr = self.apply_preview_contrast(bgr, alpha=1.08, beta=0.0)
|
||||
bgr = (
|
||||
self.apply_preview_contrast(
|
||||
bgr,
|
||||
alpha=1.08,
|
||||
beta=0.0,
|
||||
)
|
||||
)
|
||||
|
||||
return bgr
|
||||
return np.ascontiguousarray(
|
||||
bgr,
|
||||
dtype=np.uint8,
|
||||
)
|
||||
|
||||
def raw16_to_preview_jpg_bytes(
|
||||
self,
|
||||
raw16: np.ndarray,
|
||||
jpeg_quality: int = 95,
|
||||
**preview_kwargs,
|
||||
) -> bytes:
|
||||
quality = int(
|
||||
jpeg_quality
|
||||
)
|
||||
|
||||
if not (
|
||||
1 <= quality <= 100
|
||||
):
|
||||
raise ValueError(
|
||||
f"jpeg_quality inválido: {quality}"
|
||||
)
|
||||
|
||||
bgr = (
|
||||
self.raw16_to_preview_bgr(
|
||||
raw16,
|
||||
**preview_kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
ok, enc = cv2.imencode(
|
||||
".jpg",
|
||||
bgr,
|
||||
[
|
||||
int(
|
||||
cv2.IMWRITE_JPEG_QUALITY
|
||||
),
|
||||
quality,
|
||||
],
|
||||
)
|
||||
|
||||
def raw16_to_preview_jpg_bytes(self, raw16: np.ndarray, jpeg_quality: int = 95) -> bytes:
|
||||
bgr = self.raw16_to_preview_bgr(raw16)
|
||||
ok, enc = cv2.imencode(".jpg", bgr, [int(cv2.IMWRITE_JPEG_QUALITY), int(jpeg_quality)])
|
||||
if not ok:
|
||||
raise RuntimeError("Falha ao codificar preview JPG")
|
||||
raise RuntimeError(
|
||||
"Falha ao codificar preview JPG."
|
||||
)
|
||||
|
||||
return enc.tobytes()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -149,7 +149,7 @@ class SegformerNavRunner:
|
|||
"trt_fp16_enable": bool(self.config.get("trt_fp16_enable", True)),
|
||||
"trt_engine_cache_enable": bool(self.config.get("trt_engine_cache_enable", True)),
|
||||
"trt_engine_cache_path": str(
|
||||
self.config.get("trt_engine_cache_path", "./trt_cache_visual_worker")
|
||||
self.config.get("trt_engine_cache_path", "./trt_cache_weed_worker")
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1402,13 +1402,17 @@ class ModuloAtuador(ModuloDiagnosticoBase):
|
|||
calibrando = ctx["calibrando"]
|
||||
bomba_principal = ctx["bomba_principal"]
|
||||
|
||||
autonomia_corredor_aplicavel = bool(
|
||||
auto
|
||||
and ctx["operacao_iniciada"]
|
||||
and not calibrando
|
||||
and not ctx["finalizando"]
|
||||
)
|
||||
|
||||
herbicida_pode_bloquear = bool(
|
||||
auto and
|
||||
ctx["operacao_iniciada"] and
|
||||
not calibrando and
|
||||
not ctx["finalizando"] and
|
||||
not ctx["pausa"] and
|
||||
not ctx["emergencia"]
|
||||
autonomia_corredor_aplicavel
|
||||
and not ctx["pausa"]
|
||||
and not ctx["emergencia"]
|
||||
)
|
||||
|
||||
em_transicao = self._pulverizacao_em_transicao(ctx)
|
||||
|
|
@ -1491,7 +1495,7 @@ class ModuloAtuador(ModuloDiagnosticoBase):
|
|||
})
|
||||
score = min(score, 90)
|
||||
|
||||
if not ctx["herbicida_ok_corredor"]:
|
||||
if (autonomia_corredor_aplicavel and not ctx["herbicida_ok_corredor"]):
|
||||
motivo = self._texto_ou_padrao(
|
||||
ctx.get("autonomia_motivo"),
|
||||
ctx.get("motivo_herbicida_corredor"),
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
|
||||
motivos_gerais = []
|
||||
condicoes_gerais = []
|
||||
drivers_com_travamento = []
|
||||
saude_total_ativos = 0.0
|
||||
total_drivers_em_uso = 0
|
||||
algum_conectado = False
|
||||
|
|
@ -209,6 +210,17 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
conectado = self._bool(saude_mod.get("conectado_efetivo", False))
|
||||
em_uso = self._bool(saude_mod.get("em_uso", True))
|
||||
|
||||
detalhes_valores = self._as_dict(saude_mod.get("detalhes_valores", {}))
|
||||
if self._bool(detalhes_valores.get("travamento_confirmado", False)):
|
||||
drivers_com_travamento.append({
|
||||
"id": endereco_str,
|
||||
"label": saude_mod.get("label"),
|
||||
"rpm": detalhes_valores.get("rpm", 0),
|
||||
"rpm_sp": detalhes_valores.get("rpm_sp", 0),
|
||||
"corrente_motor": detalhes_valores.get("corrente_motor", 0),
|
||||
"nivel": detalhes_valores.get("travamento_nivel"),
|
||||
})
|
||||
|
||||
if conectado:
|
||||
algum_conectado = True
|
||||
|
||||
|
|
@ -253,6 +265,9 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"drivers_ativos": ativos,
|
||||
"drivers_em_uso": total_drivers_em_uso,
|
||||
"modelo": "movimentacao_inteligente_por_resposta_comando_valores_e_telemetria",
|
||||
|
||||
"travamento_confirmado": len(drivers_com_travamento) > 0,
|
||||
"drivers_com_travamento": drivers_com_travamento
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -924,6 +939,12 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"temperatura_motor": temperatura_motor,
|
||||
"temperatura_driver": temperatura_driver,
|
||||
"score_valores": int(max(0, min(100, round(score)))),
|
||||
|
||||
# Novo contrato
|
||||
"travamento_detectado": possivel_travamento,
|
||||
"travamento_confirmado": estado_travamento["nivel"] == "confirmado",
|
||||
"travamento_nivel": estado_travamento["nivel"],
|
||||
"travamento_duracao_s": estado_travamento.get("duracao_s", 0.0),
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
COMANDO_GRACE_MS = 2000.0
|
||||
COMANDO_ALERTA_MS = 3000.0
|
||||
COMANDO_FALHA_MS = 7000.0
|
||||
COMANDO_RECUPERACAO_MS = 1500.0
|
||||
|
||||
COMPONENTE_RECUPERACAO_GRACE_MS = 3000.0
|
||||
MODULO_RECUPERACAO_GRACE_MS = 10000.0
|
||||
|
|
@ -82,7 +83,10 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
updates[f"sensores.{item['key']}.saude"] = s
|
||||
|
||||
for item in servos:
|
||||
s = self._avaliar_servo(item)
|
||||
s = self._avaliar_servo(
|
||||
item,
|
||||
modulo_disponivel=sen_disponivel,
|
||||
)
|
||||
saude_individual.append(s)
|
||||
updates[f"servos.{item['key']}.saude"] = s
|
||||
|
||||
|
|
@ -92,7 +96,10 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
updates[f"reles.{item['key']}.saude"] = s
|
||||
|
||||
for item in sinaleiros:
|
||||
s = self._avaliar_sinaleiro(item)
|
||||
s = self._avaliar_sinaleiro(
|
||||
item,
|
||||
modulo_disponivel=sen_disponivel,
|
||||
)
|
||||
saude_individual.append(s)
|
||||
updates[f"sinaleiros.{item['key']}.saude"] = s
|
||||
|
||||
|
|
@ -239,12 +246,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
corrente_36v = self._valor_sensor(sensores, "AB36V", "Corrente")
|
||||
temp_bateria = self._valor_sensor(sensores, "TBAT", "Temperatura")
|
||||
|
||||
temps_motor = []
|
||||
for s in sensores:
|
||||
label = str(s.get("dados", {}).get("label", ""))
|
||||
if label.startswith("TMV"):
|
||||
temps_motor.append(self._float(self._campo_atual(s["dados"], "Temperatura", 0), 0))
|
||||
|
||||
return {
|
||||
"bms_ligado": bms_ligado,
|
||||
"percent_bateria": percent_bateria,
|
||||
|
|
@ -257,7 +258,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
"tensao_36v": tensao_36v,
|
||||
"corrente_36v": corrente_36v,
|
||||
"temp_bateria": temp_bateria,
|
||||
"temperaturas_motor": temps_motor,
|
||||
"debug": {
|
||||
"bms_ligado": bms_ligado,
|
||||
"percent_bateria": percent_bateria,
|
||||
|
|
@ -267,7 +267,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
"sensor_ab36v_em_uso": ab36v_em_uso,
|
||||
"tensao_36v": tensao_36v,
|
||||
"corrente_36v": corrente_36v,
|
||||
"temp_motor_max": max(temps_motor) if temps_motor else 0,
|
||||
"temp_bateria": temp_bateria,
|
||||
},
|
||||
}
|
||||
|
|
@ -281,7 +280,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
conectado = self._bool(dados.get("conectado", False))
|
||||
aferir = self._bool(dados.get("aferir", False))
|
||||
mandatorio = self._bool(dados.get("mandatorio", False))
|
||||
em_uso = aferir
|
||||
delegado_ao_mov = str(dados.get("label", "")).upper().startswith("TMV")
|
||||
em_uso = aferir and not delegado_ao_mov
|
||||
motivos = []
|
||||
condicoes = []
|
||||
|
||||
|
|
@ -298,7 +298,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
|
||||
saude = int(max(0, min(100, round(saude))))
|
||||
status = self._resolver_status_item(conectado, saude, condicoes)
|
||||
debug = {"resposta": resposta_score, "valores": valor_debug, "telemetria": telemetria["parametros"]}
|
||||
debug = {
|
||||
"resposta": resposta_score,
|
||||
"valores": valor_debug,
|
||||
"telemetria": telemetria["parametros"],
|
||||
"avaliacao_delegada_ao_mov": delegado_ao_mov,
|
||||
}
|
||||
return self._item(item, status, saude, motivos, condicoes, em_uso, debug, "sensor")
|
||||
|
||||
def _avaliar_valores_sensor(self, item, ctx, motivos, condicoes):
|
||||
|
|
@ -403,8 +408,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
label = str(dados.get("label", ""))
|
||||
if label == "AB36V" and ctx["sensor_ab36v_em_uso"]:
|
||||
return ["Tensao", "Corrente", "Temperatura"]
|
||||
if label.startswith("TMV"):
|
||||
return ["Temperatura"]
|
||||
if label == "TBAT" and not ctx["bms_ligado"]:
|
||||
return ["Temperatura"]
|
||||
if mandatorio:
|
||||
|
|
@ -415,73 +418,254 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
# Atuadores auxiliares
|
||||
# ============================================================
|
||||
|
||||
def _avaliar_servo(self, item):
|
||||
def _avaliar_servo(self, item, modulo_disponivel=True):
|
||||
dados = item["dados"]
|
||||
conectado = self._bool(dados.get("conectado", False))
|
||||
controlar = self._bool(dados.get("controlar", False))
|
||||
if not conectado:
|
||||
return self._item(item, StatusModulo.DESCONECTADO, 0, ["desconectado"], [], controlar, {}, "servo")
|
||||
runtime, suspenso = self._preparar_atuador_auxiliar(
|
||||
item=item,
|
||||
grupo="servo",
|
||||
nome="Servo SEN",
|
||||
modulo_disponivel=modulo_disponivel,
|
||||
)
|
||||
if suspenso is not None:
|
||||
return suspenso
|
||||
|
||||
motivos, condicoes = [], []
|
||||
resposta = self._score_resposta(dados, motivos, condicoes, "Servo SEN")
|
||||
comando, dbg_cmd = self._avaliar_comando_servo(item, motivos, condicoes)
|
||||
telem = self._avaliar_telemetria(dados, motivos, ["ServoAnguloLeitura"] if controlar else [])
|
||||
comando, dbg_cmd = self._avaliar_comando_servo(
|
||||
item,
|
||||
motivos,
|
||||
condicoes,
|
||||
runtime,
|
||||
)
|
||||
telem = self._avaliar_telemetria(
|
||||
dados,
|
||||
motivos,
|
||||
["ServoAnguloLeitura"] if controlar else [],
|
||||
)
|
||||
|
||||
saude = resposta * 0.25 + comando * 0.60 + telem["score"] * 0.15
|
||||
if any(self._is_falha_hardware(c) for c in condicoes):
|
||||
saude = min(saude, 45)
|
||||
saude = int(max(0, min(100, round(saude))))
|
||||
return self._item(item, self._resolver_status_item(conectado, saude, condicoes), saude, motivos, condicoes, controlar, {"resposta": resposta, "comando": dbg_cmd, "telemetria": telem["parametros"]}, "servo")
|
||||
|
||||
def _avaliar_comando_servo(self, item, motivos, condicoes):
|
||||
return self._item(
|
||||
item,
|
||||
self._resolver_status_item(True, saude, condicoes),
|
||||
saude,
|
||||
motivos,
|
||||
condicoes,
|
||||
controlar,
|
||||
{
|
||||
"resposta": resposta,
|
||||
"comando": dbg_cmd,
|
||||
"telemetria": telem["parametros"],
|
||||
},
|
||||
"servo",
|
||||
)
|
||||
|
||||
def _avaliar_comando_servo(self, item, motivos, condicoes, runtime):
|
||||
dados = item["dados"]
|
||||
label = dados.get("label")
|
||||
controlar = self._bool(dados.get("controlar", False))
|
||||
angulo_sp = self._float(dados.get("angulo_desejado", dados.get("angulo_sp", 0)), 0)
|
||||
angulo = self._float(dados.get("angulo_leitura", self._campo_atual(dados, "ServoAnguloLeitura", -1)), -1)
|
||||
ultimo_ms = self._float(dados.get("ultimo_comando_ms", 999999), 999999)
|
||||
tem_idade = ultimo_ms < 900000
|
||||
|
||||
if angulo < 0:
|
||||
motivos.append("Servo sem leitura de ângulo")
|
||||
condicoes.append({
|
||||
"label": "Servo SEN",
|
||||
"valor": angulo,
|
||||
"severidade": 70 if controlar else 35,
|
||||
"classe": "processo",
|
||||
"descricao": f"Servo {label} sem leitura de ângulo válida",
|
||||
"acoes": ["Verificar retorno de ServoAnguloLeitura."],
|
||||
})
|
||||
return (60 if controlar else 85), {"angulo": angulo, "angulo_sp": angulo_sp, "sem_leitura": True}
|
||||
desejado_presente = (
|
||||
"angulo_desejado" in dados or
|
||||
"angulo_sp" in dados
|
||||
)
|
||||
leitura_presente = (
|
||||
"angulo_leitura" in dados or
|
||||
self._campo_existe(dados, "ServoAnguloLeitura")
|
||||
)
|
||||
|
||||
angulo_sp = self._float(
|
||||
dados.get("angulo_desejado", dados.get("angulo_sp", 0.0)),
|
||||
0.0,
|
||||
)
|
||||
angulo = self._float(
|
||||
dados.get(
|
||||
"angulo_leitura",
|
||||
self._campo_atual(dados, "ServoAnguloLeitura", -1.0),
|
||||
),
|
||||
-1.0,
|
||||
)
|
||||
|
||||
evidencia = self._evidencia_comando(dados)
|
||||
debug = {
|
||||
"controlar": controlar,
|
||||
"angulo": angulo,
|
||||
"angulo_sp": angulo_sp,
|
||||
**evidencia,
|
||||
}
|
||||
|
||||
if not controlar:
|
||||
self._resetar_estado_comando(runtime, manter_desejado=False)
|
||||
return 100, debug
|
||||
|
||||
if not desejado_presente or not leitura_presente:
|
||||
self._resetar_estado_comando(runtime, manter_desejado=False)
|
||||
debug["contrato_comando_completo"] = False
|
||||
return 92, debug
|
||||
|
||||
token_desejado = round(angulo_sp, 3)
|
||||
self._registrar_desejado(runtime, token_desejado)
|
||||
|
||||
erro = abs(angulo - angulo_sp)
|
||||
if not controlar or erro <= self.SERVO_TOL_GRAUS:
|
||||
return 100, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "controlar": controlar}
|
||||
if tem_idade and ultimo_ms < self.COMANDO_GRACE_MS:
|
||||
return 96, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "ultimo_comando_ms": ultimo_ms}
|
||||
if erro >= self.SERVO_FALHA_GRAUS and tem_idade and ultimo_ms >= self.COMANDO_FALHA_MS:
|
||||
motivos.append(f"Servo {label} não convergiu para o SP ({angulo:.1f}/{angulo_sp:.1f})")
|
||||
condicoes.append({
|
||||
"label": "Servo SEN",
|
||||
"valor": angulo,
|
||||
"severidade": 92,
|
||||
"classe": "falha_hardware",
|
||||
"descricao": f"Servo {label} não acompanhou o comando de ângulo",
|
||||
"acoes": ["Verificar travamento mecânico ou alimentação do servo."],
|
||||
})
|
||||
return 25, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "ultimo_comando_ms": ultimo_ms}
|
||||
if erro >= self.SERVO_ALERTA_GRAUS:
|
||||
motivos.append(f"Servo {label} distante do SP ({angulo:.1f}/{angulo_sp:.1f})")
|
||||
condicoes.append({
|
||||
"label": "Servo SEN",
|
||||
"valor": angulo,
|
||||
"severidade": min(89, max(45, int(erro * 4))),
|
||||
"classe": "processo",
|
||||
"descricao": f"Servo {label} distante do ângulo desejado",
|
||||
"acoes": ["Acompanhar se o erro persiste."],
|
||||
})
|
||||
return 70, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "ultimo_comando_ms": ultimo_ms}
|
||||
motivos.append(f"Servo {label} fora da tolerância momentaneamente ({erro:.1f}°)")
|
||||
return 88, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "ultimo_comando_ms": ultimo_ms}
|
||||
debug["erro"] = erro
|
||||
|
||||
if erro <= self.SERVO_TOL_GRAUS:
|
||||
return self._confirmar_recuperacao_servo(
|
||||
runtime,
|
||||
label,
|
||||
angulo,
|
||||
angulo_sp,
|
||||
erro,
|
||||
motivos,
|
||||
condicoes,
|
||||
debug,
|
||||
)
|
||||
|
||||
runtime["coerencia_desde"] = None
|
||||
|
||||
if not evidencia["possui_comando"]:
|
||||
runtime["divergencia_desde"] = None
|
||||
debug["avaliacao_suspensa"] = "aguardando primeiro comando"
|
||||
return 96, debug
|
||||
|
||||
if not evidencia["telemetria_pos_comando"]:
|
||||
runtime["divergencia_desde"] = None
|
||||
debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando"
|
||||
return 96, debug
|
||||
|
||||
if evidencia["ultimo_comando_ms"] < self.COMANDO_GRACE_MS:
|
||||
runtime["divergencia_desde"] = None
|
||||
debug["avaliacao_suspensa"] = "período de resposta ao comando"
|
||||
return 96, debug
|
||||
|
||||
agora = time.monotonic()
|
||||
tempo_retorno_ms = (
|
||||
agora - runtime["componente_recuperado_em"]
|
||||
) * 1000.0
|
||||
if tempo_retorno_ms < self.COMPONENTE_RECUPERACAO_GRACE_MS:
|
||||
runtime["divergencia_desde"] = None
|
||||
debug["avaliacao_suspensa"] = "estabilização após reconexão"
|
||||
debug["tempo_desde_retorno_componente_ms"] = tempo_retorno_ms
|
||||
return 96, debug
|
||||
|
||||
if runtime["divergencia_desde"] is None:
|
||||
runtime["divergencia_desde"] = agora
|
||||
|
||||
tempo_divergente_ms = (
|
||||
agora - runtime["divergencia_desde"]
|
||||
) * 1000.0
|
||||
debug["tempo_divergente_ms"] = tempo_divergente_ms
|
||||
|
||||
if runtime["nivel_divergencia"] == "falha":
|
||||
return self._falha_servo(
|
||||
label, angulo, angulo_sp, erro,
|
||||
motivos, condicoes, debug,
|
||||
)
|
||||
|
||||
if tempo_divergente_ms < self.COMANDO_ALERTA_MS:
|
||||
return 96, debug
|
||||
|
||||
if (
|
||||
erro >= self.SERVO_FALHA_GRAUS and
|
||||
tempo_divergente_ms >= self.COMANDO_FALHA_MS
|
||||
):
|
||||
runtime["nivel_divergencia"] = "falha"
|
||||
return self._falha_servo(
|
||||
label, angulo, angulo_sp, erro,
|
||||
motivos, condicoes, debug,
|
||||
)
|
||||
|
||||
runtime["nivel_divergencia"] = "alerta"
|
||||
motivos.append(
|
||||
f"Servo {label} distante do SP ({angulo:.1f}/{angulo_sp:.1f})"
|
||||
)
|
||||
condicoes.append({
|
||||
"label": "Servo SEN",
|
||||
"valor": angulo,
|
||||
"severidade": min(89, max(55, int(erro * 4))),
|
||||
"classe": "processo",
|
||||
"descricao": f"Servo {label} não convergiu para o ângulo desejado",
|
||||
"acoes": [
|
||||
"Acompanhar a convergência do servo.",
|
||||
"Verificar esforço mecânico se o erro permanecer.",
|
||||
],
|
||||
})
|
||||
return (68 if erro >= self.SERVO_ALERTA_GRAUS else 82), debug
|
||||
|
||||
def _falha_servo(
|
||||
self,
|
||||
label,
|
||||
angulo,
|
||||
angulo_sp,
|
||||
erro,
|
||||
motivos,
|
||||
condicoes,
|
||||
debug,
|
||||
):
|
||||
motivos.append(
|
||||
f"Servo {label} não convergiu para o SP "
|
||||
f"({angulo:.1f}/{angulo_sp:.1f})"
|
||||
)
|
||||
condicoes.append({
|
||||
"label": "Servo SEN",
|
||||
"valor": angulo,
|
||||
"severidade": 92,
|
||||
"classe": "falha_hardware",
|
||||
"descricao": f"Servo {label} não acompanhou o comando de ângulo",
|
||||
"acoes": [
|
||||
"Verificar travamento mecânico.",
|
||||
"Verificar alimentação e retorno do servo.",
|
||||
],
|
||||
})
|
||||
debug["erro"] = erro
|
||||
return 25, debug
|
||||
|
||||
def _confirmar_recuperacao_servo(
|
||||
self,
|
||||
runtime,
|
||||
label,
|
||||
angulo,
|
||||
angulo_sp,
|
||||
erro,
|
||||
motivos,
|
||||
condicoes,
|
||||
debug,
|
||||
):
|
||||
runtime["divergencia_desde"] = None
|
||||
|
||||
if runtime["nivel_divergencia"] is None:
|
||||
runtime["coerencia_desde"] = None
|
||||
return 100, debug
|
||||
|
||||
agora = time.monotonic()
|
||||
if runtime["coerencia_desde"] is None:
|
||||
runtime["coerencia_desde"] = agora
|
||||
|
||||
tempo_coerente_ms = (
|
||||
agora - runtime["coerencia_desde"]
|
||||
) * 1000.0
|
||||
debug["recuperacao_em_confirmacao"] = True
|
||||
debug["tempo_coerente_ms"] = tempo_coerente_ms
|
||||
|
||||
if tempo_coerente_ms >= self.COMANDO_RECUPERACAO_MS:
|
||||
runtime["nivel_divergencia"] = None
|
||||
runtime["coerencia_desde"] = None
|
||||
return 100, debug
|
||||
|
||||
motivos.append(f"Servo {label} recuperou o SP; confirmando estabilidade")
|
||||
condicoes.append({
|
||||
"label": "Servo SEN",
|
||||
"valor": angulo,
|
||||
"severidade": 55,
|
||||
"classe": "processo",
|
||||
"descricao": f"Servo {label} voltou ao SP e está em confirmação",
|
||||
"acoes": ["Aguardar confirmação da estabilidade do retorno."],
|
||||
})
|
||||
return 78, debug
|
||||
|
||||
def _avaliar_rele(self, item, modulo_disponivel=True):
|
||||
return self._avaliar_saida_binaria(
|
||||
|
|
@ -493,8 +677,11 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
modulo_disponivel=modulo_disponivel,
|
||||
)
|
||||
|
||||
def _avaliar_sinaleiro(self, item):
|
||||
return self._avaliar_sinaleiro_impl(item)
|
||||
def _avaliar_sinaleiro(self, item, modulo_disponivel=True):
|
||||
return self._avaliar_sinaleiro_impl(
|
||||
item,
|
||||
modulo_disponivel=modulo_disponivel,
|
||||
)
|
||||
|
||||
def _avaliar_saida_binaria(
|
||||
self,
|
||||
|
|
@ -672,8 +859,155 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
"componente_recuperado_em": 0.0,
|
||||
|
||||
"divergencia_desde": None,
|
||||
"coerencia_desde": None,
|
||||
"nivel_divergencia": None,
|
||||
"ultimo_desejado": None,
|
||||
})
|
||||
|
||||
def _preparar_atuador_auxiliar(
|
||||
self,
|
||||
item,
|
||||
grupo,
|
||||
nome,
|
||||
modulo_disponivel,
|
||||
):
|
||||
dados = item["dados"]
|
||||
controlar = self._bool(dados.get("controlar", False))
|
||||
conectado = self._bool(dados.get("conectado", False))
|
||||
agora = time.monotonic()
|
||||
runtime = self._runtime_saida_binaria(item)
|
||||
|
||||
if not modulo_disponivel:
|
||||
runtime["modulo_disponivel_anterior"] = False
|
||||
runtime["componente_disponivel_anterior"] = False
|
||||
self._resetar_estado_comando(runtime, manter_desejado=False)
|
||||
return runtime, self._item(
|
||||
item,
|
||||
StatusModulo.DESCONECTADO,
|
||||
0,
|
||||
[],
|
||||
[],
|
||||
False,
|
||||
{
|
||||
"avaliacao_suspensa": True,
|
||||
"motivo_suspensao": "Módulo SEN indisponível",
|
||||
},
|
||||
grupo,
|
||||
)
|
||||
|
||||
if not runtime["modulo_disponivel_anterior"]:
|
||||
runtime["modulo_recuperado_em"] = agora
|
||||
runtime["componente_disponivel_anterior"] = False
|
||||
self._resetar_estado_comando(runtime, manter_desejado=False)
|
||||
|
||||
runtime["modulo_disponivel_anterior"] = True
|
||||
tempo_retorno_modulo_ms = (
|
||||
agora - runtime["modulo_recuperado_em"]
|
||||
) * 1000.0
|
||||
|
||||
if not conectado:
|
||||
runtime["componente_disponivel_anterior"] = False
|
||||
self._resetar_estado_comando(runtime, manter_desejado=False)
|
||||
|
||||
if tempo_retorno_modulo_ms < self.MODULO_RECUPERACAO_GRACE_MS:
|
||||
return runtime, self._item(
|
||||
item,
|
||||
StatusModulo.DESCONECTADO,
|
||||
0,
|
||||
[],
|
||||
[],
|
||||
False,
|
||||
{
|
||||
"avaliacao_suspensa": True,
|
||||
"motivo_suspensao": (
|
||||
"Aguardando reinicialização após retorno do SEN"
|
||||
),
|
||||
"tempo_desde_retorno_modulo_ms": tempo_retorno_modulo_ms,
|
||||
},
|
||||
grupo,
|
||||
)
|
||||
|
||||
return runtime, self._item(
|
||||
item,
|
||||
StatusModulo.DESCONECTADO,
|
||||
0,
|
||||
["desconectado após período de recuperação"],
|
||||
[{
|
||||
"label": nome,
|
||||
"valor": 0,
|
||||
"severidade": 92,
|
||||
"classe": "falha_hardware",
|
||||
"descricao": (
|
||||
f"{dados.get('label')} não reinicializou após o retorno do SEN"
|
||||
),
|
||||
"acoes": [
|
||||
"Verificar configuração do componente.",
|
||||
"Verificar retorno de telemetria.",
|
||||
"Verificar firmware do SEN.",
|
||||
],
|
||||
}],
|
||||
controlar,
|
||||
{
|
||||
"tempo_desde_retorno_modulo_ms": tempo_retorno_modulo_ms,
|
||||
},
|
||||
grupo,
|
||||
)
|
||||
|
||||
if not runtime["componente_disponivel_anterior"]:
|
||||
runtime["componente_recuperado_em"] = agora
|
||||
self._resetar_estado_comando(runtime, manter_desejado=False)
|
||||
|
||||
runtime["componente_disponivel_anterior"] = True
|
||||
return runtime, None
|
||||
|
||||
def _evidencia_comando(self, dados):
|
||||
ultimo_comando_ms = self._float(
|
||||
dados.get("ultimo_comando_ms", 999999),
|
||||
999999,
|
||||
)
|
||||
ultima_resposta_ms = self._float(
|
||||
dados.get("ultima_resposta_ms", 999999),
|
||||
999999,
|
||||
)
|
||||
possui_comando = self._bool(
|
||||
dados.get(
|
||||
"possui_comando",
|
||||
ultimo_comando_ms < 900000,
|
||||
)
|
||||
)
|
||||
|
||||
if "telemetria_pos_comando" in dados:
|
||||
telemetria_pos_comando = self._bool(
|
||||
dados.get("telemetria_pos_comando", False)
|
||||
)
|
||||
else:
|
||||
telemetria_pos_comando = (
|
||||
possui_comando and
|
||||
ultima_resposta_ms <= ultimo_comando_ms + 100.0
|
||||
)
|
||||
|
||||
return {
|
||||
"possui_comando": possui_comando,
|
||||
"telemetria_pos_comando": telemetria_pos_comando,
|
||||
"ultimo_comando_ms": ultimo_comando_ms,
|
||||
"ultima_resposta_ms": ultima_resposta_ms,
|
||||
}
|
||||
|
||||
def _registrar_desejado(self, runtime, token_desejado):
|
||||
if runtime.get("ultimo_desejado") == token_desejado:
|
||||
return
|
||||
|
||||
self._resetar_estado_comando(runtime, manter_desejado=False)
|
||||
runtime["ultimo_desejado"] = token_desejado
|
||||
|
||||
@staticmethod
|
||||
def _resetar_estado_comando(runtime, manter_desejado=True):
|
||||
runtime["divergencia_desde"] = None
|
||||
runtime["coerencia_desde"] = None
|
||||
runtime["nivel_divergencia"] = None
|
||||
if not manter_desejado:
|
||||
runtime["ultimo_desejado"] = None
|
||||
|
||||
def _avaliar_comando_binario(
|
||||
self,
|
||||
item,
|
||||
|
|
@ -874,57 +1208,213 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
|
||||
return 30, debug
|
||||
|
||||
def _avaliar_sinaleiro_impl(self, item):
|
||||
def _avaliar_sinaleiro_impl(self, item, modulo_disponivel=True):
|
||||
dados = item["dados"]
|
||||
conectado = self._bool(dados.get("conectado", False))
|
||||
controlar = self._bool(dados.get("controlar", False))
|
||||
if not conectado:
|
||||
return self._item(item, StatusModulo.DESCONECTADO, 0, ["desconectado"], [], controlar, {}, "sinaleiro")
|
||||
runtime, suspenso = self._preparar_atuador_auxiliar(
|
||||
item=item,
|
||||
grupo="sinaleiro",
|
||||
nome="Sinaleiro SEN",
|
||||
modulo_disponivel=modulo_disponivel,
|
||||
)
|
||||
if suspenso is not None:
|
||||
return suspenso
|
||||
|
||||
motivos, condicoes = [], []
|
||||
resposta = self._score_resposta(dados, motivos, condicoes, "Sinaleiro SEN")
|
||||
comando, dbg_cmd = self._avaliar_comando_sinaleiro(item, motivos, condicoes)
|
||||
resposta = self._score_resposta(
|
||||
dados,
|
||||
motivos,
|
||||
condicoes,
|
||||
"Sinaleiro SEN",
|
||||
)
|
||||
comando, dbg_cmd = self._avaliar_comando_sinaleiro(
|
||||
item,
|
||||
motivos,
|
||||
condicoes,
|
||||
runtime,
|
||||
)
|
||||
telem = self._avaliar_telemetria(dados, motivos, [])
|
||||
# Sinaleiro é diagnóstico visual: não derruba operação sozinho.
|
||||
|
||||
# Sinaleiro é diagnóstico visual: uma divergência confirmada
|
||||
# gera alerta, mas não transforma o SEN inteiro em falha.
|
||||
saude = resposta * 0.45 + comando * 0.45 + telem["score"] * 0.10
|
||||
saude = int(max(0, min(100, round(saude))))
|
||||
return self._item(item, self._resolver_status_item(conectado, saude, condicoes), saude, motivos, condicoes, controlar, {"resposta": resposta, "comando": dbg_cmd, "telemetria": telem["parametros"]}, "sinaleiro")
|
||||
|
||||
def _avaliar_comando_sinaleiro(self, item, motivos, condicoes):
|
||||
return self._item(
|
||||
item,
|
||||
self._resolver_status_item(True, saude, condicoes),
|
||||
saude,
|
||||
motivos,
|
||||
condicoes,
|
||||
controlar,
|
||||
{
|
||||
"resposta": resposta,
|
||||
"comando": dbg_cmd,
|
||||
"telemetria": telem["parametros"],
|
||||
},
|
||||
"sinaleiro",
|
||||
)
|
||||
|
||||
def _avaliar_comando_sinaleiro(
|
||||
self,
|
||||
item,
|
||||
motivos,
|
||||
condicoes,
|
||||
runtime,
|
||||
):
|
||||
dados = item["dados"]
|
||||
label = dados.get("label")
|
||||
controlar = self._bool(dados.get("controlar", False))
|
||||
id_sp = self._safe_int(dados.get("status_led_id_sp", dados.get("led_id_sp", -1)), -1)
|
||||
st_sp = self._safe_int(dados.get("status_led_sp", dados.get("led_status_sp", -1)), -1)
|
||||
id_lido = self._safe_int(dados.get("status_led_id", self._campo_atual(dados, "StatusLedID", -1)), -1)
|
||||
st_lido = self._safe_int(dados.get("status_led_leitura", self._campo_atual(dados, "StatusLedLeitura", -1)), -1)
|
||||
ultimo_ms = self._float(dados.get("ultimo_comando_ms", 999999), 999999)
|
||||
|
||||
id_sp = self._safe_int(
|
||||
dados.get("status_led_id_sp", dados.get("led_id_sp", -1)),
|
||||
-1,
|
||||
)
|
||||
st_sp = self._safe_int(
|
||||
dados.get("status_led_sp", dados.get("led_status_sp", -1)),
|
||||
-1,
|
||||
)
|
||||
id_lido = self._safe_int(
|
||||
dados.get(
|
||||
"status_led_id",
|
||||
self._campo_atual(dados, "StatusLedID", -1),
|
||||
),
|
||||
-1,
|
||||
)
|
||||
|
||||
# O publicador C# envia as propriedades em todos os ciclos.
|
||||
# Portanto, presença lógica é determinada pelo valor válido,
|
||||
# não apenas pela existência da chave no dicionário.
|
||||
id_sp_presente = id_sp >= 0
|
||||
status_sp_presente = st_sp >= 0
|
||||
st_lido = self._safe_int(
|
||||
dados.get(
|
||||
"status_led_leitura",
|
||||
self._campo_atual(dados, "StatusLedLeitura", -1),
|
||||
),
|
||||
-1,
|
||||
)
|
||||
|
||||
evidencia = self._evidencia_comando(dados)
|
||||
debug = {
|
||||
"controlar": controlar,
|
||||
"status_lido": st_lido,
|
||||
"status_sp": st_sp,
|
||||
"id_lido": id_lido,
|
||||
"id_sp": id_sp,
|
||||
**evidencia,
|
||||
}
|
||||
|
||||
if not controlar:
|
||||
return 100, {"controlar": False, "status_lido": st_lido, "id_lido": id_lido}
|
||||
if id_sp < 0 and st_sp < 0:
|
||||
return 94, {"contrato_comando_completo": False, "status_lido": st_lido, "id_lido": id_lido}
|
||||
self._resetar_estado_comando(runtime, manter_desejado=False)
|
||||
return 100, debug
|
||||
|
||||
if not id_sp_presente and not status_sp_presente:
|
||||
self._resetar_estado_comando(runtime, manter_desejado=False)
|
||||
debug["contrato_comando_completo"] = False
|
||||
return 94, debug
|
||||
|
||||
token_desejado = (
|
||||
id_sp if id_sp_presente else None,
|
||||
st_sp if status_sp_presente else None,
|
||||
)
|
||||
self._registrar_desejado(runtime, token_desejado)
|
||||
|
||||
# Se o SP existe, uma leitura ausente (-1) também é divergência.
|
||||
coerente_id = (
|
||||
not id_sp_presente or
|
||||
(id_lido >= 0 and id_lido == id_sp)
|
||||
)
|
||||
coerente_status = (
|
||||
not status_sp_presente or
|
||||
(st_lido >= 0 and st_lido == st_sp)
|
||||
)
|
||||
coerente = coerente_id and coerente_status
|
||||
debug["divergente"] = not coerente
|
||||
|
||||
coerente = True
|
||||
if id_sp >= 0 and id_lido >= 0 and id_sp != id_lido:
|
||||
coerente = False
|
||||
if st_sp >= 0 and st_lido >= 0 and st_sp != st_lido:
|
||||
coerente = False
|
||||
if coerente:
|
||||
return 100, {"status_lido": st_lido, "status_sp": st_sp, "id_lido": id_lido, "id_sp": id_sp}
|
||||
if ultimo_ms < self.COMANDO_GRACE_MS:
|
||||
return 96, {"status_lido": st_lido, "status_sp": st_sp, "id_lido": id_lido, "id_sp": id_sp, "ultimo_comando_ms": ultimo_ms}
|
||||
runtime["divergencia_desde"] = None
|
||||
|
||||
motivos.append(f"Sinaleiro {label} diferente do comportamento desejado")
|
||||
if runtime["nivel_divergencia"] is None:
|
||||
runtime["coerencia_desde"] = None
|
||||
return 100, debug
|
||||
|
||||
agora = time.monotonic()
|
||||
if runtime["coerencia_desde"] is None:
|
||||
runtime["coerencia_desde"] = agora
|
||||
|
||||
tempo_coerente_ms = (
|
||||
agora - runtime["coerencia_desde"]
|
||||
) * 1000.0
|
||||
debug["recuperacao_em_confirmacao"] = True
|
||||
debug["tempo_coerente_ms"] = tempo_coerente_ms
|
||||
|
||||
if tempo_coerente_ms >= self.COMANDO_RECUPERACAO_MS:
|
||||
runtime["nivel_divergencia"] = None
|
||||
runtime["coerencia_desde"] = None
|
||||
return 100, debug
|
||||
|
||||
motivos.append(
|
||||
f"Sinaleiro {label} recuperou o estado; confirmando estabilidade"
|
||||
)
|
||||
return 88, debug
|
||||
|
||||
runtime["coerencia_desde"] = None
|
||||
|
||||
if not evidencia["possui_comando"]:
|
||||
runtime["divergencia_desde"] = None
|
||||
debug["avaliacao_suspensa"] = "aguardando primeiro comando"
|
||||
return 96, debug
|
||||
|
||||
if not evidencia["telemetria_pos_comando"]:
|
||||
runtime["divergencia_desde"] = None
|
||||
debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando"
|
||||
return 96, debug
|
||||
|
||||
if evidencia["ultimo_comando_ms"] < self.COMANDO_GRACE_MS:
|
||||
runtime["divergencia_desde"] = None
|
||||
debug["avaliacao_suspensa"] = "período de resposta ao comando"
|
||||
return 96, debug
|
||||
|
||||
agora = time.monotonic()
|
||||
tempo_retorno_ms = (
|
||||
agora - runtime["componente_recuperado_em"]
|
||||
) * 1000.0
|
||||
if tempo_retorno_ms < self.COMPONENTE_RECUPERACAO_GRACE_MS:
|
||||
runtime["divergencia_desde"] = None
|
||||
debug["avaliacao_suspensa"] = "estabilização após reconexão"
|
||||
debug["tempo_desde_retorno_componente_ms"] = tempo_retorno_ms
|
||||
return 96, debug
|
||||
|
||||
if runtime["divergencia_desde"] is None:
|
||||
runtime["divergencia_desde"] = agora
|
||||
|
||||
tempo_divergente_ms = (
|
||||
agora - runtime["divergencia_desde"]
|
||||
) * 1000.0
|
||||
debug["tempo_divergente_ms"] = tempo_divergente_ms
|
||||
|
||||
if tempo_divergente_ms < self.COMANDO_ALERTA_MS:
|
||||
return 96, debug
|
||||
|
||||
runtime["nivel_divergencia"] = "alerta"
|
||||
motivos.append(
|
||||
f"Sinaleiro {label} diferente do comportamento desejado"
|
||||
)
|
||||
condicoes.append({
|
||||
"label": "Sinaleiro SEN",
|
||||
"valor": st_lido,
|
||||
"severidade": 45,
|
||||
"severidade": 55,
|
||||
"classe": "processo",
|
||||
"descricao": f"Sinaleiro {label} não está refletindo o comportamento desejado",
|
||||
"acoes": ["Conferir LED/sinaleiro físico e comando pelo SEN."],
|
||||
"descricao": (
|
||||
f"Sinaleiro {label} não está refletindo o comportamento desejado"
|
||||
),
|
||||
"acoes": [
|
||||
"Conferir LED/sinaleiro físico.",
|
||||
"Conferir comando e retorno publicados pelo SEN.",
|
||||
],
|
||||
})
|
||||
return 82, {"status_lido": st_lido, "status_sp": st_sp, "id_lido": id_lido, "id_sp": id_sp, "ultimo_comando_ms": ultimo_ms}
|
||||
return 78, debug
|
||||
|
||||
# ============================================================
|
||||
# Processo geral
|
||||
|
|
@ -1013,30 +1503,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
|||
score = min(score, 60)
|
||||
energia_critica = True
|
||||
|
||||
temp_motor_max = max(ctx["temperaturas_motor"]) if ctx["temperaturas_motor"] else 0
|
||||
if temp_motor_max >= 90:
|
||||
motivos.append(f"Temperatura máxima dos motores crítica ({temp_motor_max:.1f} °C)")
|
||||
condicoes.append({
|
||||
"label": "Temperatura motores",
|
||||
"valor": temp_motor_max,
|
||||
"severidade": 95,
|
||||
"classe": "falha_hardware",
|
||||
"descricao": f"Temperatura de motor crítica ({temp_motor_max:.1f} °C)",
|
||||
"acoes": ["Reduzir carga/parar movimentação e verificar refrigeração."],
|
||||
})
|
||||
score = min(score, 45)
|
||||
elif temp_motor_max >= 80:
|
||||
motivos.append(f"Temperatura máxima dos motores elevada ({temp_motor_max:.1f} °C)")
|
||||
condicoes.append({
|
||||
"label": "Temperatura motores",
|
||||
"valor": temp_motor_max,
|
||||
"severidade": 75,
|
||||
"classe": "risco_operacional",
|
||||
"descricao": f"Temperatura de motor elevada ({temp_motor_max:.1f} °C)",
|
||||
"acoes": ["Reduzir esforço se persistir."],
|
||||
})
|
||||
score = min(score, 75)
|
||||
|
||||
if ctx["temp_bateria"] >= 60:
|
||||
motivos.append(f"Temperatura da bateria crítica ({ctx['temp_bateria']:.1f} °C)")
|
||||
condicoes.append({
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ class RegrasTaticas:
|
|||
# Pulverizador automático.
|
||||
"atu_indisponivel": self._nova_persistencia(),
|
||||
"bomba_cavitada": self._nova_persistencia(),
|
||||
|
||||
# MOV / possível atolamento ou bloqueio mecânico.
|
||||
"mov_travamento": self._nova_persistencia(),
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
|
|
@ -51,6 +54,13 @@ class RegrasTaticas:
|
|||
motivos.extend(novos_motivos)
|
||||
freio_necessario = freio_necessario or freio
|
||||
|
||||
novos_motivos, freio = self._avaliar_travamento_mov(
|
||||
controle=controle,
|
||||
operacao=operacao
|
||||
)
|
||||
motivos.extend(novos_motivos)
|
||||
freio_necessario = freio_necessario or freio
|
||||
|
||||
novos_motivos, freio = self._avaliar_oak_parada_por_bloqueio(
|
||||
controle=controle,
|
||||
operacao=operacao
|
||||
|
|
@ -126,6 +136,92 @@ class RegrasTaticas:
|
|||
|
||||
return motivos, freio_necessario
|
||||
|
||||
def _avaliar_travamento_mov(self, controle: dict, operacao: dict):
|
||||
movimento_automatico = self._bool(
|
||||
controle.get("movimento_automatico", False)
|
||||
)
|
||||
|
||||
controle_manual = self._bool(
|
||||
controle.get("controle_manual_acionado", False)
|
||||
)
|
||||
|
||||
status_operacao = self._enum_or(
|
||||
StatusOperacao,
|
||||
operacao.get(
|
||||
"status",
|
||||
StatusOperacao.NaoIniciado.value
|
||||
),
|
||||
StatusOperacao.NaoIniciado
|
||||
)
|
||||
|
||||
pausa = self._bool(operacao.get("pausa", False))
|
||||
emergencia = self._bool(operacao.get("emergencia", False))
|
||||
calibrando = self._bool(operacao.get("calibrando", False))
|
||||
finalizando = self._bool(operacao.get("finalizando", False))
|
||||
|
||||
deve_avaliar = (
|
||||
status_operacao == StatusOperacao.EmAndamento
|
||||
and movimento_automatico
|
||||
and not controle_manual
|
||||
and not pausa
|
||||
and not emergencia
|
||||
and not calibrando
|
||||
and not finalizando
|
||||
)
|
||||
|
||||
if not deve_avaliar:
|
||||
self._resetar_persistencia("mov_travamento")
|
||||
return [], False
|
||||
|
||||
mov, _, operante = self._modulo_operante(
|
||||
T_Code.Mov,
|
||||
aceitar_alerta=True
|
||||
)
|
||||
|
||||
if not operante:
|
||||
# Indisponibilidade do módulo não deve ser confundida com atolamento.
|
||||
self._resetar_persistencia("mov_travamento")
|
||||
return [], False
|
||||
|
||||
saude = self._dict(mov.get("saude", {}))
|
||||
detalhes = self._dict(saude.get("detalhes", {}))
|
||||
|
||||
travamento_health = self._bool(
|
||||
detalhes.get("travamento_confirmado", False)
|
||||
)
|
||||
|
||||
# O health já exige aproximadamente 2 segundos e 3 amostras.
|
||||
# Esta persistência curta apenas protege a regra contra troca de
|
||||
# snapshot ou inconsistência momentânea no Redis.
|
||||
travamento_tatico = self._avaliar_persistencia(
|
||||
nome="mov_travamento",
|
||||
estado_bruto=travamento_health,
|
||||
tempo_acionar=0.30,
|
||||
tempo_liberar=2.00
|
||||
)
|
||||
|
||||
if not travamento_tatico:
|
||||
return [], False
|
||||
|
||||
drivers = detalhes.get("drivers_com_travamento", []) or []
|
||||
|
||||
ids = [
|
||||
str(driver.get("label") or driver.get("id"))
|
||||
for driver in drivers
|
||||
if isinstance(driver, dict)
|
||||
]
|
||||
|
||||
complemento = (
|
||||
f" | motores: {', '.join(ids)}"
|
||||
if ids
|
||||
else ""
|
||||
)
|
||||
|
||||
return [
|
||||
"Possível atolamento ou bloqueio mecânico detectado"
|
||||
f"{complemento}. Operação pausada para intervenção do operador."
|
||||
], True
|
||||
|
||||
def _avaliar_oak_parada_por_bloqueio(self, controle: dict, operacao: dict):
|
||||
"""
|
||||
Bloqueia somente se a função de parada por bloqueio estiver ativa.
|
||||
|
|
|
|||
|
|
@ -630,6 +630,90 @@ class ContextoGlobalRedis:
|
|||
"motivos": motivos_controle_runtime,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 5.1) Solicitação de intertravamento sistêmico
|
||||
# ------------------------------------------------------------
|
||||
|
||||
parada_seguranca = cls._dict(
|
||||
controle.get("parada_seguranca", {})
|
||||
)
|
||||
|
||||
parada_seguranca_ativa = cls._bool(
|
||||
parada_seguranca.get("ativa", False)
|
||||
)
|
||||
|
||||
codigo_parada_seguranca = str(
|
||||
parada_seguranca.get("codigo", "") or ""
|
||||
).strip().upper()
|
||||
|
||||
codigos_que_solicitam_emergencia = {
|
||||
"MOV_TRAVAMENTO",
|
||||
}
|
||||
|
||||
codigo_valido_emergencia = (
|
||||
codigo_parada_seguranca
|
||||
in codigos_que_solicitam_emergencia
|
||||
)
|
||||
|
||||
solicitacao_da_regra = cls._bool(
|
||||
parada_seguranca.get(
|
||||
"solicitar_emergencia",
|
||||
False
|
||||
)
|
||||
)
|
||||
|
||||
emergencia_sistema_solicitada = bool(
|
||||
parada_seguranca_ativa
|
||||
and codigo_valido_emergencia
|
||||
and solicitacao_da_regra
|
||||
)
|
||||
|
||||
motivo_emergencia_sistema = (
|
||||
str(
|
||||
parada_seguranca.get(
|
||||
"motivo",
|
||||
"Intertravamento sistêmico solicitado"
|
||||
)
|
||||
)
|
||||
if emergencia_sistema_solicitada
|
||||
else None
|
||||
)
|
||||
|
||||
origem_emergencia_sistema = (
|
||||
parada_seguranca.get("origem")
|
||||
if emergencia_sistema_solicitada
|
||||
else None
|
||||
)
|
||||
|
||||
severidade_emergencia_sistema = (
|
||||
cls._int(
|
||||
parada_seguranca.get("severidade", 90),
|
||||
90
|
||||
)
|
||||
if emergencia_sistema_solicitada
|
||||
else 0
|
||||
)
|
||||
|
||||
ts_evento_emergencia_sistema = (
|
||||
parada_seguranca.get("ts_evento")
|
||||
if emergencia_sistema_solicitada
|
||||
else None
|
||||
)
|
||||
|
||||
detalhes_regras["intertravamento_sistema"] = {
|
||||
"ativo": emergencia_sistema_solicitada,
|
||||
"codigo": (
|
||||
codigo_parada_seguranca
|
||||
if emergencia_sistema_solicitada
|
||||
else None
|
||||
),
|
||||
"origem": origem_emergencia_sistema,
|
||||
"severidade": severidade_emergencia_sistema,
|
||||
"motivo": motivo_emergencia_sistema,
|
||||
"ts_evento": ts_evento_emergencia_sistema,
|
||||
"drivers": parada_seguranca.get("drivers", []),
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 6) Decisão final
|
||||
# ------------------------------------------------------------
|
||||
|
|
@ -667,6 +751,22 @@ class ContextoGlobalRedis:
|
|||
liberado=liberou,
|
||||
motivo_nao_liberado="\n\n".join(motivos) if not liberou else None,
|
||||
motivos_nao_liberado_lista=motivos,
|
||||
|
||||
emergencia_sistema_solicitada=emergencia_sistema_solicitada,
|
||||
emergencia_sistema={
|
||||
"solicitada": emergencia_sistema_solicitada,
|
||||
"codigo": (
|
||||
codigo_parada_seguranca
|
||||
if emergencia_sistema_solicitada
|
||||
else None
|
||||
),
|
||||
"origem": origem_emergencia_sistema,
|
||||
"severidade": severidade_emergencia_sistema,
|
||||
"motivo": motivo_emergencia_sistema,
|
||||
"ts_evento": ts_evento_emergencia_sistema,
|
||||
"drivers": parada_seguranca.get("drivers", []),
|
||||
},
|
||||
|
||||
operacao_liberacao_debug={
|
||||
"ts": time.time(),
|
||||
"liberado": liberou,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import datetime
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
|
@ -24,6 +26,13 @@ from visual_worker.utils import converter_valores_numpy
|
|||
from shared.perf_monitor import VisualPerfMonitor
|
||||
|
||||
|
||||
CAMERA_MANAGER_VERSION = "production_v1_2026_08_24"
|
||||
|
||||
PRODUCT_ASSEMBLY_SCHEMA = "multispec_module_params_assembly_v1"
|
||||
PRODUCT_MODULE_SCHEMA = "multispec_module_params_v3"
|
||||
PHYSICAL_RAW5_CHANNELS = 5
|
||||
|
||||
|
||||
class CameraManager:
|
||||
"""
|
||||
CameraManager v1 do Weed Worker.
|
||||
|
|
@ -61,6 +70,12 @@ class CameraManager:
|
|||
self.weed_detector = None
|
||||
self.seg_config = None
|
||||
|
||||
# Contrato imutável da sessão produto.
|
||||
# Não faz parte do reset operacional porque descreve modelo + MP + hardware.
|
||||
self._pipeline_contract = {}
|
||||
self._module_params_snapshot = {}
|
||||
self._module_params_path = None
|
||||
|
||||
self.operante = False
|
||||
self.iniciando = False
|
||||
self.debug_visual = False
|
||||
|
|
@ -226,6 +241,488 @@ class CameraManager:
|
|||
self._proxima_inicializacao_monotonic = time.monotonic() + atraso
|
||||
return atraso
|
||||
|
||||
@staticmethod
|
||||
def _normalizar_size_wh(value, field_name):
|
||||
if not isinstance(value, (list, tuple)) or len(value) != 2:
|
||||
raise RuntimeError(
|
||||
f"{field_name} deve ser [W,H]; recebido={value!r}"
|
||||
)
|
||||
|
||||
w = int(value[0])
|
||||
h = int(value[1])
|
||||
|
||||
if w <= 0 or h <= 0:
|
||||
raise RuntimeError(
|
||||
f"{field_name} inválido: {value!r}"
|
||||
)
|
||||
|
||||
return [w, h]
|
||||
|
||||
@staticmethod
|
||||
def _path_normalizado(value):
|
||||
if not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
p = Path(str(value))
|
||||
if p.is_file():
|
||||
return str(p.resolve())
|
||||
|
||||
# Mesmo quando ainda não existe, normaliza sem exigir existência.
|
||||
return str(p.expanduser().absolute())
|
||||
except Exception:
|
||||
return str(value)
|
||||
|
||||
@staticmethod
|
||||
def _carregar_json_obrigatorio(path_value, field_name):
|
||||
if not path_value:
|
||||
raise RuntimeError(f"{field_name} não definido.")
|
||||
|
||||
p = Path(str(path_value))
|
||||
if not p.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"{field_name} não encontrado: {p}"
|
||||
)
|
||||
|
||||
with p.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError(
|
||||
f"{field_name} deve conter JSON object/dict."
|
||||
)
|
||||
|
||||
return p.resolve(), data
|
||||
|
||||
def _obter_tamanho_entrada_modelo(self):
|
||||
if self.model_svc is None:
|
||||
raise RuntimeError("Modelo ONNX ainda não inicializado.")
|
||||
|
||||
shape = list(
|
||||
getattr(self.model_svc, "onnx_input_shape", None)
|
||||
or []
|
||||
)
|
||||
|
||||
if len(shape) != 4:
|
||||
raise RuntimeError(
|
||||
f"ONNX input deve ser BCHW 4D; shape={shape}"
|
||||
)
|
||||
|
||||
h = shape[2]
|
||||
w = shape[3]
|
||||
|
||||
if not isinstance(h, (int, np.integer)) or not isinstance(w, (int, np.integer)):
|
||||
raise RuntimeError(
|
||||
"Produto exige resolução ONNX estática em H/W; "
|
||||
f"shape={shape}"
|
||||
)
|
||||
|
||||
if int(w) <= 0 or int(h) <= 0:
|
||||
raise RuntimeError(
|
||||
f"Resolução ONNX inválida: {shape}"
|
||||
)
|
||||
|
||||
return [int(w), int(h)]
|
||||
|
||||
def _validar_module_params_produto(self, seg_config, mx_id):
|
||||
module_path, mp = self._carregar_json_obrigatorio(
|
||||
seg_config.get("module_calibration_json"),
|
||||
"module_calibration_json",
|
||||
)
|
||||
|
||||
require_product = bool(
|
||||
seg_config.get("require_product_contract", True)
|
||||
)
|
||||
|
||||
if mp.get("schema") != PRODUCT_MODULE_SCHEMA:
|
||||
raise RuntimeError(
|
||||
f"module_params schema inválido: {mp.get('schema')!r}. "
|
||||
f"Esperado={PRODUCT_MODULE_SCHEMA!r}"
|
||||
)
|
||||
|
||||
assembly = mp.get("assembly_metadata", {}) or {}
|
||||
assembly_schema = assembly.get("schema")
|
||||
product_contract = assembly_schema == PRODUCT_ASSEMBLY_SCHEMA
|
||||
|
||||
if require_product and not product_contract:
|
||||
raise RuntimeError(
|
||||
"CameraManager produto exige module_params gerado pelo "
|
||||
"module_params_assembler_production.py. "
|
||||
f"assembly_metadata.schema={assembly_schema!r}"
|
||||
)
|
||||
|
||||
fusion = mp.get("fusion_config", {}) or {}
|
||||
mp_target = self._normalizar_size_wh(
|
||||
fusion.get("target_size"),
|
||||
"module_params.fusion_config.target_size",
|
||||
)
|
||||
|
||||
sizes_raw = mp.get("sensor_size_by_role", {}) or {}
|
||||
hardware = mp.get("camera_hardware", {}) or {}
|
||||
|
||||
sizes = {}
|
||||
for role in ("rgb", "re", "nir"):
|
||||
size = sizes_raw.get(role)
|
||||
|
||||
if size is None:
|
||||
hw_role = hardware.get(role, {}) or {}
|
||||
size = hw_role.get("size")
|
||||
|
||||
if require_product and size is None:
|
||||
raise RuntimeError(
|
||||
f"module_params sem sensor_size_by_role.{role}"
|
||||
)
|
||||
|
||||
if size is not None:
|
||||
sizes[role] = self._normalizar_size_wh(
|
||||
size,
|
||||
f"module_params.sensor_size_by_role.{role}",
|
||||
)
|
||||
|
||||
if require_product and set(sizes) != {"rgb", "re", "nir"}:
|
||||
raise RuntimeError(
|
||||
f"module_params incompleto em sensor_size_by_role: {sizes}"
|
||||
)
|
||||
|
||||
provenance = mp.get("calibration_provenance", {}) or {}
|
||||
calibrated_mx = provenance.get("device_mx_id")
|
||||
|
||||
if calibrated_mx and str(calibrated_mx) != str(mx_id):
|
||||
raise RuntimeError(
|
||||
"MX ID solicitado diverge do módulo calibrado: "
|
||||
f"solicitado={mx_id} calibrado={calibrated_mx}"
|
||||
)
|
||||
|
||||
return {
|
||||
"path": str(module_path),
|
||||
"module_params": mp,
|
||||
"product_contract": product_contract,
|
||||
"require_product_contract": require_product,
|
||||
"target_size": mp_target,
|
||||
"sensor_size_by_role": sizes,
|
||||
"camera_hardware": hardware,
|
||||
"calibrated_mx_id": calibrated_mx,
|
||||
}
|
||||
|
||||
def _validar_contrato_pipeline(self, seg_config, mx_id):
|
||||
"""
|
||||
Fecha a autoridade da resolução antes de abrir a OAK:
|
||||
|
||||
ONNX input [W,H]
|
||||
==
|
||||
module_params.fusion_config.target_size
|
||||
==
|
||||
seg_config.ia_resolution
|
||||
|
||||
camera_width/camera_height deixam de participar da decisão física.
|
||||
"""
|
||||
model_target = self._obter_tamanho_entrada_modelo()
|
||||
mp_info = self._validar_module_params_produto(
|
||||
seg_config,
|
||||
mx_id,
|
||||
)
|
||||
|
||||
mp_target = list(mp_info["target_size"])
|
||||
|
||||
if model_target != mp_target:
|
||||
raise RuntimeError(
|
||||
"Contrato de resolução incompatível entre ONNX e module_params: "
|
||||
f"ONNX={model_target} module_params={mp_target}"
|
||||
)
|
||||
|
||||
cfg_target_raw = seg_config.get("ia_resolution")
|
||||
if cfg_target_raw is not None:
|
||||
cfg_target = self._normalizar_size_wh(
|
||||
cfg_target_raw,
|
||||
"seg_config.ia_resolution",
|
||||
)
|
||||
|
||||
if cfg_target != model_target:
|
||||
raise RuntimeError(
|
||||
"Contrato de resolução incompatível entre config, ONNX e MP: "
|
||||
f"config={cfg_target} ONNX={model_target} MP={mp_target}"
|
||||
)
|
||||
else:
|
||||
cfg_target = list(model_target)
|
||||
|
||||
# Canonicaliza o config em memória. A partir daqui quem consulta
|
||||
# ia_resolution recebe o contrato já validado, não um default solto.
|
||||
seg_config["ia_resolution"] = list(model_target)
|
||||
seg_config["module_calibration_json"] = mp_info["path"]
|
||||
|
||||
model_channels = int(
|
||||
getattr(self.model_svc, "channels", 0)
|
||||
or 0
|
||||
)
|
||||
|
||||
input_names = list(
|
||||
getattr(
|
||||
self.model_svc,
|
||||
"input_channel_names",
|
||||
[],
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
if model_channels <= 0 or len(input_names) != model_channels:
|
||||
raise RuntimeError(
|
||||
"Contrato de canais do modelo inválido: "
|
||||
f"C={model_channels} names={input_names}"
|
||||
)
|
||||
|
||||
physical_allowed = {
|
||||
"R", "G", "B", "RE", "NIR", "NDVI", "NDRE",
|
||||
}
|
||||
|
||||
invalid = [
|
||||
str(x)
|
||||
for x in input_names
|
||||
if str(x).upper() not in physical_allowed
|
||||
]
|
||||
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Canais ONNX não suportados pelo pipeline: {invalid}"
|
||||
)
|
||||
|
||||
rgb_native = (
|
||||
list(mp_info["sensor_size_by_role"].get("rgb"))
|
||||
if mp_info["sensor_size_by_role"].get("rgb") is not None
|
||||
else None
|
||||
)
|
||||
|
||||
if mp_info["require_product_contract"] and rgb_native is None:
|
||||
raise RuntimeError(
|
||||
"Produto exige resolução RGB nativa no module_params."
|
||||
)
|
||||
|
||||
model_path = getattr(
|
||||
self.model_svc,
|
||||
"onnx_path",
|
||||
None,
|
||||
)
|
||||
|
||||
contract = {
|
||||
"validated": True,
|
||||
"camera_manager_version": CAMERA_MANAGER_VERSION,
|
||||
"product_contract": bool(mp_info["product_contract"]),
|
||||
"require_product_contract": bool(
|
||||
mp_info["require_product_contract"]
|
||||
),
|
||||
"tensor_size": list(model_target),
|
||||
"onnx_input_shape": list(
|
||||
getattr(
|
||||
self.model_svc,
|
||||
"onnx_input_shape",
|
||||
[],
|
||||
)
|
||||
or []
|
||||
),
|
||||
"onnx_input_channels": input_names,
|
||||
"onnx_model_path": (
|
||||
str(model_path)
|
||||
if model_path is not None
|
||||
else None
|
||||
),
|
||||
"module_params_path": mp_info["path"],
|
||||
"sensor_size_by_role": dict(
|
||||
mp_info["sensor_size_by_role"]
|
||||
),
|
||||
"camera_hardware": dict(
|
||||
mp_info["camera_hardware"]
|
||||
),
|
||||
"rgb_native_size": rgb_native,
|
||||
"calibrated_mx_id": mp_info[
|
||||
"calibrated_mx_id"
|
||||
],
|
||||
}
|
||||
|
||||
self._pipeline_contract = contract
|
||||
self._module_params_snapshot = mp_info[
|
||||
"module_params"
|
||||
]
|
||||
self._module_params_path = mp_info[
|
||||
"path"
|
||||
]
|
||||
|
||||
self.mostrar_log(
|
||||
"[weed][CONTRACT] "
|
||||
f"tensor={contract['tensor_size']} "
|
||||
f"channels={contract['onnx_input_channels']} "
|
||||
f"product={contract['product_contract']} "
|
||||
f"mp={contract['module_params_path']}"
|
||||
)
|
||||
|
||||
return contract
|
||||
|
||||
def _validar_tensor_camera(self, tensor5, contexto="runtime"):
|
||||
if not isinstance(tensor5, np.ndarray):
|
||||
raise RuntimeError(
|
||||
f"[{contexto}] tensor da câmera deve ser np.ndarray; "
|
||||
f"recebido={type(tensor5)}"
|
||||
)
|
||||
|
||||
if tensor5.ndim != 3:
|
||||
raise RuntimeError(
|
||||
f"[{contexto}] Raw5 deve ser CHW 3D; shape={tensor5.shape}"
|
||||
)
|
||||
|
||||
if int(tensor5.shape[0]) != PHYSICAL_RAW5_CHANNELS:
|
||||
raise RuntimeError(
|
||||
f"[{contexto}] câmera deve entregar Raw5 com C=5; "
|
||||
f"shape={tensor5.shape}"
|
||||
)
|
||||
|
||||
expected = (
|
||||
self._pipeline_contract.get(
|
||||
"tensor_size"
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
if len(expected) == 2:
|
||||
expected_w = int(expected[0])
|
||||
expected_h = int(expected[1])
|
||||
|
||||
if (
|
||||
int(tensor5.shape[2]) != expected_w
|
||||
or int(tensor5.shape[1]) != expected_h
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"[{contexto}] tensor H/W incompatível: "
|
||||
f"recebido={tensor5.shape} "
|
||||
f"esperado=(5,{expected_h},{expected_w})"
|
||||
)
|
||||
|
||||
if tensor5.dtype != np.float32:
|
||||
raise RuntimeError(
|
||||
f"[{contexto}] Raw5 produto deve ser float32; "
|
||||
f"dtype={tensor5.dtype}"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def _validar_config_dinamica_contrato(self, cfg):
|
||||
"""
|
||||
Somente parâmetros operacionais podem mudar durante a sessão.
|
||||
Modelo, MP, canais e resolução exigem reinicialização completa.
|
||||
"""
|
||||
if not self._pipeline_contract.get("validated", False):
|
||||
return cfg
|
||||
|
||||
expected_size = list(
|
||||
self._pipeline_contract[
|
||||
"tensor_size"
|
||||
]
|
||||
)
|
||||
|
||||
requested_size = cfg.get(
|
||||
"ia_resolution"
|
||||
)
|
||||
|
||||
if requested_size is not None:
|
||||
requested_size = self._normalizar_size_wh(
|
||||
requested_size,
|
||||
"config dinâmica ia_resolution",
|
||||
)
|
||||
|
||||
if requested_size != expected_size:
|
||||
raise RuntimeError(
|
||||
"ia_resolution não pode mudar com a sessão ativa: "
|
||||
f"atual={expected_size} solicitado={requested_size}. "
|
||||
"Reinicie CameraManager com o novo modelo/MP."
|
||||
)
|
||||
|
||||
cfg["ia_resolution"] = list(
|
||||
expected_size
|
||||
)
|
||||
|
||||
expected_mp = self._pipeline_contract.get(
|
||||
"module_params_path"
|
||||
)
|
||||
|
||||
requested_mp = self._path_normalizado(
|
||||
cfg.get(
|
||||
"module_calibration_json"
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
expected_mp
|
||||
and requested_mp
|
||||
and requested_mp != self._path_normalizado(
|
||||
expected_mp
|
||||
)
|
||||
):
|
||||
raise RuntimeError(
|
||||
"module_calibration_json não pode mudar com a sessão ativa. "
|
||||
"Feche/reinicialize o CameraManager."
|
||||
)
|
||||
|
||||
current_model = self._path_normalizado(
|
||||
self._pipeline_contract.get(
|
||||
"onnx_model_path"
|
||||
)
|
||||
)
|
||||
|
||||
requested_model = self._path_normalizado(
|
||||
cfg.get(
|
||||
"onnx_model_path"
|
||||
)
|
||||
or cfg.get(
|
||||
"ia_model_path"
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
current_model
|
||||
and requested_model
|
||||
and current_model != requested_model
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Modelo ONNX não pode ser trocado dinamicamente na sessão ativa. "
|
||||
"Feche/reinicialize o CameraManager."
|
||||
)
|
||||
|
||||
requested_channels = cfg.get(
|
||||
"input_channels"
|
||||
)
|
||||
|
||||
if isinstance(
|
||||
requested_channels,
|
||||
str,
|
||||
):
|
||||
requested_channels = [
|
||||
x.strip().upper()
|
||||
for x in requested_channels.split(",")
|
||||
if x.strip()
|
||||
]
|
||||
|
||||
elif requested_channels is not None:
|
||||
requested_channels = [
|
||||
str(x).upper()
|
||||
for x in requested_channels
|
||||
]
|
||||
|
||||
current_channels = [
|
||||
str(x).upper()
|
||||
for x in self._pipeline_contract.get(
|
||||
"onnx_input_channels",
|
||||
[]
|
||||
)
|
||||
]
|
||||
|
||||
if (
|
||||
requested_channels is not None
|
||||
and requested_channels != current_channels
|
||||
):
|
||||
raise RuntimeError(
|
||||
"input_channels não pode mudar com ONNX ativo: "
|
||||
f"atual={current_channels} solicitado={requested_channels}"
|
||||
)
|
||||
|
||||
return cfg
|
||||
|
||||
def inicializar(self, mx_id):
|
||||
if mx_id is None:
|
||||
return False
|
||||
|
|
@ -271,7 +768,20 @@ class CameraManager:
|
|||
self.seg_config.get("posproc_intervalo_min_s", 5.0)
|
||||
)
|
||||
|
||||
camera_nova = self._inicializar_camera(mx_id, self.seg_config)
|
||||
# Produto: valida o modelo e o MP ANTES de abrir a OAK.
|
||||
# Assim não ocupamos USB/pipeline para só depois descobrir
|
||||
# que ONNX, module_params e ia_resolution são incompatíveis.
|
||||
self._inicializar_modelo()
|
||||
self._validar_contrato_pipeline(
|
||||
self.seg_config,
|
||||
mx_id,
|
||||
)
|
||||
|
||||
camera_nova = self._inicializar_camera(
|
||||
mx_id,
|
||||
self.seg_config,
|
||||
)
|
||||
|
||||
if camera_nova is None:
|
||||
atraso = self._registrar_falha_inicializacao()
|
||||
self.mostrar_log(
|
||||
|
|
@ -294,7 +804,6 @@ class CameraManager:
|
|||
self.debug_visual = bool(self.seg_config.get("debug_visual", False))
|
||||
self.debug_perf = bool(self.seg_config.get("debug_perf", False))
|
||||
|
||||
self._inicializar_modelo()
|
||||
self._inicializar_detector()
|
||||
|
||||
self.operante = False
|
||||
|
|
@ -374,15 +883,56 @@ class CameraManager:
|
|||
nova = None
|
||||
|
||||
try:
|
||||
contract = self._pipeline_contract or {}
|
||||
|
||||
if not contract.get("validated", False):
|
||||
raise RuntimeError(
|
||||
"Contrato pipeline não validado antes da câmera."
|
||||
)
|
||||
|
||||
target_size = list(
|
||||
contract["tensor_size"]
|
||||
)
|
||||
|
||||
rgb_native = (
|
||||
contract.get(
|
||||
"rgb_native_size"
|
||||
)
|
||||
or [1280, 800]
|
||||
)
|
||||
|
||||
# camera_width/camera_height do config deixam de ser autoridade.
|
||||
# Estes valores são apenas fallback de compatibilidade do construtor
|
||||
# e vêm do MP homologado.
|
||||
fallback_w = int(
|
||||
rgb_native[0]
|
||||
)
|
||||
fallback_h = int(
|
||||
rgb_native[1]
|
||||
)
|
||||
|
||||
nova = CameraMultispectral(
|
||||
self.mostrar_log,
|
||||
mx_id=mx_id,
|
||||
module_calibration_json=seg_config.get("module_calibration_json"),
|
||||
width=int(seg_config.get("camera_width", 1280)),
|
||||
height=int(seg_config.get("camera_height", 800)),
|
||||
# Default conservador para campo. Se o JSON definir 40, ele será respeitado.
|
||||
fps=int(seg_config.get("camera_fps", 20)),
|
||||
target_size=seg_config.get("ia_resolution", [1024, 640]),
|
||||
module_calibration_json=contract[
|
||||
"module_params_path"
|
||||
],
|
||||
width=fallback_w,
|
||||
height=fallback_h,
|
||||
# FPS continua sendo política operacional.
|
||||
fps=int(
|
||||
seg_config.get(
|
||||
"camera_fps",
|
||||
20,
|
||||
)
|
||||
),
|
||||
target_size=target_size,
|
||||
require_product_contract=bool(
|
||||
contract.get(
|
||||
"require_product_contract",
|
||||
True,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
if not nova.iniciado:
|
||||
|
|
@ -392,6 +942,62 @@ class CameraManager:
|
|||
pass
|
||||
return None
|
||||
|
||||
if bool(
|
||||
contract.get(
|
||||
"require_product_contract",
|
||||
True,
|
||||
)
|
||||
):
|
||||
if not bool(
|
||||
getattr(
|
||||
nova,
|
||||
"product_contract",
|
||||
False,
|
||||
)
|
||||
):
|
||||
raise RuntimeError(
|
||||
"CameraMultispectral iniciou sem contrato produto."
|
||||
)
|
||||
|
||||
camera_tensor_size = list(
|
||||
getattr(
|
||||
nova,
|
||||
"tensor_size",
|
||||
[],
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
if camera_tensor_size != target_size:
|
||||
raise RuntimeError(
|
||||
"CameraMultispectral divergiu do tamanho final: "
|
||||
f"camera={camera_tensor_size} "
|
||||
f"contrato={target_size}"
|
||||
)
|
||||
|
||||
camera_sizes = dict(
|
||||
getattr(
|
||||
nova,
|
||||
"sensor_size_by_role",
|
||||
{},
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
expected_sizes = dict(
|
||||
contract.get(
|
||||
"sensor_size_by_role",
|
||||
{},
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
if camera_sizes != expected_sizes:
|
||||
raise RuntimeError(
|
||||
"CameraMultispectral divergiu do hardware homologado: "
|
||||
f"camera={camera_sizes} esperado={expected_sizes}"
|
||||
)
|
||||
|
||||
return nova
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -400,12 +1006,51 @@ class CameraManager:
|
|||
nova.parar()
|
||||
except Exception:
|
||||
pass
|
||||
self.mostrar_log(f"⚠️ Camera com ID {mx_id} não conectada: {e}")
|
||||
|
||||
self.mostrar_log(
|
||||
f"⚠️ Camera com ID {mx_id} não conectada: {e}"
|
||||
)
|
||||
return None
|
||||
|
||||
def _inicializar_modelo(self):
|
||||
requested = (
|
||||
self.seg_config.get("onnx_model_path")
|
||||
or self.seg_config.get("ia_model_path")
|
||||
or self.seg_config.get("model_path")
|
||||
or self.seg_config.get("onnx_path")
|
||||
)
|
||||
|
||||
if self.model_svc is not None:
|
||||
return
|
||||
current = getattr(
|
||||
self.model_svc,
|
||||
"onnx_path",
|
||||
None,
|
||||
)
|
||||
|
||||
current_norm = self._path_normalizado(
|
||||
current
|
||||
)
|
||||
|
||||
requested_norm = self._path_normalizado(
|
||||
requested
|
||||
)
|
||||
|
||||
if (
|
||||
current_norm
|
||||
and requested_norm
|
||||
and current_norm != requested_norm
|
||||
):
|
||||
self.mostrar_log(
|
||||
"[weed][MODEL] modelo mudou entre sessões; "
|
||||
f"recriando runtime | antigo={current_norm} "
|
||||
f"novo={requested_norm}"
|
||||
)
|
||||
|
||||
# Libera a referência. O ORT/TRT anterior será coletado
|
||||
# quando não houver mais referências ao serviço.
|
||||
self.model_svc = None
|
||||
else:
|
||||
return
|
||||
|
||||
self.model_svc = MultiSpecSegformerService(
|
||||
model_config=self.seg_config,
|
||||
|
|
@ -414,7 +1059,8 @@ class CameraManager:
|
|||
|
||||
self.mostrar_log(
|
||||
f"[weed][MODEL] backend={getattr(self.model_svc, 'runtime_backend', 'onnx')} "
|
||||
f"runtime_mode={getattr(self.model_svc, 'runtime_mode', None)}"
|
||||
f"runtime_mode={getattr(self.model_svc, 'runtime_mode', None)} "
|
||||
f"input_shape={getattr(self.model_svc, 'onnx_input_shape', None)}"
|
||||
)
|
||||
|
||||
def _inicializar_detector(self):
|
||||
|
|
@ -689,6 +1335,11 @@ class CameraManager:
|
|||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
self._validar_tensor_camera(
|
||||
tensor5,
|
||||
contexto="warmup",
|
||||
)
|
||||
|
||||
frame_id = int(
|
||||
res.get("frame_id")
|
||||
or res.get("async_packet_seq")
|
||||
|
|
@ -910,6 +1561,7 @@ class CameraManager:
|
|||
from weed_worker.config import load_seg_config
|
||||
|
||||
cfg = load_seg_config()
|
||||
cfg = self._validar_config_dinamica_contrato(cfg)
|
||||
self.seg_config = cfg
|
||||
self.qtd_bicos = int(cfg.get("qtd_bicos", self.qtd_bicos or 7) or 7)
|
||||
self.posproc_intervalo_min_s = float(cfg.get("posproc_intervalo_min_s", self.posproc_intervalo_min_s))
|
||||
|
|
@ -974,7 +1626,9 @@ class CameraManager:
|
|||
|
||||
motivos = []
|
||||
|
||||
if self.camera is None:
|
||||
if not self._pipeline_contract.get("validated", False):
|
||||
motivos.append("contrato ONNX/module_params não validado")
|
||||
elif self.camera is None:
|
||||
motivos.append("câmera indisponível")
|
||||
elif self.iniciando:
|
||||
motivos.append("câmera inicializando")
|
||||
|
|
@ -1010,6 +1664,23 @@ class CameraManager:
|
|||
"timeout_tensor_s": timeout_tensor,
|
||||
"timeout_inferencia_s": timeout_inferencia,
|
||||
"timeout_deteccao_s": timeout_deteccao,
|
||||
"contract": {
|
||||
"validated": bool(
|
||||
self._pipeline_contract.get("validated", False)
|
||||
),
|
||||
"product_contract": bool(
|
||||
self._pipeline_contract.get("product_contract", False)
|
||||
),
|
||||
"tensor_size": list(
|
||||
self._pipeline_contract.get("tensor_size", []) or []
|
||||
),
|
||||
"onnx_input_channels": list(
|
||||
self._pipeline_contract.get("onnx_input_channels", []) or []
|
||||
),
|
||||
"module_params_path": self._pipeline_contract.get(
|
||||
"module_params_path"
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
self.pipeline_ia_ok = ok
|
||||
|
|
@ -1017,6 +1688,20 @@ class CameraManager:
|
|||
|
||||
return resultado
|
||||
|
||||
def get_pipeline_contract(self):
|
||||
"""Snapshot somente-leitura do contrato ONNX + MP da sessão atual."""
|
||||
try:
|
||||
return json.loads(
|
||||
json.dumps(
|
||||
self._pipeline_contract,
|
||||
default=str,
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
return dict(
|
||||
self._pipeline_contract
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Caches
|
||||
# ============================================================
|
||||
|
|
@ -1415,6 +2100,11 @@ class CameraManager:
|
|||
time.sleep(0.50 if reiniciou else 0.05)
|
||||
continue
|
||||
|
||||
self._validar_tensor_camera(
|
||||
tensor5,
|
||||
contexto="capture",
|
||||
)
|
||||
|
||||
if not self._sessao_camera_valida(camera_atual, generation):
|
||||
self.perf.inc("tensor_descartado_geracao")
|
||||
continue
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue