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.lblTipoMovimento = new System.Windows.Forms.Label();
|
||||||
this.btnSalvarSentido = new System.Windows.Forms.Button();
|
this.btnSalvarSentido = new System.Windows.Forms.Button();
|
||||||
this.gpbDF = new System.Windows.Forms.GroupBox();
|
this.gpbDF = new System.Windows.Forms.GroupBox();
|
||||||
|
this.chbDFUsar = new System.Windows.Forms.CheckBox();
|
||||||
this.label5 = new System.Windows.Forms.Label();
|
this.label5 = new System.Windows.Forms.Label();
|
||||||
this.txtDFOffsetAngulo = new System.Windows.Forms.TextBox();
|
this.txtDFOffsetAngulo = new System.Windows.Forms.TextBox();
|
||||||
this.label2 = new System.Windows.Forms.Label();
|
this.label2 = new System.Windows.Forms.Label();
|
||||||
|
|
@ -47,6 +48,7 @@ namespace AgroBase.Forms.Direcional
|
||||||
this.lblDFEsquerda = new System.Windows.Forms.Label();
|
this.lblDFEsquerda = new System.Windows.Forms.Label();
|
||||||
this.lblDFDireita = new System.Windows.Forms.Label();
|
this.lblDFDireita = new System.Windows.Forms.Label();
|
||||||
this.gpbEF = new System.Windows.Forms.GroupBox();
|
this.gpbEF = new System.Windows.Forms.GroupBox();
|
||||||
|
this.chbEFUsar = new System.Windows.Forms.CheckBox();
|
||||||
this.label6 = new System.Windows.Forms.Label();
|
this.label6 = new System.Windows.Forms.Label();
|
||||||
this.txtEFOffsetAngulo = new System.Windows.Forms.TextBox();
|
this.txtEFOffsetAngulo = new System.Windows.Forms.TextBox();
|
||||||
this.label4 = new System.Windows.Forms.Label();
|
this.label4 = new System.Windows.Forms.Label();
|
||||||
|
|
@ -58,6 +60,7 @@ namespace AgroBase.Forms.Direcional
|
||||||
this.lblEFEsquerda = new System.Windows.Forms.Label();
|
this.lblEFEsquerda = new System.Windows.Forms.Label();
|
||||||
this.lblEFDireita = new System.Windows.Forms.Label();
|
this.lblEFDireita = new System.Windows.Forms.Label();
|
||||||
this.gpbDT = new System.Windows.Forms.GroupBox();
|
this.gpbDT = new System.Windows.Forms.GroupBox();
|
||||||
|
this.chbDTUsar = new System.Windows.Forms.CheckBox();
|
||||||
this.label8 = new System.Windows.Forms.Label();
|
this.label8 = new System.Windows.Forms.Label();
|
||||||
this.txtDTOffsetAngulo = new System.Windows.Forms.TextBox();
|
this.txtDTOffsetAngulo = new System.Windows.Forms.TextBox();
|
||||||
this.label3 = new System.Windows.Forms.Label();
|
this.label3 = new System.Windows.Forms.Label();
|
||||||
|
|
@ -69,6 +72,7 @@ namespace AgroBase.Forms.Direcional
|
||||||
this.lblDTEsquerda = new System.Windows.Forms.Label();
|
this.lblDTEsquerda = new System.Windows.Forms.Label();
|
||||||
this.lblDTDireita = new System.Windows.Forms.Label();
|
this.lblDTDireita = new System.Windows.Forms.Label();
|
||||||
this.gpbET = new System.Windows.Forms.GroupBox();
|
this.gpbET = new System.Windows.Forms.GroupBox();
|
||||||
|
this.chbETUsar = new System.Windows.Forms.CheckBox();
|
||||||
this.label7 = new System.Windows.Forms.Label();
|
this.label7 = new System.Windows.Forms.Label();
|
||||||
this.txtETOffsetAngulo = new System.Windows.Forms.TextBox();
|
this.txtETOffsetAngulo = new System.Windows.Forms.TextBox();
|
||||||
this.label1 = new System.Windows.Forms.Label();
|
this.label1 = new System.Windows.Forms.Label();
|
||||||
|
|
@ -79,10 +83,14 @@ namespace AgroBase.Forms.Direcional
|
||||||
this.cmbETEsquerda = new System.Windows.Forms.ComboBox();
|
this.cmbETEsquerda = new System.Windows.Forms.ComboBox();
|
||||||
this.lblETEsquerda = new System.Windows.Forms.Label();
|
this.lblETEsquerda = new System.Windows.Forms.Label();
|
||||||
this.lblETDireita = new System.Windows.Forms.Label();
|
this.lblETDireita = new System.Windows.Forms.Label();
|
||||||
this.chbDFUsar = new System.Windows.Forms.CheckBox();
|
this.txtDTOffsetRef = new System.Windows.Forms.TextBox();
|
||||||
this.chbDTUsar = new System.Windows.Forms.CheckBox();
|
this.label9 = new System.Windows.Forms.Label();
|
||||||
this.chbEFUsar = new System.Windows.Forms.CheckBox();
|
this.label10 = new System.Windows.Forms.Label();
|
||||||
this.chbETUsar = new System.Windows.Forms.CheckBox();
|
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.gpbSentido.SuspendLayout();
|
||||||
this.gpbDF.SuspendLayout();
|
this.gpbDF.SuspendLayout();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.nudDFOffset)).BeginInit();
|
((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.Margin = new System.Windows.Forms.Padding(2);
|
||||||
this.gpbSentido.Name = "gpbSentido";
|
this.gpbSentido.Name = "gpbSentido";
|
||||||
this.gpbSentido.Padding = new System.Windows.Forms.Padding(2);
|
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.TabIndex = 80;
|
||||||
this.gpbSentido.TabStop = false;
|
this.gpbSentido.TabStop = false;
|
||||||
this.gpbSentido.Text = "Sentido de giro";
|
this.gpbSentido.Text = "Sentido de giro";
|
||||||
|
|
@ -117,7 +125,7 @@ namespace AgroBase.Forms.Direcional
|
||||||
// lblIP
|
// lblIP
|
||||||
//
|
//
|
||||||
this.lblIP.AutoSize = true;
|
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.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||||
this.lblIP.Name = "lblIP";
|
this.lblIP.Name = "lblIP";
|
||||||
this.lblIP.Size = new System.Drawing.Size(17, 13);
|
this.lblIP.Size = new System.Drawing.Size(17, 13);
|
||||||
|
|
@ -126,7 +134,7 @@ namespace AgroBase.Forms.Direcional
|
||||||
//
|
//
|
||||||
// txtIP
|
// 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.Margin = new System.Windows.Forms.Padding(2);
|
||||||
this.txtIP.Name = "txtIP";
|
this.txtIP.Name = "txtIP";
|
||||||
this.txtIP.Size = new System.Drawing.Size(85, 20);
|
this.txtIP.Size = new System.Drawing.Size(85, 20);
|
||||||
|
|
@ -154,7 +162,7 @@ namespace AgroBase.Forms.Direcional
|
||||||
//
|
//
|
||||||
// btnSalvarSentido
|
// 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.Margin = new System.Windows.Forms.Padding(2);
|
||||||
this.btnSalvarSentido.Name = "btnSalvarSentido";
|
this.btnSalvarSentido.Name = "btnSalvarSentido";
|
||||||
this.btnSalvarSentido.Size = new System.Drawing.Size(88, 24);
|
this.btnSalvarSentido.Size = new System.Drawing.Size(88, 24);
|
||||||
|
|
@ -165,6 +173,8 @@ namespace AgroBase.Forms.Direcional
|
||||||
//
|
//
|
||||||
// gpbDF
|
// gpbDF
|
||||||
//
|
//
|
||||||
|
this.gpbDF.Controls.Add(this.label12);
|
||||||
|
this.gpbDF.Controls.Add(this.txtDFOffsetRef);
|
||||||
this.gpbDF.Controls.Add(this.chbDFUsar);
|
this.gpbDF.Controls.Add(this.chbDFUsar);
|
||||||
this.gpbDF.Controls.Add(this.label5);
|
this.gpbDF.Controls.Add(this.label5);
|
||||||
this.gpbDF.Controls.Add(this.txtDFOffsetAngulo);
|
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.Margin = new System.Windows.Forms.Padding(2);
|
||||||
this.gpbDF.Name = "gpbDF";
|
this.gpbDF.Name = "gpbDF";
|
||||||
this.gpbDF.Padding = new System.Windows.Forms.Padding(2);
|
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.TabIndex = 9;
|
||||||
this.gpbDF.TabStop = false;
|
this.gpbDF.TabStop = false;
|
||||||
this.gpbDF.Text = "M4 - Direito Frente (DF)";
|
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
|
// label5
|
||||||
//
|
//
|
||||||
this.label5.AutoSize = true;
|
this.label5.AutoSize = true;
|
||||||
|
|
@ -294,6 +314,8 @@ namespace AgroBase.Forms.Direcional
|
||||||
//
|
//
|
||||||
// gpbEF
|
// gpbEF
|
||||||
//
|
//
|
||||||
|
this.gpbEF.Controls.Add(this.label11);
|
||||||
|
this.gpbEF.Controls.Add(this.txtEFOffsetRef);
|
||||||
this.gpbEF.Controls.Add(this.chbEFUsar);
|
this.gpbEF.Controls.Add(this.chbEFUsar);
|
||||||
this.gpbEF.Controls.Add(this.label6);
|
this.gpbEF.Controls.Add(this.label6);
|
||||||
this.gpbEF.Controls.Add(this.txtEFOffsetAngulo);
|
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.Margin = new System.Windows.Forms.Padding(2);
|
||||||
this.gpbEF.Name = "gpbEF";
|
this.gpbEF.Name = "gpbEF";
|
||||||
this.gpbEF.Padding = new System.Windows.Forms.Padding(2);
|
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.TabIndex = 9;
|
||||||
this.gpbEF.TabStop = false;
|
this.gpbEF.TabStop = false;
|
||||||
this.gpbEF.Text = "M2 - Esquerdo Frente (EF)";
|
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
|
// label6
|
||||||
//
|
//
|
||||||
this.label6.AutoSize = true;
|
this.label6.AutoSize = true;
|
||||||
|
|
@ -423,6 +455,8 @@ namespace AgroBase.Forms.Direcional
|
||||||
//
|
//
|
||||||
// gpbDT
|
// gpbDT
|
||||||
//
|
//
|
||||||
|
this.gpbDT.Controls.Add(this.label9);
|
||||||
|
this.gpbDT.Controls.Add(this.txtDTOffsetRef);
|
||||||
this.gpbDT.Controls.Add(this.chbDTUsar);
|
this.gpbDT.Controls.Add(this.chbDTUsar);
|
||||||
this.gpbDT.Controls.Add(this.label8);
|
this.gpbDT.Controls.Add(this.label8);
|
||||||
this.gpbDT.Controls.Add(this.txtDTOffsetAngulo);
|
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.cmbDTEsquerda);
|
||||||
this.gpbDT.Controls.Add(this.lblDTEsquerda);
|
this.gpbDT.Controls.Add(this.lblDTEsquerda);
|
||||||
this.gpbDT.Controls.Add(this.lblDTDireita);
|
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.Margin = new System.Windows.Forms.Padding(2);
|
||||||
this.gpbDT.Name = "gpbDT";
|
this.gpbDT.Name = "gpbDT";
|
||||||
this.gpbDT.Padding = new System.Windows.Forms.Padding(2);
|
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.TabIndex = 9;
|
||||||
this.gpbDT.TabStop = false;
|
this.gpbDT.TabStop = false;
|
||||||
this.gpbDT.Text = "M3 - Direito Trás (DT)";
|
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
|
// label8
|
||||||
//
|
//
|
||||||
this.label8.AutoSize = true;
|
this.label8.AutoSize = true;
|
||||||
|
|
@ -552,6 +596,8 @@ namespace AgroBase.Forms.Direcional
|
||||||
//
|
//
|
||||||
// gpbET
|
// gpbET
|
||||||
//
|
//
|
||||||
|
this.gpbET.Controls.Add(this.label10);
|
||||||
|
this.gpbET.Controls.Add(this.txtETOffsetRef);
|
||||||
this.gpbET.Controls.Add(this.chbETUsar);
|
this.gpbET.Controls.Add(this.chbETUsar);
|
||||||
this.gpbET.Controls.Add(this.label7);
|
this.gpbET.Controls.Add(this.label7);
|
||||||
this.gpbET.Controls.Add(this.txtETOffsetAngulo);
|
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.cmbETEsquerda);
|
||||||
this.gpbET.Controls.Add(this.lblETEsquerda);
|
this.gpbET.Controls.Add(this.lblETEsquerda);
|
||||||
this.gpbET.Controls.Add(this.lblETDireita);
|
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.Margin = new System.Windows.Forms.Padding(2);
|
||||||
this.gpbET.Name = "gpbET";
|
this.gpbET.Name = "gpbET";
|
||||||
this.gpbET.Padding = new System.Windows.Forms.Padding(2);
|
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.TabIndex = 0;
|
||||||
this.gpbET.TabStop = false;
|
this.gpbET.TabStop = false;
|
||||||
this.gpbET.Text = "M1 - Esquerdo Trás (ET)";
|
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
|
// label7
|
||||||
//
|
//
|
||||||
this.label7.AutoSize = true;
|
this.label7.AutoSize = true;
|
||||||
|
|
@ -679,52 +735,88 @@ namespace AgroBase.Forms.Direcional
|
||||||
this.lblETDireita.TabIndex = 2;
|
this.lblETDireita.TabIndex = 2;
|
||||||
this.lblETDireita.Text = "Direita";
|
this.lblETDireita.Text = "Direita";
|
||||||
//
|
//
|
||||||
// chbDFUsar
|
// txtDTOffsetRef
|
||||||
//
|
//
|
||||||
this.chbDFUsar.AutoSize = true;
|
this.txtDTOffsetRef.Location = new System.Drawing.Point(65, 146);
|
||||||
this.chbDFUsar.Location = new System.Drawing.Point(127, 124);
|
this.txtDTOffsetRef.Margin = new System.Windows.Forms.Padding(2);
|
||||||
this.chbDFUsar.Name = "chbDFUsar";
|
this.txtDTOffsetRef.Name = "txtDTOffsetRef";
|
||||||
this.chbDFUsar.Size = new System.Drawing.Size(32, 17);
|
this.txtDTOffsetRef.Size = new System.Drawing.Size(57, 20);
|
||||||
this.chbDFUsar.TabIndex = 19;
|
this.txtDTOffsetRef.TabIndex = 24;
|
||||||
this.chbDFUsar.Text = "L";
|
this.txtDTOffsetRef.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
|
||||||
this.chbDFUsar.UseVisualStyleBackColor = true;
|
|
||||||
//
|
//
|
||||||
// chbDTUsar
|
// label9
|
||||||
//
|
//
|
||||||
this.chbDTUsar.AutoSize = true;
|
this.label9.AutoSize = true;
|
||||||
this.chbDTUsar.Location = new System.Drawing.Point(125, 124);
|
this.label9.Location = new System.Drawing.Point(10, 149);
|
||||||
this.chbDTUsar.Name = "chbDTUsar";
|
this.label9.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||||
this.chbDTUsar.Size = new System.Drawing.Size(32, 17);
|
this.label9.Name = "label9";
|
||||||
this.chbDTUsar.TabIndex = 23;
|
this.label9.Size = new System.Drawing.Size(55, 13);
|
||||||
this.chbDTUsar.Text = "L";
|
this.label9.TabIndex = 25;
|
||||||
this.chbDTUsar.UseVisualStyleBackColor = true;
|
this.label9.Text = "Offset Ref";
|
||||||
//
|
//
|
||||||
// chbEFUsar
|
// label10
|
||||||
//
|
//
|
||||||
this.chbEFUsar.AutoSize = true;
|
this.label10.AutoSize = true;
|
||||||
this.chbEFUsar.Location = new System.Drawing.Point(127, 124);
|
this.label10.Location = new System.Drawing.Point(10, 149);
|
||||||
this.chbEFUsar.Name = "chbEFUsar";
|
this.label10.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||||
this.chbEFUsar.Size = new System.Drawing.Size(32, 17);
|
this.label10.Name = "label10";
|
||||||
this.chbEFUsar.TabIndex = 21;
|
this.label10.Size = new System.Drawing.Size(55, 13);
|
||||||
this.chbEFUsar.Text = "L";
|
this.label10.TabIndex = 27;
|
||||||
this.chbEFUsar.UseVisualStyleBackColor = true;
|
this.label10.Text = "Offset Ref";
|
||||||
//
|
//
|
||||||
// chbETUsar
|
// txtETOffsetRef
|
||||||
//
|
//
|
||||||
this.chbETUsar.AutoSize = true;
|
this.txtETOffsetRef.Location = new System.Drawing.Point(65, 146);
|
||||||
this.chbETUsar.Location = new System.Drawing.Point(127, 124);
|
this.txtETOffsetRef.Margin = new System.Windows.Forms.Padding(2);
|
||||||
this.chbETUsar.Name = "chbETUsar";
|
this.txtETOffsetRef.Name = "txtETOffsetRef";
|
||||||
this.chbETUsar.Size = new System.Drawing.Size(32, 17);
|
this.txtETOffsetRef.Size = new System.Drawing.Size(57, 20);
|
||||||
this.chbETUsar.TabIndex = 23;
|
this.txtETOffsetRef.TabIndex = 26;
|
||||||
this.chbETUsar.Text = "L";
|
this.txtETOffsetRef.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
|
||||||
this.chbETUsar.UseVisualStyleBackColor = true;
|
//
|
||||||
|
// 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
|
// frmDirConfig
|
||||||
//
|
//
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
this.BackColor = System.Drawing.Color.White;
|
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.Controls.Add(this.gpbSentido);
|
||||||
this.Margin = new System.Windows.Forms.Padding(2);
|
this.Margin = new System.Windows.Forms.Padding(2);
|
||||||
this.Name = "frmDirConfig";
|
this.Name = "frmDirConfig";
|
||||||
|
|
@ -805,5 +897,13 @@ namespace AgroBase.Forms.Direcional
|
||||||
private System.Windows.Forms.CheckBox chbEFUsar;
|
private System.Windows.Forms.CheckBox chbEFUsar;
|
||||||
private System.Windows.Forms.CheckBox chbDTUsar;
|
private System.Windows.Forms.CheckBox chbDTUsar;
|
||||||
private System.Windows.Forms.CheckBox chbETUsar;
|
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");
|
TextBox txtOffsetAngulo = FuncoesGlobais.FindControlRecursive<TextBox>(gpbSentido, "txt" + Modulo.Modulo_ID + "OffsetAngulo");
|
||||||
txtOffsetAngulo.Text = Modulo.DirMotor.OffsetAnguloReal.ToString();
|
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");
|
CheckBox chbUsar = FuncoesGlobais.FindControlRecursive<CheckBox>(gpbSentido, "chb" + Modulo.Modulo_ID + "Usar");
|
||||||
chbUsar.Checked = Modulo.DirMotor.UsarAnguloSensor;
|
chbUsar.Checked = Modulo.DirMotor.UsarAnguloSensor;
|
||||||
}
|
}
|
||||||
|
|
@ -119,6 +122,9 @@ namespace AgroBase.Forms.Direcional
|
||||||
TextBox txtOffsetAngulo = FuncoesGlobais.FindControlRecursive<TextBox>(gpbSentido, "txt" + Modulo.Modulo_ID + "OffsetAngulo");
|
TextBox txtOffsetAngulo = FuncoesGlobais.FindControlRecursive<TextBox>(gpbSentido, "txt" + Modulo.Modulo_ID + "OffsetAngulo");
|
||||||
Modulo.DirMotor.OffsetAnguloReal = Convert.ToDouble(txtOffsetAngulo.Text);
|
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");
|
CheckBox chbUsar = FuncoesGlobais.FindControlRecursive<CheckBox>(gpbSentido, "chb" + Modulo.Modulo_ID + "Usar");
|
||||||
Modulo.DirMotor.UsarAnguloSensor = chbUsar.Checked;
|
Modulo.DirMotor.UsarAnguloSensor = chbUsar.Checked;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -339,13 +339,28 @@ namespace AgroBase.Models.Modules
|
||||||
{
|
{
|
||||||
var op = Variaveis.OperacaoEmAndamento;
|
var op = Variaveis.OperacaoEmAndamento;
|
||||||
|
|
||||||
if (op.CalibrandoRuntime)
|
if (op == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
long tickSolicitado = Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
lock (_metricasLock)
|
lock (_metricasLock)
|
||||||
{
|
{
|
||||||
long agoraTicks = Stopwatch.GetTimestamp();
|
long agoraTicks = Stopwatch.GetTimestamp();
|
||||||
DateTime agora = DateTime.Now;
|
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 operacaoConcluida = op.Sensoriamento?.Operacao?.StatusOperacaoAtual == StatusOperacao.Concluido;
|
||||||
bool operacaoAtiva = op.Sensoriamento?.Operacao?.OperacaoIniciada == true && !operacaoConcluida;
|
bool operacaoAtiva = op.Sensoriamento?.Operacao?.OperacaoIniciada == true && !operacaoConcluida;
|
||||||
|
|
@ -411,10 +426,11 @@ namespace AgroBase.Models.Modules
|
||||||
: new HashSet<int>();
|
: new HashSet<int>();
|
||||||
|
|
||||||
double vazaoAtualMLs = operacaoAtiva ? ObterVazaoAtualConfiavelMLs(agora) : 0.0;
|
double vazaoAtualMLs = operacaoAtiva ? ObterVazaoAtualConfiavelMLs(agora) : 0.0;
|
||||||
double tempoMovimentoAtualSeg = op.Sensoriamento?.Movimentacao?.TempoMovimentoSegs ?? 0.0;
|
|
||||||
|
|
||||||
if (_ultimoTickMetricas != 0)
|
if (_ultimoTickMetricas != 0)
|
||||||
{
|
{
|
||||||
|
bool haviaAtuacaoNoIntervaloAnterior = _bicosAtivosIntervaloAnterior.Count > 0 || _bombasAtivasIntervaloAnterior.Count > 0 || _vazaoIntervaloAnteriorMLs > 0.01;
|
||||||
|
|
||||||
double dt = (agoraTicks - _ultimoTickMetricas) / (double)Stopwatch.Frequency;
|
double dt = (agoraTicks - _ultimoTickMetricas) / (double)Stopwatch.Frequency;
|
||||||
|
|
||||||
if (dt > 0 && dt <= IntervaloMaximoIntegracaoSeg)
|
if (dt > 0 && dt <= IntervaloMaximoIntegracaoSeg)
|
||||||
|
|
@ -443,8 +459,26 @@ namespace AgroBase.Models.Modules
|
||||||
|
|
||||||
if (dt > IntervaloMaximoIntegracaoSeg)
|
if (dt > IntervaloMaximoIntegracaoSeg)
|
||||||
{
|
{
|
||||||
Variaveis.MostrarLog("[ATU/METRICAS] Integração ignorada por dt alto: " + dt.ToString("0.000") + "s");
|
string mensagem =
|
||||||
op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Alerta, 50, "[ATU/METRICAS] Integração ignorada por dt alto: " + dt.ToString("0.000") + "s");
|
"[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)
|
private void IntegrarIntervaloAnterior(double dt, double tempoMovimentoIntervaloSeg)
|
||||||
{
|
{
|
||||||
var bicosDoIntervalo = (BicosPulverizadores ??
|
var bicosDoIntervalo = (BicosPulverizadores ??
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ namespace AgroBase.Models.Modules
|
||||||
public Sentido Sentido_SP { get; set; } = Sentido.Parado;
|
public Sentido Sentido_SP { get; set; } = Sentido.Parado;
|
||||||
public double Angulo_SP { get; set; } = 0;
|
public double Angulo_SP { get; set; } = 0;
|
||||||
public double FolgaMecanica { get; set; } = 0;
|
public double FolgaMecanica { get; set; } = 0;
|
||||||
|
public double OffsetReferenciamento { get; set; } = 0;
|
||||||
public double Velocidade_Max { get; set; } = 600;
|
public double Velocidade_Max { get; set; } = 600;
|
||||||
public int Velocidade
|
public int Velocidade
|
||||||
{
|
{
|
||||||
|
|
@ -149,11 +150,16 @@ namespace AgroBase.Models.Modules
|
||||||
return !Comandar || Sentido_SP == Sentido.Parado ? 0 : Angulo_SP;
|
return !Comandar || Sentido_SP == Sentido.Parado ? 0 : Angulo_SP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public double ErroAnguloSP
|
public double ErroAngulo
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return Math.Abs(AnguloLeitura - AnguloSPAtual);
|
double anguloSp =
|
||||||
|
!Comandar || Sentido_SP == Sentido.Parado
|
||||||
|
? 0
|
||||||
|
: Angulo_SP;
|
||||||
|
|
||||||
|
return anguloSp - AnguloLeitura;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool RetornandoAzero
|
public bool RetornandoAzero
|
||||||
|
|
@ -165,13 +171,7 @@ namespace AgroBase.Models.Modules
|
||||||
return foraZero && parar;
|
return foraZero && parar;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool EmMovimento
|
public bool EmMovimento { get; set; }
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return Math.Abs(RPM) > 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public bool AtingiuAnguloSP
|
public bool AtingiuAnguloSP
|
||||||
{
|
{
|
||||||
get
|
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 Dictionary<TipoMovimentoDirecional, ConfiguracaoSentidoMotor> ConfigSentidos { get; set; }
|
||||||
public List<FuncoesPinout> Funcoes { get; set; }
|
public List<FuncoesPinout> Funcoes { get; set; }
|
||||||
|
|
||||||
|
|
@ -313,38 +416,41 @@ namespace AgroBase.Models.Modules
|
||||||
var _Controle = op.Controle;
|
var _Controle = op.Controle;
|
||||||
int AnguloTipoMovimento = VariaveisEquipamento.AnguloMovimento[_Controle.TipoMovimento];
|
int AnguloTipoMovimento = VariaveisEquipamento.AnguloMovimento[_Controle.TipoMovimento];
|
||||||
|
|
||||||
|
Sentido novoSentido = Sentido.Parado;
|
||||||
|
double novoAngulo = 0;
|
||||||
|
|
||||||
if (!Comandar)
|
if (!Comandar)
|
||||||
{
|
{
|
||||||
Sentido_SP = Sentido.Parado;
|
novoSentido = Sentido.Parado;
|
||||||
}
|
}
|
||||||
else if (Direcao == Direcao.Direita)
|
else if (Direcao == Direcao.Direita)
|
||||||
{
|
{
|
||||||
Sentido_SP = ConfigSentidos[_Controle.TipoMovimento].Direita;
|
novoSentido = ConfigSentidos[_Controle.TipoMovimento].Direita;
|
||||||
}
|
}
|
||||||
else if (Direcao == Direcao.Esquerda)
|
else if (Direcao == Direcao.Esquerda)
|
||||||
{
|
{
|
||||||
Sentido_SP = ConfigSentidos[_Controle.TipoMovimento].Esquerda;
|
novoSentido = ConfigSentidos[_Controle.TipoMovimento].Esquerda;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Sentido_SP = Sentido.Parado;
|
novoSentido = Sentido.Parado;
|
||||||
}
|
}
|
||||||
|
|
||||||
UltimaDirecao = Direcao;
|
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;
|
UltimaDirecao = Direcao.Parado;
|
||||||
}
|
}
|
||||||
else if (AnguloTipoMovimento != -1)
|
else if (AnguloTipoMovimento != -1)
|
||||||
{
|
{
|
||||||
Angulo_SP = AnguloTipoMovimento;
|
novoAngulo = AnguloTipoMovimento;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Angulo_SP = _Controle.Angulo;
|
novoAngulo = _Controle.Angulo;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Comandar)
|
if (Comandar)
|
||||||
|
|
@ -353,8 +459,16 @@ namespace AgroBase.Models.Modules
|
||||||
//Console.WriteLine($"[{Mod_ID}] Angulo Atualizado para {Variaveis.OperacaoEmAndamento.SimulacaoAnguloControle}");
|
//Console.WriteLine($"[{Mod_ID}] Angulo Atualizado para {Variaveis.OperacaoEmAndamento.SimulacaoAnguloControle}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Angulo_SP = Math.Abs(Angulo_SP) * mx;
|
novoAngulo = Math.Abs(novoAngulo) * mx;
|
||||||
Angulo_SP += AnguloFolgaCompensar;
|
novoAngulo += AnguloFolgaCompensar;
|
||||||
|
|
||||||
|
if (Math.Abs(novoAngulo - Angulo_SP) >= 0.05)
|
||||||
|
{
|
||||||
|
UltimaMudancaAnguloSP = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
Angulo_SP = novoAngulo;
|
||||||
|
Sentido_SP = novoSentido;
|
||||||
|
|
||||||
if (_Simulando)
|
if (_Simulando)
|
||||||
{
|
{
|
||||||
|
|
@ -473,9 +587,7 @@ namespace AgroBase.Models.Modules
|
||||||
_sentido = _sentido == Sentido.Horario ? Sentido.Antihorario : Sentido.Horario;
|
_sentido = _sentido == Sentido.Horario ? Sentido.Antihorario : Sentido.Horario;
|
||||||
Variaveis.MostrarLog($"[DirecionalModel] [ReferenciaMotorRelativo] [DIR {Mod_ID}] - Invertendo Sentido de Giro para {_sentido.ToString()}");
|
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, OffsetReferenciamento);
|
||||||
|
|
||||||
_anguloSP = CalcularCentroCompensado(AnguloEntrada, AnguloSaida, _sentido, offset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
verificacoesAngulo++;
|
verificacoesAngulo++;
|
||||||
|
|
|
||||||
|
|
@ -494,6 +494,8 @@ namespace AgroBase.Models.Modules
|
||||||
modulo.DirMotor.EnviarComandoControle(controleDir.UltimaDirecao);
|
modulo.DirMotor.EnviarComandoControle(controleDir.UltimaDirecao);
|
||||||
enviouControle = true;
|
enviouControle = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modulo.DirMotor.AtualizarSaudeSeguimentoAngular(agora);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enviouControle)
|
if (enviouControle)
|
||||||
|
|
|
||||||
|
|
@ -625,9 +625,9 @@ namespace AgroBase.Models.Modules
|
||||||
posicao = CanMessagePosicaoDados.Dados1,
|
posicao = CanMessagePosicaoDados.Dados1,
|
||||||
indice = 0,
|
indice = 0,
|
||||||
analise_health = true,
|
analise_health = true,
|
||||||
maximo = 5.2,
|
maximo = 5.3,
|
||||||
minimo = 4.9,
|
minimo = 4.75,
|
||||||
nominal = 5.0,
|
nominal = 5.1,
|
||||||
unidade_medida = "V"
|
unidade_medida = "V"
|
||||||
},
|
},
|
||||||
new SensorValoresLeituraModel()
|
new SensorValoresLeituraModel()
|
||||||
|
|
@ -636,9 +636,9 @@ namespace AgroBase.Models.Modules
|
||||||
posicao = CanMessagePosicaoDados.Dados1,
|
posicao = CanMessagePosicaoDados.Dados1,
|
||||||
indice = 1,
|
indice = 1,
|
||||||
analise_health = true,
|
analise_health = true,
|
||||||
maximo = 1500,
|
maximo = 800,
|
||||||
minimo = 100,
|
minimo = 100,
|
||||||
nominal = 280,
|
nominal = 430,
|
||||||
unidade_medida = "mA"
|
unidade_medida = "mA"
|
||||||
},
|
},
|
||||||
new SensorValoresLeituraModel()
|
new SensorValoresLeituraModel()
|
||||||
|
|
|
||||||
|
|
@ -4094,6 +4094,21 @@ namespace AgroBase.Models
|
||||||
|
|
||||||
op.TempoIniciarOperacao = Math.Max(0, Convert.ToInt32(Math.Round(tempoAguardarInicio)));
|
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()
|
var operacaoAtualizada = new OperacaoSensoriamentoLogModel()
|
||||||
{
|
{
|
||||||
Momento = agora,
|
Momento = agora,
|
||||||
|
|
@ -4102,7 +4117,7 @@ namespace AgroBase.Models
|
||||||
Status = op.Status,
|
Status = op.Status,
|
||||||
StatusOperacaoAnterior = Operacao.StatusOperacaoAnterior,
|
StatusOperacaoAnterior = Operacao.StatusOperacaoAnterior,
|
||||||
OperacaoIniciada = Operacao.OperacaoIniciada,
|
OperacaoIniciada = Operacao.OperacaoIniciada,
|
||||||
Emergencia = Operacao.Emergencia,
|
Emergencia = emergencia,
|
||||||
Pausa = Operacao.Pausa,
|
Pausa = Operacao.Pausa,
|
||||||
Calibrando = Operacao.Calibrando,
|
Calibrando = Operacao.Calibrando,
|
||||||
Finalizando = Operacao.Finalizando,
|
Finalizando = Operacao.Finalizando,
|
||||||
|
|
@ -4889,7 +4904,7 @@ namespace AgroBase.Models
|
||||||
? (velocidade / velocidadeMax) * 100.0
|
? (velocidade / velocidadeMax) * 100.0
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
double erroAngulo = dir != null ? CalcularErroAngulo(dir) : 0;
|
double erroAngulo = dir?.ErroAngulo ?? 0;
|
||||||
|
|
||||||
return new DirecionalModuloLogModel
|
return new DirecionalModuloLogModel
|
||||||
{
|
{
|
||||||
|
|
@ -4919,6 +4934,7 @@ namespace AgroBase.Models
|
||||||
|
|
||||||
ErroAngulo = Math.Round(erroAngulo, 2),
|
ErroAngulo = Math.Round(erroAngulo, 2),
|
||||||
ErroAnguloAbs = Math.Round(Math.Abs(erroAngulo), 2),
|
ErroAnguloAbs = Math.Round(Math.Abs(erroAngulo), 2),
|
||||||
|
ErroSeguimentoAngularPersistente = dir?.ErroSeguimentoAngularPersistente ?? false,
|
||||||
AtingiuAnguloSP = dir?.AtingiuAnguloSP ?? false,
|
AtingiuAnguloSP = dir?.AtingiuAnguloSP ?? false,
|
||||||
RetornandoAzero = dir?.RetornandoAzero ?? 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)
|
DirecionalStatusLogModel CriarStatus((bool, List<string>) status)
|
||||||
{
|
{
|
||||||
return new DirecionalStatusLogModel
|
return new DirecionalStatusLogModel
|
||||||
|
|
@ -5021,7 +5024,7 @@ namespace AgroBase.Models
|
||||||
|
|
||||||
model.AnguloMedioAbs = MediaOuZero(controlando, x => Math.Abs(x.DirMotor.AnguloLeitura));
|
model.AnguloMedioAbs = MediaOuZero(controlando, x => Math.Abs(x.DirMotor.AnguloLeitura));
|
||||||
model.AnguloSPMedioAbs = MediaOuZero(controlando, x => Math.Abs(x.DirMotor.Angulo_SP));
|
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 =
|
model.Status =
|
||||||
dados != null && controle != null
|
dados != null && controle != null
|
||||||
|
|
@ -5999,11 +6002,9 @@ namespace AgroBase.Models
|
||||||
|
|
||||||
public DirecionalStatusLogModel Status { get; set; } = new DirecionalStatusLogModel();
|
public DirecionalStatusLogModel Status { get; set; } = new DirecionalStatusLogModel();
|
||||||
|
|
||||||
public bool StatusGeralOk =>
|
public bool StatusEstruturalOk => Disponivel && Status.Ok && ModulosInicializados > 0;
|
||||||
Disponivel &&
|
|
||||||
Status.Ok &&
|
public bool StatusGeralOk => StatusEstruturalOk && !Modulos.Any(x => x.Controlar && x.ErroSeguimentoAngularPersistente);
|
||||||
ModulosInicializados > 0 &&
|
|
||||||
!Modulos.Any(x => x.ErroAnguloAbs > 3.0 && x.Controlar);
|
|
||||||
|
|
||||||
public List<DirecionalModuloLogModel> Modulos { get; set; } = new List<DirecionalModuloLogModel>();
|
public List<DirecionalModuloLogModel> Modulos { get; set; } = new List<DirecionalModuloLogModel>();
|
||||||
|
|
||||||
|
|
@ -6078,6 +6079,7 @@ namespace AgroBase.Models
|
||||||
// Diagnóstico geométrico
|
// Diagnóstico geométrico
|
||||||
public double ErroAngulo { get; set; }
|
public double ErroAngulo { get; set; }
|
||||||
public double ErroAnguloAbs { get; set; }
|
public double ErroAnguloAbs { get; set; }
|
||||||
|
public bool ErroSeguimentoAngularPersistente { get; set; }
|
||||||
public bool AtingiuAnguloSP { get; set; }
|
public bool AtingiuAnguloSP { get; set; }
|
||||||
public bool RetornandoAzero { get; set; }
|
public bool RetornandoAzero { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1228,6 +1228,8 @@ namespace AgroBase.Services
|
||||||
short rpm = DecodeInt16BE(data, 1);
|
short rpm = DecodeInt16BE(data, 1);
|
||||||
|
|
||||||
item.RPM.valor = rpm;
|
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)
|
private void ProcessarPulsosRecebidos(MKS057DModel item, byte[] data)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AgroBase.Models;
|
using AgroBase.Models;
|
||||||
|
using AgroBase.Models.Modules;
|
||||||
using AgroBase.Models.Operadores;
|
using AgroBase.Models.Operadores;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -788,15 +789,30 @@ namespace AgroBase.Services.Operadores
|
||||||
})
|
})
|
||||||
.ToList();
|
.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 =
|
double idadeUltimoComandoServoMs =
|
||||||
servo.UltimoComandoEnviado == DateTime.MinValue
|
!possuiComandoServo
|
||||||
? 999999
|
? 999999
|
||||||
: (agora_dt - servo.UltimoComandoEnviado).TotalMilliseconds;
|
: Math.Max(0, (agora_dt - servo.UltimoComandoEnviado).TotalMilliseconds);
|
||||||
|
|
||||||
double idadeUltimaRespostaServoMs =
|
double idadeUltimaRespostaServoMs =
|
||||||
servo.UltimaLeitura == DateTime.MinValue
|
!possuiRespostaServo
|
||||||
? 999999
|
? 999999
|
||||||
: (agora_dt - servo.UltimaLeitura).TotalMilliseconds;
|
: Math.Max(0, (agora_dt - ultimaRespostaAnguloServo).TotalMilliseconds);
|
||||||
|
|
||||||
|
bool telemetriaPosComandoServo =
|
||||||
|
possuiComandoServo &&
|
||||||
|
possuiRespostaServo &&
|
||||||
|
ultimaRespostaAnguloServo >= servo.UltimoComandoEnviado;
|
||||||
|
|
||||||
var servoObj = new
|
var servoObj = new
|
||||||
{
|
{
|
||||||
|
|
@ -806,15 +822,21 @@ namespace AgroBase.Services.Operadores
|
||||||
tipo,
|
tipo,
|
||||||
controlar = servo.Controlar,
|
controlar = servo.Controlar,
|
||||||
mandatorio = servo.Mandatorio,
|
mandatorio = servo.Mandatorio,
|
||||||
|
|
||||||
angulo_sp = servo._AnguloControle,
|
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,
|
dados,
|
||||||
timestamp = agora,
|
timestamp = agora,
|
||||||
saude = saude ?? new { },
|
saude = saude ?? new { },
|
||||||
|
|
||||||
angulo_desejado = servo.UltimoAnguloDesejado,
|
|
||||||
angulo_leitura = servo._AnguloLeiutra,
|
|
||||||
ultimo_comando_ms = idadeUltimoComandoServoMs,
|
|
||||||
ultima_resposta_ms = idadeUltimaRespostaServoMs,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
senServosAtivos.Add($"{tipo}.{id}");
|
senServosAtivos.Add($"{tipo}.{id}");
|
||||||
|
|
@ -843,15 +865,30 @@ namespace AgroBase.Services.Operadores
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
bool possuiComandoRele = rele.UltimoComandoEnviado != DateTime.MinValue;
|
DateTime ultimaRespostaEstadoRele =
|
||||||
bool possuiRespostaRele = rele.UltimaLeitura != DateTime.MinValue;
|
rele.ValoresLeituras?
|
||||||
double idadeUltimoComandoReleMs = !possuiComandoRele ? 999999 : Math.Max(0, (agora_dt - rele.UltimoComandoEnviado).TotalMilliseconds);
|
.Where(x =>x.funcao == FuncoesPinout.EstadoLeitura)
|
||||||
double idadeUltimaRespostaReleMs = !possuiRespostaRele ? 999999 : Math.Max(0, (agora_dt - rele.UltimaLeitura).TotalMilliseconds);
|
.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 &&
|
possuiComandoRele &&
|
||||||
possuiRespostaRele &&
|
possuiRespostaRele &&
|
||||||
rele.UltimaLeitura >= rele.UltimoComandoEnviado;
|
ultimaRespostaEstadoRele >= rele.UltimoComandoEnviado;
|
||||||
|
|
||||||
var releObj = new
|
var releObj = new
|
||||||
{
|
{
|
||||||
|
|
@ -870,7 +907,7 @@ namespace AgroBase.Services.Operadores
|
||||||
estado_leitura = rele._EstadoLeitura,
|
estado_leitura = rele._EstadoLeitura,
|
||||||
|
|
||||||
possui_comando = possuiComandoRele,
|
possui_comando = possuiComandoRele,
|
||||||
telemetria_pos_comando = telemetriaPosComando,
|
telemetria_pos_comando = telemetriaPosComandoRele,
|
||||||
|
|
||||||
ultimo_comando_ms = idadeUltimoComandoReleMs,
|
ultimo_comando_ms = idadeUltimoComandoReleMs,
|
||||||
ultima_resposta_ms = idadeUltimaRespostaReleMs,
|
ultima_resposta_ms = idadeUltimaRespostaReleMs,
|
||||||
|
|
@ -902,15 +939,50 @@ namespace AgroBase.Services.Operadores
|
||||||
})
|
})
|
||||||
.ToList();
|
.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 =
|
double idadeUltimoComandoLedMs =
|
||||||
led.UltimoComandoEnviado == DateTime.MinValue
|
!possuiComandoLed
|
||||||
? 999999
|
? 999999
|
||||||
: (agora_dt - led.UltimoComandoEnviado).TotalMilliseconds;
|
: Math.Max(0, (agora_dt - led.UltimoComandoEnviado).TotalMilliseconds);
|
||||||
|
|
||||||
double idadeUltimaRespostaLedMs =
|
double idadeUltimaRespostaLedMs =
|
||||||
led.UltimaLeitura == DateTime.MinValue
|
!possuiRespostaLed
|
||||||
? 999999
|
? 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
|
var ledObj = new
|
||||||
{
|
{
|
||||||
|
|
@ -920,16 +992,24 @@ namespace AgroBase.Services.Operadores
|
||||||
tipo,
|
tipo,
|
||||||
controlar = led.Controlar,
|
controlar = led.Controlar,
|
||||||
mandatorio = led.Mandatorio,
|
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,
|
dados,
|
||||||
timestamp = agora,
|
timestamp = agora,
|
||||||
saude = saude ?? new { },
|
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}");
|
senLedsAtivos.Add($"{tipo}.{id}");
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
import hashlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
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
|
from camera_worker.camera_imu import IMUCamera
|
||||||
|
|
||||||
|
|
||||||
|
CAMERA_MULTISPECTRAL_VERSION = "production_v1_2026_08_24"
|
||||||
|
|
||||||
|
|
||||||
class CameraMultispectral:
|
class CameraMultispectral:
|
||||||
"""
|
"""
|
||||||
Adaptador do módulo OAK-FFC-3 multiespectral para o ecossistema do robô.
|
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.
|
A classe NÃO expõe lógica radiométrica/fusão/flatfield para o worker.
|
||||||
Isso fica dentro do core OAK-FCC-3.
|
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__(
|
def __init__(
|
||||||
|
|
@ -41,6 +48,7 @@ class CameraMultispectral:
|
||||||
target_size=None,
|
target_size=None,
|
||||||
cache_max_age_s=0.15,
|
cache_max_age_s=0.15,
|
||||||
timeout_s=1.0,
|
timeout_s=1.0,
|
||||||
|
require_product_contract=False,
|
||||||
):
|
):
|
||||||
self.mostrar_log = mostrar_log
|
self.mostrar_log = mostrar_log
|
||||||
self.mx_id = str(mx_id) if mx_id else None
|
self.mx_id = str(mx_id) if mx_id else None
|
||||||
|
|
@ -61,11 +69,42 @@ class CameraMultispectral:
|
||||||
self.rodando = False
|
self.rodando = False
|
||||||
self.ultima_saude = {}
|
self.ultima_saude = {}
|
||||||
|
|
||||||
self.width = int(width)
|
# width/height recebidos do caller são somente compatibilidade legado.
|
||||||
self.height = int(height)
|
# 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.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.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._cache_max_age_s = float(cache_max_age_s)
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
@ -115,6 +154,9 @@ class CameraMultispectral:
|
||||||
module_calibration_json = self._resolver_module_params_default()
|
module_calibration_json = self._resolver_module_params_default()
|
||||||
|
|
||||||
self.module_calibration_json = module_calibration_json
|
self.module_calibration_json = module_calibration_json
|
||||||
|
self.module_params_sha256 = self._sha256_file_safe(
|
||||||
|
self.module_calibration_json
|
||||||
|
)
|
||||||
|
|
||||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
ContextoGlobalRedis.CamKey(self.mx_id),
|
ContextoGlobalRedis.CamKey(self.mx_id),
|
||||||
|
|
@ -137,8 +179,10 @@ class CameraMultispectral:
|
||||||
try:
|
try:
|
||||||
self.client = OakFcc3Client(
|
self.client = OakFcc3Client(
|
||||||
mx_id=self.mx_id,
|
mx_id=self.mx_id,
|
||||||
width=self.width,
|
# Fallbacks legado. O Client produto ignora estes valores como
|
||||||
height=self.height,
|
# autoridade física e usa o module_params homologado.
|
||||||
|
width=self.legacy_width,
|
||||||
|
height=self.legacy_height,
|
||||||
bayer="BGGR",
|
bayer="BGGR",
|
||||||
fps=self.fps,
|
fps=self.fps,
|
||||||
frame_type="RAW_BRUTO",
|
frame_type="RAW_BRUTO",
|
||||||
|
|
@ -146,15 +190,31 @@ class CameraMultispectral:
|
||||||
capture_mode="TRIPLE",
|
capture_mode="TRIPLE",
|
||||||
raw_policy="require_triple",
|
raw_policy="require_triple",
|
||||||
module_calibration_json=self.module_calibration_json,
|
module_calibration_json=self.module_calibration_json,
|
||||||
sync_mode="best",
|
sync_mode="strict",
|
||||||
sync_tolerance_ms=25.0,
|
hardware_sync_enabled=False,
|
||||||
|
frame_sync_master="CAM_A",
|
||||||
|
sync_tolerance_ms=15.0,
|
||||||
|
|
||||||
imu_modo=self.imu_modo,
|
imu_modo=self.imu_modo,
|
||||||
imu_freq_hz=self.imu_freq_hz,
|
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)
|
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.
|
# Se mx_id veio None, pega o ID real aberto pelo core antes de criar IMU.
|
||||||
try:
|
try:
|
||||||
self.mx_id = str(getattr(self.client, "mx_id", None) or self.mx_id)
|
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}")
|
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.atualizar_ctx_dict(
|
||||||
ContextoGlobalRedis.CamKey(self.mx_id),
|
ContextoGlobalRedis.CamKey(self.mx_id),
|
||||||
mx_id=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.
|
# Devolve o default mesmo se não existir, para o erro ficar claro no core.
|
||||||
return "calibration/module_params.json"
|
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):
|
def _montar_parametros(self, status=None, start_resp=None):
|
||||||
status = status or {}
|
status = status or {}
|
||||||
|
|
||||||
altura_camera = 111.0
|
altura_camera = 111.0
|
||||||
|
|
||||||
# Valores medidos por você anteriormente:
|
# FOV prático histórico medido no conjunto mecânico.
|
||||||
# FOV prático: 96 x 157 cm a 111 cm de altura.
|
# É mantido como referência operacional, mas não como calibração óptica.
|
||||||
largura_real = 157.0
|
largura_real = 157.0
|
||||||
altura_real = 96.0
|
altura_real = 96.0
|
||||||
|
|
||||||
cm_por_px_x = largura_real / float(self.width)
|
tensor_w = int(self.tensor_size[0])
|
||||||
cm_por_px_y = altura_real / float(self.height)
|
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 = {
|
parametros = {
|
||||||
"tipo": "multispectral",
|
"tipo": "multispectral",
|
||||||
|
"adapter_version": CAMERA_MULTISPECTRAL_VERSION,
|
||||||
|
"product_contract": bool(self.product_contract),
|
||||||
"module_calibration_json": self.module_calibration_json,
|
"module_calibration_json": self.module_calibration_json,
|
||||||
|
"module_params_sha256": self.module_params_sha256,
|
||||||
"frame_type": "RAW_BRUTO",
|
"frame_type": "RAW_BRUTO",
|
||||||
"channels": ["R", "G", "B", "RE", "NIR"],
|
"channels": ["R", "G", "B", "RE", "NIR"],
|
||||||
"output_layout": "CHW",
|
"output_layout": "CHW",
|
||||||
"dtype": "float32",
|
"dtype": "float32",
|
||||||
"tensor_min": 0.0,
|
"tensor_min": 0.0,
|
||||||
"tensor_max": 1.0,
|
"tensor_max": 1.0,
|
||||||
"rgb_width": self.width,
|
|
||||||
"rgb_height": self.height,
|
"reference_camera": "rgb",
|
||||||
"tensor_width": self.width if self.target_size is None else int(self.target_size[0]),
|
"rgb_native_size": list(self.rgb_native_size),
|
||||||
"tensor_height": self.height if self.target_size is None else int(self.target_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,
|
||||||
|
|
||||||
|
# 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,
|
"fps": self.fps,
|
||||||
|
|
||||||
"altura_camera": altura_camera,
|
"altura_camera": altura_camera,
|
||||||
"largura_real": largura_real,
|
"largura_real": largura_real,
|
||||||
"altura_real": altura_real,
|
"altura_real": altura_real,
|
||||||
"cm_por_px_x": cm_por_px_x,
|
"cm_por_px_x": cm_por_px_x,
|
||||||
"cm_por_px_y": cm_por_px_y,
|
"cm_por_px_y": cm_por_px_y,
|
||||||
|
"cm_por_px_nominal": True,
|
||||||
|
|
||||||
"status": status,
|
"status": status,
|
||||||
"start_resp": start_resp,
|
"start_resp": start_resp,
|
||||||
|
"client_contract": json.loads(
|
||||||
|
json.dumps(self.client_contract, default=str)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return parametros
|
return parametros
|
||||||
|
|
@ -557,6 +786,15 @@ class CameraMultispectral:
|
||||||
f"Tensor multiespectral deveria ter 5 canais, veio shape={tensor.shape}"
|
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
|
t_validate_ms = (time.perf_counter() - t0) * 1000.0
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -595,6 +833,13 @@ class CameraMultispectral:
|
||||||
"shape": list(tensor.shape),
|
"shape": list(tensor.shape),
|
||||||
"dtype": str(tensor.dtype),
|
"dtype": str(tensor.dtype),
|
||||||
"channels": ["R", "G", "B", "RE", "NIR"],
|
"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),
|
"frame_id": int(meta.get("frame_id", 0) or 0),
|
||||||
"async_packet_seq": int(
|
"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:
|
with self._lock:
|
||||||
self.ultimo_tensor_multispec = tensor
|
self.ultimo_tensor_multispec = tensor
|
||||||
self.ultimo_meta = meta
|
self.ultimo_meta = meta
|
||||||
|
|
@ -674,7 +935,10 @@ class CameraMultispectral:
|
||||||
|
|
||||||
def requisitar_frame_rgb(self, force: bool = False, max_age_s: float = None):
|
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:
|
try:
|
||||||
agora = time.time()
|
agora = time.time()
|
||||||
|
|
@ -682,28 +946,59 @@ class CameraMultispectral:
|
||||||
max_age_s = self._cache_max_age_s
|
max_age_s = self._cache_max_age_s
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
cache_ok = (
|
preview_ok = (
|
||||||
self.ultimo_frame_rgb is not None
|
self.ultimo_frame_rgb is not None
|
||||||
and self.timestamp_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
|
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)
|
return self.ultimo_frame_rgb, dict(self._ultimo_resultado_rgb)
|
||||||
|
|
||||||
# Atualiza tensor, que por consequência atualiza preview RGB.
|
tensor_cache = self.ultimo_tensor_multispec
|
||||||
tensor, res = self.requisitar_tensor_multispec(force=force, max_age_s=max_age_s)
|
ts_tensor = self.timestamp_ultimo_tensor
|
||||||
|
|
||||||
with self._lock:
|
tensor_fresco = (
|
||||||
if self.ultimo_frame_rgb is not None:
|
tensor_cache is not None
|
||||||
return self.ultimo_frame_rgb, dict(self._ultimo_resultado_rgb)
|
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, {
|
return None, {
|
||||||
"erro": res.get("erro") or "preview RGB indisponível",
|
"erro": res.get("erro") or "tensor indisponível para preview",
|
||||||
"duracao": res.get("duracao", 0.0),
|
"duracao": res.get("duracao", 0.0),
|
||||||
"frame_valido": False,
|
"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:
|
||||||
|
self.ultimo_frame_rgb = preview
|
||||||
|
self.timestamp_ultimo_frame_rgb = time.time()
|
||||||
|
self._ultimo_resultado_rgb = resultado
|
||||||
|
|
||||||
|
return preview, resultado
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.mostrar_log(f"[CameraMultispectral] Erro ao requisitar RGB: {e}")
|
self.mostrar_log(f"[CameraMultispectral] Erro ao requisitar RGB: {e}")
|
||||||
return None, {
|
return None, {
|
||||||
|
|
@ -1341,8 +1636,12 @@ class CameraMultispectral:
|
||||||
core = getattr(self.client, "core", None)
|
core = getattr(self.client, "core", None)
|
||||||
if core is not None and hasattr(core, "fuse_multispec_cameras"):
|
if core is not None and hasattr(core, "fuse_multispec_cameras"):
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
out = core.fuse_multispec_cameras(decoded_fixed, meta, 5)
|
out = core.fuse_multispec_cameras(
|
||||||
out = core.resize_tensor_chw(out, target_size=self.target_size)
|
decoded_fixed,
|
||||||
|
meta,
|
||||||
|
5,
|
||||||
|
target_size=self.target_size,
|
||||||
|
)
|
||||||
|
|
||||||
#t_core_build_ms = (time.perf_counter() - t0) * 1000.0
|
#t_core_build_ms = (time.perf_counter() - t0) * 1000.0
|
||||||
#t_total_ms = (time.perf_counter() - t_total0) * 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("role", role)
|
||||||
meta_item.setdefault("socket", info.get("socket", cam_id))
|
meta_item.setdefault("socket", info.get("socket", cam_id))
|
||||||
meta_item.setdefault("sensor", info.get("sensor"))
|
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
|
fixed["meta"] = meta_item
|
||||||
|
|
||||||
out[cam_id] = fixed
|
out[cam_id] = fixed
|
||||||
|
|
@ -1423,6 +1732,16 @@ class CameraMultispectral:
|
||||||
"role": role,
|
"role": role,
|
||||||
"socket": info.get("socket", cam_id),
|
"socket": info.get("socket", cam_id),
|
||||||
"sensor": info.get("sensor"),
|
"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,
|
"timestamp": (meta.get("timestamps") or {}).get(cam_id) if isinstance(meta, dict) else None,
|
||||||
"shape": list(item.shape),
|
"shape": list(item.shape),
|
||||||
"dtype": str(item.dtype),
|
"dtype": str(item.dtype),
|
||||||
|
|
@ -1441,6 +1760,28 @@ class CameraMultispectral:
|
||||||
# Salvamento científico / pós-processamento
|
# 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:
|
def _ts_name(self) -> str:
|
||||||
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
|
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
|
||||||
|
|
||||||
|
|
@ -1466,6 +1807,20 @@ class CameraMultispectral:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
raw_meta = dict(self.ultimo_meta or {})
|
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"}
|
required = {"CAM_A", "CAM_B", "CAM_C"}
|
||||||
presentes = set(raw_frame.keys())
|
presentes = set(raw_frame.keys())
|
||||||
faltando = sorted(required - presentes)
|
faltando = sorted(required - presentes)
|
||||||
|
|
@ -1526,9 +1881,9 @@ class CameraMultispectral:
|
||||||
preview = self.client.build_save_preview_from_cam_a(
|
preview = self.client.build_save_preview_from_cam_a(
|
||||||
packed_raw_by_camera=raw_frame,
|
packed_raw_by_camera=raw_frame,
|
||||||
meta_stream=raw_meta,
|
meta_stream=raw_meta,
|
||||||
sensor_width=self.width,
|
sensor_width=int(self.rgb_native_size[0]),
|
||||||
sensor_height=self.height,
|
sensor_height=int(self.rgb_native_size[1]),
|
||||||
bayer_pattern="BGGR",
|
bayer_pattern=self.bayer_pattern,
|
||||||
)
|
)
|
||||||
|
|
||||||
if preview is not None:
|
if preview is not None:
|
||||||
|
|
@ -1641,9 +1996,22 @@ class CameraMultispectral:
|
||||||
"mx_id": self.mx_id,
|
"mx_id": self.mx_id,
|
||||||
"module_calibration_json": self.module_calibration_json,
|
"module_calibration_json": self.module_calibration_json,
|
||||||
|
|
||||||
"sensor_width": self.width,
|
# Compatibilidade root: sensor_width/height representam CAM_A/RGB.
|
||||||
"sensor_height": self.height,
|
"reference_camera": "rgb",
|
||||||
"bayer_pattern": "BGGR",
|
"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,
|
"fps_target": self.fps,
|
||||||
|
|
||||||
"frame_type": "RAW_BRUTO",
|
"frame_type": "RAW_BRUTO",
|
||||||
|
|
@ -1662,6 +2030,10 @@ class CameraMultispectral:
|
||||||
"stream_meta": raw_meta,
|
"stream_meta": raw_meta,
|
||||||
"actual_camera_controls": self.client.get_current_camera_controls() if self.client else None,
|
"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,
|
"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,
|
"resultado": resultado,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ from .radiometric_controller import RadiometricController
|
||||||
|
|
||||||
PHYSICAL_CHANNEL_NAMES = ("R", "G", "B", "RE", "NIR")
|
PHYSICAL_CHANNEL_NAMES = ("R", "G", "B", "RE", "NIR")
|
||||||
PHYSICAL_CHANNEL_COUNT = len(PHYSICAL_CHANNEL_NAMES)
|
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:
|
class OakFcc3Client:
|
||||||
|
|
@ -46,58 +49,105 @@ class OakFcc3Client:
|
||||||
mx_id=None,
|
mx_id=None,
|
||||||
imu_modo="rotation_vector",
|
imu_modo="rotation_vector",
|
||||||
imu_freq_hz=200,
|
imu_freq_hz=200,
|
||||||
|
evaluate_quality=True,
|
||||||
|
require_product_contract=False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.width = width
|
# width/height/bayer recebidos do caller ficam apenas como fallback legado.
|
||||||
self.height = height
|
# Em module_params de produção, hardware real + MP são a autoridade.
|
||||||
self.bayer = bayer
|
self.legacy_width = int(width)
|
||||||
self.fps = fps
|
self.legacy_height = int(height)
|
||||||
self.frame_type = frame_type
|
self.legacy_bayer = str(bayer or "BGGR").upper()
|
||||||
self.output_dtype = output_dtype
|
|
||||||
self.capture_mode = capture_mode
|
self.fps = float(fps)
|
||||||
self.raw_policy = raw_policy
|
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_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.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_modo = str(imu_modo).strip().lower()
|
||||||
self.imu_freq_hz = int(imu_freq_hz)
|
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
|
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(
|
self.svc = OakFcc3Service(
|
||||||
timeout=10,
|
timeout=10,
|
||||||
fps=fps,
|
fps=self.fps,
|
||||||
width=width,
|
width=self.width,
|
||||||
height=height,
|
height=self.height,
|
||||||
frame_type=frame_type,
|
frame_type=self.frame_type,
|
||||||
output_dtype=output_dtype,
|
output_dtype=self.output_dtype,
|
||||||
capture_mode=capture_mode,
|
capture_mode=self.capture_mode,
|
||||||
raw_policy=raw_policy,
|
raw_policy=self.raw_policy,
|
||||||
sync_mode=sync_mode,
|
sync_mode=sync_mode,
|
||||||
sync_tolerance_ms=sync_tolerance_ms,
|
sync_tolerance_ms=sync_tolerance_ms,
|
||||||
mx_id=self.mx_id,
|
mx_id=self.mx_id,
|
||||||
module_calibration_json=module_calibration_json,
|
module_calibration_json=module_calibration_json,
|
||||||
|
module_params=self.module_params,
|
||||||
|
require_product_contract=self.require_product_contract,
|
||||||
imu_modo=self.imu_modo,
|
imu_modo=self.imu_modo,
|
||||||
imu_freq_hz=self.imu_freq_hz,
|
imu_freq_hz=self.imu_freq_hz,
|
||||||
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.applied_camera_controls = {}
|
self.applied_camera_controls = {}
|
||||||
self.radiometric_controller = None
|
self.radiometric_controller = None
|
||||||
|
self.radiometric_controller_enabled = False
|
||||||
|
|
||||||
self.core = RawProcessorCore(
|
self.core = RawProcessorCore(
|
||||||
sensor_width=width,
|
sensor_width=self.rgb_native_width,
|
||||||
sensor_height=height,
|
sensor_height=self.rgb_native_height,
|
||||||
bayer_pattern=bayer,
|
bayer_pattern=self.bayer,
|
||||||
calibration_json_path=module_calibration_json,
|
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(
|
self.preview = RawProcessorPreview(
|
||||||
sensor_width=width,
|
sensor_width=self.rgb_native_width,
|
||||||
sensor_height=height,
|
sensor_height=self.rgb_native_height,
|
||||||
bayer_pattern=bayer,
|
bayer_pattern=self.bayer,
|
||||||
)
|
)
|
||||||
|
self._preview_cache = {
|
||||||
|
(self.rgb_native_width, self.rgb_native_height, self.bayer): self.preview,
|
||||||
|
}
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.start()
|
self.start()
|
||||||
|
|
@ -106,19 +156,247 @@ class OakFcc3Client:
|
||||||
def __exit__(self, exc_type, exc, tb):
|
def __exit__(self, exc_type, exc, tb):
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
def _load_module_params(self, path):
|
def _load_module_params(self, path, required=False):
|
||||||
if not path or not os.path.isfile(path):
|
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 {}
|
return {}
|
||||||
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
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):
|
def apply_module_camera_settings(self):
|
||||||
camera_settings = self.module_params.get("camera_settings", {}) or {}
|
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):
|
if not isinstance(settings, dict):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -137,17 +415,43 @@ class OakFcc3Client:
|
||||||
}
|
}
|
||||||
|
|
||||||
self.applied_camera_controls = applied
|
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
|
return applied
|
||||||
|
|
||||||
def enable_radiometric_controller(self):
|
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(
|
self.radiometric_controller = RadiometricController(
|
||||||
client=self,
|
client=self,
|
||||||
config_json_path=self.module_calibration_json,
|
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.sync_from_actual_camera_controls()
|
||||||
|
self.radiometric_controller_enabled = bool(
|
||||||
|
getattr(self.radiometric_controller, "enabled", True)
|
||||||
|
)
|
||||||
return self.radiometric_controller
|
return self.radiometric_controller
|
||||||
|
|
||||||
def update_radiometry(self, decoded, meta=None):
|
def update_radiometry(self, decoded, meta=None):
|
||||||
|
|
@ -187,17 +491,45 @@ class OakFcc3Client:
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
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:
|
||||||
|
# Se a validação de contrato falhar, fecha hardware antes de propagar.
|
||||||
|
try:
|
||||||
|
self.svc.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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()
|
applied = self.apply_module_camera_settings()
|
||||||
|
|
||||||
|
self.enable_radiometric_controller()
|
||||||
|
|
||||||
if print_debug:
|
if print_debug:
|
||||||
print("[OAK CLIENT] START:", resp)
|
print("[OAK CLIENT] START:", resp)
|
||||||
|
print("[OAK CLIENT] CONTRACT:", self.get_contract())
|
||||||
print("[OAK CLIENT] APPLIED CAMERA SETTINGS:", applied)
|
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
|
return resp
|
||||||
|
|
||||||
|
|
@ -209,7 +541,29 @@ class OakFcc3Client:
|
||||||
return self.svc.get_device_metrics()
|
return self.svc.get_device_metrics()
|
||||||
|
|
||||||
def get_status(self):
|
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):
|
def get_next_raw_frame(self, timeout=1.0):
|
||||||
return self.svc.capture_frame(timeout=timeout)
|
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()
|
frame_type = str(raw_meta.get("frame_type", self.frame_type)).upper()
|
||||||
meta = dict(raw_meta)
|
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":
|
if frame_type == "RAW_BRUTO":
|
||||||
decoded = self.decode_stream_cameras(raw_frame, raw_meta)
|
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):
|
def build_infer_tensor(self, frame, meta, channels_expected, target_size=None):
|
||||||
channels_expected = self._validate_physical_channel_count(channels_expected)
|
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(
|
return self.core.build_infer_tensor_from_stream(
|
||||||
frame,
|
frame,
|
||||||
meta,
|
meta,
|
||||||
|
|
@ -327,19 +706,57 @@ class OakFcc3Client:
|
||||||
target_size=target_size,
|
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)
|
channels_expected = self._validate_physical_channel_count(channels_expected)
|
||||||
tensor = self.core.fuse_multispec_cameras(decoded, meta, channels_expected)
|
target_size = self._resolve_target_size(target_size)
|
||||||
tensor = self.core.resize_tensor_chw(tensor, 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 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)
|
tensor = self.core.apply_patch_normalization_to_tensor(tensor)
|
||||||
|
|
||||||
|
if evaluate_quality:
|
||||||
self.core.last_frame_quality_result = self.core.evaluate_frame_quality(tensor)
|
self.core.last_frame_quality_result = self.core.evaluate_frame_quality(tensor)
|
||||||
return 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):
|
def decode_stream_cameras(self, frame, meta):
|
||||||
if str(meta.get("frame_type", self.frame_type)).upper() == "PREVIEW":
|
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))
|
tensor = np.transpose(rgb01.astype(np.float32), (2, 0, 1))
|
||||||
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
|
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(
|
tensor = self.build_infer_tensor_from_decoded(
|
||||||
decoded=decoded,
|
decoded=decoded,
|
||||||
meta=meta,
|
meta=meta,
|
||||||
channels_expected=5,
|
channels_expected=5,
|
||||||
target_size=target_size,
|
target_size=target_size,
|
||||||
|
evaluate_quality=evaluate_quality,
|
||||||
)
|
)
|
||||||
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
|
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
|
||||||
|
|
||||||
|
|
@ -448,10 +872,14 @@ class OakFcc3Client:
|
||||||
arr = arr[:, :, 0]
|
arr = arr[:, :, 0]
|
||||||
|
|
||||||
if bit_depth == 10 and arr.ndim == 2:
|
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(
|
raw16 = self.core.unpack_raw10_packed(
|
||||||
arr,
|
arr,
|
||||||
sensor_width=int(info.get("width", self.width)),
|
sensor_width=int(info.get("width", role_size[0])),
|
||||||
sensor_height=int(info.get("height", self.height)),
|
sensor_height=int(info.get("height", role_size[1])),
|
||||||
)
|
)
|
||||||
|
|
||||||
if role == "rgb":
|
if role == "rgb":
|
||||||
|
|
@ -513,7 +941,7 @@ class OakFcc3Client:
|
||||||
or cam_meta.get("bayer")
|
or cam_meta.get("bayer")
|
||||||
or stream_meta.get("bayer_pattern")
|
or stream_meta.get("bayer_pattern")
|
||||||
or bayer_pattern
|
or bayer_pattern
|
||||||
or "RGGB"
|
or self.bayer
|
||||||
)
|
)
|
||||||
bayer = str(bayer).upper()
|
bayer = str(bayer).upper()
|
||||||
|
|
||||||
|
|
@ -545,19 +973,17 @@ class OakFcc3Client:
|
||||||
real_w = int(cam_meta.get("width", sensor_width))
|
real_w = int(cam_meta.get("width", sensor_width))
|
||||||
real_h = int(cam_meta.get("height", sensor_height))
|
real_h = int(cam_meta.get("height", sensor_height))
|
||||||
|
|
||||||
core = RawProcessorCore(
|
cache_key = (real_w, real_h, bayer)
|
||||||
sensor_width=real_w,
|
preview = self._preview_cache.get(cache_key)
|
||||||
sensor_height=real_h,
|
if preview is None:
|
||||||
bayer_pattern=bayer,
|
|
||||||
)
|
|
||||||
|
|
||||||
preview = RawProcessorPreview(
|
preview = RawProcessorPreview(
|
||||||
sensor_width=real_w,
|
sensor_width=real_w,
|
||||||
sensor_height=real_h,
|
sensor_height=real_h,
|
||||||
bayer_pattern=bayer,
|
bayer_pattern=bayer,
|
||||||
)
|
)
|
||||||
|
self._preview_cache[cache_key] = preview
|
||||||
|
|
||||||
raw16 = core.unpack_raw10_packed(
|
raw16 = self.core.unpack_raw10_packed(
|
||||||
packed,
|
packed,
|
||||||
sensor_width=real_w,
|
sensor_width=real_w,
|
||||||
sensor_height=real_h,
|
sensor_height=real_h,
|
||||||
|
|
@ -624,6 +1050,11 @@ class OakFcc3Client:
|
||||||
|
|
||||||
|
|
||||||
def decode_oak_aligned_multispec(self, frame, meta):
|
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.
|
Decodifica frames já alinhados pela OAK.
|
||||||
|
|
||||||
|
|
@ -672,6 +1103,11 @@ class OakFcc3Client:
|
||||||
return decoded
|
return decoded
|
||||||
|
|
||||||
def build_multispec_tensor_from_oak_aligned(self, decoded, meta=None):
|
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.
|
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 cv2
|
||||||
import numpy as np
|
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:
|
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()
|
|
||||||
|
|
||||||
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
|
|
||||||
) -> np.ndarray:
|
|
||||||
"""
|
"""
|
||||||
Conversão para visualização:
|
Preview RAW Bayer para debug/stream/salvamento visual.
|
||||||
- auto-level
|
|
||||||
- gamma
|
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.
|
||||||
"""
|
"""
|
||||||
max_val = float((1 << bit_depth) - 1)
|
|
||||||
|
|
||||||
raw = raw16.astype(np.float32)
|
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 black_level is None:
|
if self.sensor_width <= 0 or self.sensor_height <= 0:
|
||||||
black_level = float(raw.min())
|
raise ValueError(
|
||||||
if white_level is None:
|
"Resolução inválida para preview: "
|
||||||
white_level = float(raw.max())
|
f"{self.sensor_width}x{self.sensor_height}"
|
||||||
|
)
|
||||||
|
|
||||||
if white_level <= black_level:
|
self.bayer_pattern = str(
|
||||||
norm = raw / max_val
|
bayer_pattern
|
||||||
else:
|
).strip().upper()
|
||||||
norm = (raw - black_level) / (white_level - black_level)
|
|
||||||
|
|
||||||
norm = np.clip(norm, 0.0, 1.0)
|
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}"
|
||||||
|
)
|
||||||
|
|
||||||
if gamma is not None and gamma > 0:
|
self.sensor_name = (
|
||||||
norm = np.power(norm, 1.0 / gamma)
|
str(sensor_name).strip().upper()
|
||||||
|
if sensor_name
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
return (norm * 255.0).clip(0, 255).astype(np.uint8)
|
self.strict_shape = bool(
|
||||||
|
strict_shape
|
||||||
|
)
|
||||||
|
|
||||||
def _debayer_code(self):
|
# ============================================================
|
||||||
mapping = {
|
# Contrato
|
||||||
"RGGB": cv2.COLOR_BayerRG2RGB_EA,
|
# ============================================================
|
||||||
"BGGR": cv2.COLOR_BayerBG2RGB_EA,
|
|
||||||
"GRBG": cv2.COLOR_BayerGR2RGB_EA,
|
def get_contract(self) -> dict:
|
||||||
"GBRG": cv2.COLOR_BayerGB2RGB_EA,
|
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",
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.bayer_pattern not in mapping:
|
# ============================================================
|
||||||
raise ValueError(f"Padrão Bayer não suportado: {self.bayer_pattern}")
|
# Validação
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
return mapping[self.bayer_pattern]
|
def _validate_raw(
|
||||||
|
self,
|
||||||
|
raw16: np.ndarray,
|
||||||
|
bit_depth: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
if raw16 is None:
|
||||||
|
raise ValueError(
|
||||||
|
"RAW de preview é None."
|
||||||
|
)
|
||||||
|
|
||||||
def apply_preview_white_balance(self, bgr: np.ndarray, strength: float = 1.0) -> np.ndarray:
|
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[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:
|
||||||
"""
|
"""
|
||||||
Gray-world simples para deixar o preview mais agradável.
|
Converte RAW Bayer para uint8 de DISPLAY.
|
||||||
Não usar no raw de treino.
|
|
||||||
|
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.
|
||||||
"""
|
"""
|
||||||
img = bgr.astype(np.float32)
|
raw = self._validate_raw(
|
||||||
|
raw16,
|
||||||
|
bit_depth,
|
||||||
|
)
|
||||||
|
|
||||||
mean_b = float(img[:, :, 0].mean())
|
max_val = float(
|
||||||
mean_g = float(img[:, :, 1].mean())
|
(1 << int(bit_depth))
|
||||||
mean_r = float(img[:, :, 2].mean())
|
- 1
|
||||||
|
)
|
||||||
|
|
||||||
mean_gray = (mean_b + mean_g + mean_r) / 3.0
|
black, white = (
|
||||||
|
self._validate_levels(
|
||||||
|
black_level,
|
||||||
|
white_level,
|
||||||
|
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:
|
||||||
|
sample = raw_f.reshape(
|
||||||
|
-1
|
||||||
|
)
|
||||||
|
|
||||||
|
if black is None:
|
||||||
|
black = float(
|
||||||
|
np.percentile(
|
||||||
|
sample,
|
||||||
|
low_p,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if white is None:
|
||||||
|
white = float(
|
||||||
|
np.percentile(
|
||||||
|
sample,
|
||||||
|
high_p,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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_BayerBG2BGR_EA
|
||||||
|
),
|
||||||
|
"BGGR": (
|
||||||
|
cv2.COLOR_BayerRG2BGR_EA
|
||||||
|
),
|
||||||
|
"GRBG": (
|
||||||
|
cv2.COLOR_BayerGB2BGR_EA
|
||||||
|
),
|
||||||
|
"GBRG": (
|
||||||
|
cv2.COLOR_BayerGR2BGR_EA
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping[
|
||||||
|
self.bayer_pattern
|
||||||
|
]
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Ajustes VISUAIS
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def apply_preview_white_balance(
|
||||||
|
bgr: np.ndarray,
|
||||||
|
strength: float = 1.0,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Gray-world simples somente para deixar o preview legível.
|
||||||
|
|
||||||
|
Não usar no RAW científico.
|
||||||
|
"""
|
||||||
|
img = np.asarray(
|
||||||
|
bgr
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
img.ndim != 3
|
||||||
|
or img.shape[2] != 3
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"Preview WB exige BGR HxWx3; "
|
||||||
|
f"shape={img.shape}"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
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 = mean_gray / max(
|
||||||
gain_b = 1.0 + (gain_b - 1.0) * strength
|
mean_b,
|
||||||
gain_g = 1.0 + (gain_g - 1.0) * strength
|
eps,
|
||||||
gain_r = 1.0 + (gain_r - 1.0) * strength
|
)
|
||||||
|
|
||||||
img[:, :, 0] *= gain_b
|
gain_g = mean_gray / max(
|
||||||
img[:, :, 1] *= gain_g
|
mean_g,
|
||||||
img[:, :, 2] *= gain_r
|
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)
|
alpha = float(
|
||||||
return out
|
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(
|
def raw16_to_preview_bgr(
|
||||||
self,
|
self,
|
||||||
|
|
@ -99,28 +638,89 @@ class RawProcessorPreview:
|
||||||
apply_wb: bool = True,
|
apply_wb: bool = True,
|
||||||
apply_contrast: bool = True,
|
apply_contrast: bool = True,
|
||||||
bit_depth: int = 10,
|
bit_depth: int = 10,
|
||||||
|
black_level: Optional[float] = None,
|
||||||
|
white_level: Optional[float] = None,
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Pipeline de preview bonito:
|
Pipeline visual:
|
||||||
1. auto-level + gamma no mosaico
|
1. robust levels + gamma no mosaico
|
||||||
2. demosaic
|
2. demosaic BGR
|
||||||
3. white balance simples
|
3. gray-world opcional
|
||||||
4. leve contraste final
|
4. contraste opcional
|
||||||
"""
|
"""
|
||||||
vis8 = self.raw16_to_vis8(raw16, gamma=gamma, bit_depth=bit_depth)
|
vis8 = self.raw16_to_vis8(
|
||||||
bgr = cv2.cvtColor(vis8, self._debayer_code())
|
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:
|
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:
|
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:
|
if not ok:
|
||||||
raise RuntimeError("Falha ao codificar preview JPG")
|
raise RuntimeError(
|
||||||
|
"Falha ao codificar preview JPG."
|
||||||
|
)
|
||||||
|
|
||||||
return enc.tobytes()
|
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_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_enable": bool(self.config.get("trt_engine_cache_enable", True)),
|
||||||
"trt_engine_cache_path": str(
|
"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"]
|
calibrando = ctx["calibrando"]
|
||||||
bomba_principal = ctx["bomba_principal"]
|
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(
|
herbicida_pode_bloquear = bool(
|
||||||
auto and
|
autonomia_corredor_aplicavel
|
||||||
ctx["operacao_iniciada"] and
|
and not ctx["pausa"]
|
||||||
not calibrando and
|
and not ctx["emergencia"]
|
||||||
not ctx["finalizando"] and
|
|
||||||
not ctx["pausa"] and
|
|
||||||
not ctx["emergencia"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
em_transicao = self._pulverizacao_em_transicao(ctx)
|
em_transicao = self._pulverizacao_em_transicao(ctx)
|
||||||
|
|
@ -1491,7 +1495,7 @@ class ModuloAtuador(ModuloDiagnosticoBase):
|
||||||
})
|
})
|
||||||
score = min(score, 90)
|
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(
|
motivo = self._texto_ou_padrao(
|
||||||
ctx.get("autonomia_motivo"),
|
ctx.get("autonomia_motivo"),
|
||||||
ctx.get("motivo_herbicida_corredor"),
|
ctx.get("motivo_herbicida_corredor"),
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
|
|
||||||
motivos_gerais = []
|
motivos_gerais = []
|
||||||
condicoes_gerais = []
|
condicoes_gerais = []
|
||||||
|
drivers_com_travamento = []
|
||||||
saude_total_ativos = 0.0
|
saude_total_ativos = 0.0
|
||||||
total_drivers_em_uso = 0
|
total_drivers_em_uso = 0
|
||||||
algum_conectado = False
|
algum_conectado = False
|
||||||
|
|
@ -209,6 +210,17 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
conectado = self._bool(saude_mod.get("conectado_efetivo", False))
|
conectado = self._bool(saude_mod.get("conectado_efetivo", False))
|
||||||
em_uso = self._bool(saude_mod.get("em_uso", True))
|
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:
|
if conectado:
|
||||||
algum_conectado = True
|
algum_conectado = True
|
||||||
|
|
||||||
|
|
@ -253,6 +265,9 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"drivers_ativos": ativos,
|
"drivers_ativos": ativos,
|
||||||
"drivers_em_uso": total_drivers_em_uso,
|
"drivers_em_uso": total_drivers_em_uso,
|
||||||
"modelo": "movimentacao_inteligente_por_resposta_comando_valores_e_telemetria",
|
"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_motor": temperatura_motor,
|
||||||
"temperatura_driver": temperatura_driver,
|
"temperatura_driver": temperatura_driver,
|
||||||
"score_valores": int(max(0, min(100, round(score)))),
|
"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 {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
COMANDO_GRACE_MS = 2000.0
|
COMANDO_GRACE_MS = 2000.0
|
||||||
COMANDO_ALERTA_MS = 3000.0
|
COMANDO_ALERTA_MS = 3000.0
|
||||||
COMANDO_FALHA_MS = 7000.0
|
COMANDO_FALHA_MS = 7000.0
|
||||||
|
COMANDO_RECUPERACAO_MS = 1500.0
|
||||||
|
|
||||||
COMPONENTE_RECUPERACAO_GRACE_MS = 3000.0
|
COMPONENTE_RECUPERACAO_GRACE_MS = 3000.0
|
||||||
MODULO_RECUPERACAO_GRACE_MS = 10000.0
|
MODULO_RECUPERACAO_GRACE_MS = 10000.0
|
||||||
|
|
@ -82,7 +83,10 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
updates[f"sensores.{item['key']}.saude"] = s
|
updates[f"sensores.{item['key']}.saude"] = s
|
||||||
|
|
||||||
for item in servos:
|
for item in servos:
|
||||||
s = self._avaliar_servo(item)
|
s = self._avaliar_servo(
|
||||||
|
item,
|
||||||
|
modulo_disponivel=sen_disponivel,
|
||||||
|
)
|
||||||
saude_individual.append(s)
|
saude_individual.append(s)
|
||||||
updates[f"servos.{item['key']}.saude"] = s
|
updates[f"servos.{item['key']}.saude"] = s
|
||||||
|
|
||||||
|
|
@ -92,7 +96,10 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
updates[f"reles.{item['key']}.saude"] = s
|
updates[f"reles.{item['key']}.saude"] = s
|
||||||
|
|
||||||
for item in sinaleiros:
|
for item in sinaleiros:
|
||||||
s = self._avaliar_sinaleiro(item)
|
s = self._avaliar_sinaleiro(
|
||||||
|
item,
|
||||||
|
modulo_disponivel=sen_disponivel,
|
||||||
|
)
|
||||||
saude_individual.append(s)
|
saude_individual.append(s)
|
||||||
updates[f"sinaleiros.{item['key']}.saude"] = s
|
updates[f"sinaleiros.{item['key']}.saude"] = s
|
||||||
|
|
||||||
|
|
@ -239,12 +246,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
corrente_36v = self._valor_sensor(sensores, "AB36V", "Corrente")
|
corrente_36v = self._valor_sensor(sensores, "AB36V", "Corrente")
|
||||||
temp_bateria = self._valor_sensor(sensores, "TBAT", "Temperatura")
|
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 {
|
return {
|
||||||
"bms_ligado": bms_ligado,
|
"bms_ligado": bms_ligado,
|
||||||
"percent_bateria": percent_bateria,
|
"percent_bateria": percent_bateria,
|
||||||
|
|
@ -257,7 +258,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
"tensao_36v": tensao_36v,
|
"tensao_36v": tensao_36v,
|
||||||
"corrente_36v": corrente_36v,
|
"corrente_36v": corrente_36v,
|
||||||
"temp_bateria": temp_bateria,
|
"temp_bateria": temp_bateria,
|
||||||
"temperaturas_motor": temps_motor,
|
|
||||||
"debug": {
|
"debug": {
|
||||||
"bms_ligado": bms_ligado,
|
"bms_ligado": bms_ligado,
|
||||||
"percent_bateria": percent_bateria,
|
"percent_bateria": percent_bateria,
|
||||||
|
|
@ -267,7 +267,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
"sensor_ab36v_em_uso": ab36v_em_uso,
|
"sensor_ab36v_em_uso": ab36v_em_uso,
|
||||||
"tensao_36v": tensao_36v,
|
"tensao_36v": tensao_36v,
|
||||||
"corrente_36v": corrente_36v,
|
"corrente_36v": corrente_36v,
|
||||||
"temp_motor_max": max(temps_motor) if temps_motor else 0,
|
|
||||||
"temp_bateria": temp_bateria,
|
"temp_bateria": temp_bateria,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +280,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
conectado = self._bool(dados.get("conectado", False))
|
conectado = self._bool(dados.get("conectado", False))
|
||||||
aferir = self._bool(dados.get("aferir", False))
|
aferir = self._bool(dados.get("aferir", False))
|
||||||
mandatorio = self._bool(dados.get("mandatorio", 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 = []
|
motivos = []
|
||||||
condicoes = []
|
condicoes = []
|
||||||
|
|
||||||
|
|
@ -298,7 +298,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
|
|
||||||
saude = int(max(0, min(100, round(saude))))
|
saude = int(max(0, min(100, round(saude))))
|
||||||
status = self._resolver_status_item(conectado, saude, condicoes)
|
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")
|
return self._item(item, status, saude, motivos, condicoes, em_uso, debug, "sensor")
|
||||||
|
|
||||||
def _avaliar_valores_sensor(self, item, ctx, motivos, condicoes):
|
def _avaliar_valores_sensor(self, item, ctx, motivos, condicoes):
|
||||||
|
|
@ -403,8 +408,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
label = str(dados.get("label", ""))
|
label = str(dados.get("label", ""))
|
||||||
if label == "AB36V" and ctx["sensor_ab36v_em_uso"]:
|
if label == "AB36V" and ctx["sensor_ab36v_em_uso"]:
|
||||||
return ["Tensao", "Corrente", "Temperatura"]
|
return ["Tensao", "Corrente", "Temperatura"]
|
||||||
if label.startswith("TMV"):
|
|
||||||
return ["Temperatura"]
|
|
||||||
if label == "TBAT" and not ctx["bms_ligado"]:
|
if label == "TBAT" and not ctx["bms_ligado"]:
|
||||||
return ["Temperatura"]
|
return ["Temperatura"]
|
||||||
if mandatorio:
|
if mandatorio:
|
||||||
|
|
@ -415,73 +418,254 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
# Atuadores auxiliares
|
# Atuadores auxiliares
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def _avaliar_servo(self, item):
|
def _avaliar_servo(self, item, modulo_disponivel=True):
|
||||||
dados = item["dados"]
|
dados = item["dados"]
|
||||||
conectado = self._bool(dados.get("conectado", False))
|
|
||||||
controlar = self._bool(dados.get("controlar", False))
|
controlar = self._bool(dados.get("controlar", False))
|
||||||
if not conectado:
|
runtime, suspenso = self._preparar_atuador_auxiliar(
|
||||||
return self._item(item, StatusModulo.DESCONECTADO, 0, ["desconectado"], [], controlar, {}, "servo")
|
item=item,
|
||||||
|
grupo="servo",
|
||||||
|
nome="Servo SEN",
|
||||||
|
modulo_disponivel=modulo_disponivel,
|
||||||
|
)
|
||||||
|
if suspenso is not None:
|
||||||
|
return suspenso
|
||||||
|
|
||||||
motivos, condicoes = [], []
|
motivos, condicoes = [], []
|
||||||
resposta = self._score_resposta(dados, motivos, condicoes, "Servo SEN")
|
resposta = self._score_resposta(dados, motivos, condicoes, "Servo SEN")
|
||||||
comando, dbg_cmd = self._avaliar_comando_servo(item, motivos, condicoes)
|
comando, dbg_cmd = self._avaliar_comando_servo(
|
||||||
telem = self._avaliar_telemetria(dados, motivos, ["ServoAnguloLeitura"] if controlar else [])
|
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
|
saude = resposta * 0.25 + comando * 0.60 + telem["score"] * 0.15
|
||||||
if any(self._is_falha_hardware(c) for c in condicoes):
|
if any(self._is_falha_hardware(c) for c in condicoes):
|
||||||
saude = min(saude, 45)
|
saude = min(saude, 45)
|
||||||
saude = int(max(0, min(100, round(saude))))
|
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"]
|
dados = item["dados"]
|
||||||
label = dados.get("label")
|
label = dados.get("label")
|
||||||
controlar = self._bool(dados.get("controlar", False))
|
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:
|
desejado_presente = (
|
||||||
motivos.append("Servo sem leitura de ângulo")
|
"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)
|
||||||
|
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({
|
condicoes.append({
|
||||||
"label": "Servo SEN",
|
"label": "Servo SEN",
|
||||||
"valor": angulo,
|
"valor": angulo,
|
||||||
"severidade": 70 if controlar else 35,
|
"severidade": min(89, max(55, int(erro * 4))),
|
||||||
"classe": "processo",
|
"classe": "processo",
|
||||||
"descricao": f"Servo {label} sem leitura de ângulo válida",
|
"descricao": f"Servo {label} não convergiu para o ângulo desejado",
|
||||||
"acoes": ["Verificar retorno de ServoAnguloLeitura."],
|
"acoes": [
|
||||||
|
"Acompanhar a convergência do servo.",
|
||||||
|
"Verificar esforço mecânico se o erro permanecer.",
|
||||||
|
],
|
||||||
})
|
})
|
||||||
return (60 if controlar else 85), {"angulo": angulo, "angulo_sp": angulo_sp, "sem_leitura": True}
|
return (68 if erro >= self.SERVO_ALERTA_GRAUS else 82), debug
|
||||||
|
|
||||||
erro = abs(angulo - angulo_sp)
|
def _falha_servo(
|
||||||
if not controlar or erro <= self.SERVO_TOL_GRAUS:
|
self,
|
||||||
return 100, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "controlar": controlar}
|
label,
|
||||||
if tem_idade and ultimo_ms < self.COMANDO_GRACE_MS:
|
angulo,
|
||||||
return 96, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "ultimo_comando_ms": ultimo_ms}
|
angulo_sp,
|
||||||
if erro >= self.SERVO_FALHA_GRAUS and tem_idade and ultimo_ms >= self.COMANDO_FALHA_MS:
|
erro,
|
||||||
motivos.append(f"Servo {label} não convergiu para o SP ({angulo:.1f}/{angulo_sp:.1f})")
|
motivos,
|
||||||
|
condicoes,
|
||||||
|
debug,
|
||||||
|
):
|
||||||
|
motivos.append(
|
||||||
|
f"Servo {label} não convergiu para o SP "
|
||||||
|
f"({angulo:.1f}/{angulo_sp:.1f})"
|
||||||
|
)
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
"label": "Servo SEN",
|
"label": "Servo SEN",
|
||||||
"valor": angulo,
|
"valor": angulo,
|
||||||
"severidade": 92,
|
"severidade": 92,
|
||||||
"classe": "falha_hardware",
|
"classe": "falha_hardware",
|
||||||
"descricao": f"Servo {label} não acompanhou o comando de ângulo",
|
"descricao": f"Servo {label} não acompanhou o comando de ângulo",
|
||||||
"acoes": ["Verificar travamento mecânico ou alimentação do servo."],
|
"acoes": [
|
||||||
|
"Verificar travamento mecânico.",
|
||||||
|
"Verificar alimentação e retorno do servo.",
|
||||||
|
],
|
||||||
})
|
})
|
||||||
return 25, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "ultimo_comando_ms": ultimo_ms}
|
debug["erro"] = erro
|
||||||
if erro >= self.SERVO_ALERTA_GRAUS:
|
return 25, debug
|
||||||
motivos.append(f"Servo {label} distante do SP ({angulo:.1f}/{angulo_sp:.1f})")
|
|
||||||
|
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({
|
condicoes.append({
|
||||||
"label": "Servo SEN",
|
"label": "Servo SEN",
|
||||||
"valor": angulo,
|
"valor": angulo,
|
||||||
"severidade": min(89, max(45, int(erro * 4))),
|
"severidade": 55,
|
||||||
"classe": "processo",
|
"classe": "processo",
|
||||||
"descricao": f"Servo {label} distante do ângulo desejado",
|
"descricao": f"Servo {label} voltou ao SP e está em confirmação",
|
||||||
"acoes": ["Acompanhar se o erro persiste."],
|
"acoes": ["Aguardar confirmação da estabilidade do retorno."],
|
||||||
})
|
})
|
||||||
return 70, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "ultimo_comando_ms": ultimo_ms}
|
return 78, debug
|
||||||
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}
|
|
||||||
|
|
||||||
def _avaliar_rele(self, item, modulo_disponivel=True):
|
def _avaliar_rele(self, item, modulo_disponivel=True):
|
||||||
return self._avaliar_saida_binaria(
|
return self._avaliar_saida_binaria(
|
||||||
|
|
@ -493,8 +677,11 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
modulo_disponivel=modulo_disponivel,
|
modulo_disponivel=modulo_disponivel,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _avaliar_sinaleiro(self, item):
|
def _avaliar_sinaleiro(self, item, modulo_disponivel=True):
|
||||||
return self._avaliar_sinaleiro_impl(item)
|
return self._avaliar_sinaleiro_impl(
|
||||||
|
item,
|
||||||
|
modulo_disponivel=modulo_disponivel,
|
||||||
|
)
|
||||||
|
|
||||||
def _avaliar_saida_binaria(
|
def _avaliar_saida_binaria(
|
||||||
self,
|
self,
|
||||||
|
|
@ -672,8 +859,155 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
"componente_recuperado_em": 0.0,
|
"componente_recuperado_em": 0.0,
|
||||||
|
|
||||||
"divergencia_desde": None,
|
"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(
|
def _avaliar_comando_binario(
|
||||||
self,
|
self,
|
||||||
item,
|
item,
|
||||||
|
|
@ -874,57 +1208,213 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
|
|
||||||
return 30, debug
|
return 30, debug
|
||||||
|
|
||||||
def _avaliar_sinaleiro_impl(self, item):
|
def _avaliar_sinaleiro_impl(self, item, modulo_disponivel=True):
|
||||||
dados = item["dados"]
|
dados = item["dados"]
|
||||||
conectado = self._bool(dados.get("conectado", False))
|
|
||||||
controlar = self._bool(dados.get("controlar", False))
|
controlar = self._bool(dados.get("controlar", False))
|
||||||
if not conectado:
|
runtime, suspenso = self._preparar_atuador_auxiliar(
|
||||||
return self._item(item, StatusModulo.DESCONECTADO, 0, ["desconectado"], [], controlar, {}, "sinaleiro")
|
item=item,
|
||||||
|
grupo="sinaleiro",
|
||||||
|
nome="Sinaleiro SEN",
|
||||||
|
modulo_disponivel=modulo_disponivel,
|
||||||
|
)
|
||||||
|
if suspenso is not None:
|
||||||
|
return suspenso
|
||||||
|
|
||||||
motivos, condicoes = [], []
|
motivos, condicoes = [], []
|
||||||
resposta = self._score_resposta(dados, motivos, condicoes, "Sinaleiro SEN")
|
resposta = self._score_resposta(
|
||||||
comando, dbg_cmd = self._avaliar_comando_sinaleiro(item, motivos, condicoes)
|
dados,
|
||||||
|
motivos,
|
||||||
|
condicoes,
|
||||||
|
"Sinaleiro SEN",
|
||||||
|
)
|
||||||
|
comando, dbg_cmd = self._avaliar_comando_sinaleiro(
|
||||||
|
item,
|
||||||
|
motivos,
|
||||||
|
condicoes,
|
||||||
|
runtime,
|
||||||
|
)
|
||||||
telem = self._avaliar_telemetria(dados, motivos, [])
|
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 = resposta * 0.45 + comando * 0.45 + telem["score"] * 0.10
|
||||||
saude = int(max(0, min(100, round(saude))))
|
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"]
|
dados = item["dados"]
|
||||||
label = dados.get("label")
|
label = dados.get("label")
|
||||||
controlar = self._bool(dados.get("controlar", False))
|
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_sp = self._safe_int(
|
||||||
id_lido = self._safe_int(dados.get("status_led_id", self._campo_atual(dados, "StatusLedID", -1)), -1)
|
dados.get("status_led_id_sp", dados.get("led_id_sp", -1)),
|
||||||
st_lido = self._safe_int(dados.get("status_led_leitura", self._campo_atual(dados, "StatusLedLeitura", -1)), -1)
|
-1,
|
||||||
ultimo_ms = self._float(dados.get("ultimo_comando_ms", 999999), 999999)
|
)
|
||||||
|
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:
|
if not controlar:
|
||||||
return 100, {"controlar": False, "status_lido": st_lido, "id_lido": id_lido}
|
self._resetar_estado_comando(runtime, manter_desejado=False)
|
||||||
if id_sp < 0 and st_sp < 0:
|
return 100, debug
|
||||||
return 94, {"contrato_comando_completo": False, "status_lido": st_lido, "id_lido": id_lido}
|
|
||||||
|
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:
|
if coerente:
|
||||||
return 100, {"status_lido": st_lido, "status_sp": st_sp, "id_lido": id_lido, "id_sp": id_sp}
|
runtime["divergencia_desde"] = None
|
||||||
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}
|
|
||||||
|
|
||||||
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({
|
condicoes.append({
|
||||||
"label": "Sinaleiro SEN",
|
"label": "Sinaleiro SEN",
|
||||||
"valor": st_lido,
|
"valor": st_lido,
|
||||||
"severidade": 45,
|
"severidade": 55,
|
||||||
"classe": "processo",
|
"classe": "processo",
|
||||||
"descricao": f"Sinaleiro {label} não está refletindo o comportamento desejado",
|
"descricao": (
|
||||||
"acoes": ["Conferir LED/sinaleiro físico e comando pelo SEN."],
|
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
|
# Processo geral
|
||||||
|
|
@ -1013,30 +1503,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
|
||||||
score = min(score, 60)
|
score = min(score, 60)
|
||||||
energia_critica = True
|
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:
|
if ctx["temp_bateria"] >= 60:
|
||||||
motivos.append(f"Temperatura da bateria crítica ({ctx['temp_bateria']:.1f} °C)")
|
motivos.append(f"Temperatura da bateria crítica ({ctx['temp_bateria']:.1f} °C)")
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ class RegrasTaticas:
|
||||||
# Pulverizador automático.
|
# Pulverizador automático.
|
||||||
"atu_indisponivel": self._nova_persistencia(),
|
"atu_indisponivel": self._nova_persistencia(),
|
||||||
"bomba_cavitada": 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)
|
motivos.extend(novos_motivos)
|
||||||
freio_necessario = freio_necessario or freio
|
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(
|
novos_motivos, freio = self._avaliar_oak_parada_por_bloqueio(
|
||||||
controle=controle,
|
controle=controle,
|
||||||
operacao=operacao
|
operacao=operacao
|
||||||
|
|
@ -126,6 +136,92 @@ class RegrasTaticas:
|
||||||
|
|
||||||
return motivos, freio_necessario
|
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):
|
def _avaliar_oak_parada_por_bloqueio(self, controle: dict, operacao: dict):
|
||||||
"""
|
"""
|
||||||
Bloqueia somente se a função de parada por bloqueio estiver ativa.
|
Bloqueia somente se a função de parada por bloqueio estiver ativa.
|
||||||
|
|
|
||||||
|
|
@ -630,6 +630,90 @@ class ContextoGlobalRedis:
|
||||||
"motivos": motivos_controle_runtime,
|
"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
|
# 6) Decisão final
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
@ -667,6 +751,22 @@ class ContextoGlobalRedis:
|
||||||
liberado=liberou,
|
liberado=liberou,
|
||||||
motivo_nao_liberado="\n\n".join(motivos) if not liberou else None,
|
motivo_nao_liberado="\n\n".join(motivos) if not liberou else None,
|
||||||
motivos_nao_liberado_lista=motivos,
|
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={
|
operacao_liberacao_debug={
|
||||||
"ts": time.time(),
|
"ts": time.time(),
|
||||||
"liberado": liberou,
|
"liberado": liberou,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -24,6 +26,13 @@ from visual_worker.utils import converter_valores_numpy
|
||||||
from shared.perf_monitor import VisualPerfMonitor
|
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:
|
class CameraManager:
|
||||||
"""
|
"""
|
||||||
CameraManager v1 do Weed Worker.
|
CameraManager v1 do Weed Worker.
|
||||||
|
|
@ -61,6 +70,12 @@ class CameraManager:
|
||||||
self.weed_detector = None
|
self.weed_detector = None
|
||||||
self.seg_config = 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.operante = False
|
||||||
self.iniciando = False
|
self.iniciando = False
|
||||||
self.debug_visual = False
|
self.debug_visual = False
|
||||||
|
|
@ -226,6 +241,488 @@ class CameraManager:
|
||||||
self._proxima_inicializacao_monotonic = time.monotonic() + atraso
|
self._proxima_inicializacao_monotonic = time.monotonic() + atraso
|
||||||
return 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):
|
def inicializar(self, mx_id):
|
||||||
if mx_id is None:
|
if mx_id is None:
|
||||||
return False
|
return False
|
||||||
|
|
@ -271,7 +768,20 @@ class CameraManager:
|
||||||
self.seg_config.get("posproc_intervalo_min_s", 5.0)
|
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:
|
if camera_nova is None:
|
||||||
atraso = self._registrar_falha_inicializacao()
|
atraso = self._registrar_falha_inicializacao()
|
||||||
self.mostrar_log(
|
self.mostrar_log(
|
||||||
|
|
@ -294,7 +804,6 @@ class CameraManager:
|
||||||
self.debug_visual = bool(self.seg_config.get("debug_visual", False))
|
self.debug_visual = bool(self.seg_config.get("debug_visual", False))
|
||||||
self.debug_perf = bool(self.seg_config.get("debug_perf", False))
|
self.debug_perf = bool(self.seg_config.get("debug_perf", False))
|
||||||
|
|
||||||
self._inicializar_modelo()
|
|
||||||
self._inicializar_detector()
|
self._inicializar_detector()
|
||||||
|
|
||||||
self.operante = False
|
self.operante = False
|
||||||
|
|
@ -374,15 +883,56 @@ class CameraManager:
|
||||||
nova = None
|
nova = None
|
||||||
|
|
||||||
try:
|
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(
|
nova = CameraMultispectral(
|
||||||
self.mostrar_log,
|
self.mostrar_log,
|
||||||
mx_id=mx_id,
|
mx_id=mx_id,
|
||||||
module_calibration_json=seg_config.get("module_calibration_json"),
|
module_calibration_json=contract[
|
||||||
width=int(seg_config.get("camera_width", 1280)),
|
"module_params_path"
|
||||||
height=int(seg_config.get("camera_height", 800)),
|
],
|
||||||
# Default conservador para campo. Se o JSON definir 40, ele será respeitado.
|
width=fallback_w,
|
||||||
fps=int(seg_config.get("camera_fps", 20)),
|
height=fallback_h,
|
||||||
target_size=seg_config.get("ia_resolution", [1024, 640]),
|
# 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:
|
if not nova.iniciado:
|
||||||
|
|
@ -392,6 +942,62 @@ class CameraManager:
|
||||||
pass
|
pass
|
||||||
return None
|
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
|
return nova
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -400,11 +1006,50 @@ class CameraManager:
|
||||||
nova.parar()
|
nova.parar()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
return None
|
||||||
|
|
||||||
def _inicializar_modelo(self):
|
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:
|
if self.model_svc is not None:
|
||||||
|
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
|
return
|
||||||
|
|
||||||
self.model_svc = MultiSpecSegformerService(
|
self.model_svc = MultiSpecSegformerService(
|
||||||
|
|
@ -414,7 +1059,8 @@ class CameraManager:
|
||||||
|
|
||||||
self.mostrar_log(
|
self.mostrar_log(
|
||||||
f"[weed][MODEL] backend={getattr(self.model_svc, 'runtime_backend', 'onnx')} "
|
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):
|
def _inicializar_detector(self):
|
||||||
|
|
@ -689,6 +1335,11 @@ class CameraManager:
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
self._validar_tensor_camera(
|
||||||
|
tensor5,
|
||||||
|
contexto="warmup",
|
||||||
|
)
|
||||||
|
|
||||||
frame_id = int(
|
frame_id = int(
|
||||||
res.get("frame_id")
|
res.get("frame_id")
|
||||||
or res.get("async_packet_seq")
|
or res.get("async_packet_seq")
|
||||||
|
|
@ -910,6 +1561,7 @@ class CameraManager:
|
||||||
from weed_worker.config import load_seg_config
|
from weed_worker.config import load_seg_config
|
||||||
|
|
||||||
cfg = load_seg_config()
|
cfg = load_seg_config()
|
||||||
|
cfg = self._validar_config_dinamica_contrato(cfg)
|
||||||
self.seg_config = cfg
|
self.seg_config = cfg
|
||||||
self.qtd_bicos = int(cfg.get("qtd_bicos", self.qtd_bicos or 7) or 7)
|
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))
|
self.posproc_intervalo_min_s = float(cfg.get("posproc_intervalo_min_s", self.posproc_intervalo_min_s))
|
||||||
|
|
@ -974,7 +1626,9 @@ class CameraManager:
|
||||||
|
|
||||||
motivos = []
|
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")
|
motivos.append("câmera indisponível")
|
||||||
elif self.iniciando:
|
elif self.iniciando:
|
||||||
motivos.append("câmera inicializando")
|
motivos.append("câmera inicializando")
|
||||||
|
|
@ -1010,6 +1664,23 @@ class CameraManager:
|
||||||
"timeout_tensor_s": timeout_tensor,
|
"timeout_tensor_s": timeout_tensor,
|
||||||
"timeout_inferencia_s": timeout_inferencia,
|
"timeout_inferencia_s": timeout_inferencia,
|
||||||
"timeout_deteccao_s": timeout_deteccao,
|
"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
|
self.pipeline_ia_ok = ok
|
||||||
|
|
@ -1017,6 +1688,20 @@ class CameraManager:
|
||||||
|
|
||||||
return resultado
|
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
|
# Caches
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -1415,6 +2100,11 @@ class CameraManager:
|
||||||
time.sleep(0.50 if reiniciou else 0.05)
|
time.sleep(0.50 if reiniciou else 0.05)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
self._validar_tensor_camera(
|
||||||
|
tensor5,
|
||||||
|
contexto="capture",
|
||||||
|
)
|
||||||
|
|
||||||
if not self._sessao_camera_valida(camera_atual, generation):
|
if not self._sessao_camera_valida(camera_atual, generation):
|
||||||
self.perf.inc("tensor_descartado_geracao")
|
self.perf.inc("tensor_descartado_geracao")
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue