16. Sistema com Bombeamento e Detecção - Irrigador com balança

Um amigo biólogo tinha que fazer vários experimentos com vasos de plantas em um estufa, controlando a quantidade de água usada na irrigação. Ao longo dos experimentos a reposição de água nos vasos era feita de forma manual, 3 vezes por semana, pesando cada vaso em uma balança e adicionando água suficiente para repor o peso original do vaso no início do experimento.

Para facilitar essa atividade providenciamos a montagem de um irrigador automatizado para fazer a adição controlada de água nos vasos através do monitoramento do peso.

Cada experimento era feito com vários vasos simultâneamente e portanto não era viável um sistema dedicada para cada vaso. Mas mesmo assim já seria uma ferramenta útil para otimizar o trabalho de irrigação durante os experimentos.

16.1. Estrutura do Irrigador

Os componentes físicos do sistema foram montados conforme a figura 136.

Figura 136. Diagrama esquemático dos componentes do irrigador

Diagrama esquemático dos componentes do irrigador

O circuito para amplificar as leituras da balança foi montado com base no diagrama da figura C.14 mas fizemos algumas alterações adicionando um diodo Zenner no pino analógico 1, trocamos as baterias por dois carregadores de celular idênticos e utilizamos o pino digital 8 para alimentar a ponte de Wheatstone da célula de carga, e chegamos ao circuito da figura 137.

Figura 137. Diagrama do circuito amplificador diferencial instrumental para a célula de carga, que utiliza o pino digital 8 para alimentar ponte de Wheatstone da célula de carga.

Diagrama do circuito amplificador diferencial instrumental para a célula de carga, que utiliza o pino digital 8 para alimentar ponte de Wheatstone da célula de carga.

A célula de carga é alimentada pelo pino digital 8 e as leituras de massa são feitas pelo pino analógico A0.

O circuito para o controle da bomba foi montado segundo o diagrama da figura 138.

Figura 138. Diagrama do circuito para controle da bomba do irrigador.

Diagrama do circuito para controle da bomba do irrigador.

16.2. Programação do Arduino

Para a comunicação com a placa Arduino™ usei como base o Sketch disponível na seção Sistema Didático - Programação em Tcl/Tk.

Mas esse Sketch apresenta o inconveniente de interromper a execução do programa durante 50 ms para aguardar o recebimento de até ~40 caracteres pela porta serial.

Para evitar essa limitação seguimos as dicas do tutorial How to process incoming serial data without blocking e implementamos o seguinte Sketch:

  /*
Final version of the sketch to interpret the commands SET and GET
26/08/2015
Adapted from:
Example of processing incoming serial data without blocking.
Author:   Nick Gammon
(http://www.gammon.com.au/forum/?id=11425)
*/

const unsigned int MAX_INPUT = 20;
char message[20];
char *cmd;
char *pin;
char *value;

char c;
byte length;
byte length_message;


void setup () {
  Serial.begin(9600);
  
  Serial.flush();

//Available pins
//D2, D3 (PWM), D4, D5(PWM), D6(PWM), D7, D8, D9(PWM), D10(PWM), D11(PWM) e D12
//A0, A1, A2, A3, A4 e A5

//Pins 2-5 are configured as digital input pins, 
//and pins 8-12 are configured as digital output pins.

  pinMode(8,  OUTPUT);

  //Pin 8 are used to power the  Wheatstone Bridge of strain gouges
  //at load cell
  digitalWrite(8, HIGH);

  pinMode(9,  OUTPUT);
  pinMode(10, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(12, OUTPUT);

  pinMode(2, INPUT);
  digitalWrite(2,HIGH); // enable pull-up resistors 

  pinMode(3, INPUT);
  digitalWrite(3,HIGH); // enable pull-up resistors 

  pinMode(4, INPUT);
  digitalWrite(4,HIGH); // enable pull-up resistors 

  pinMode(5, INPUT);
  digitalWrite(5,HIGH); // enable pull-up resistors 
}
 

// here to process incoming serial data after a terminator received
void process_data (char *data) {
   
   cmd = strtok(data, ";");
  
     if ( (strcmp(cmd, "SET") == 0) || (strcmp(cmd, "set") == 0) ) {
      pin = strtok(NULL, ";");
      value = strtok(NULL, ";");
      setPin(pin, value); 
      
      } else if (strcmp(cmd, "GET") == 0 || strcmp(cmd, "get") == 0) {

        pin = strtok(NULL, ";");

         getPin(pin);
         
      } else {

        Serial.println("unknown command");

      } // end of if
        

  }  // end of process_data
  
void processIncomingByte (const byte inByte) {
  static char input_line[MAX_INPUT];
  static unsigned int input_pos = 0;

  switch (inByte)
    {

    case '\n':   // end of text
      input_line[input_pos] = 0;  // terminating null byte
      
      // terminator reached! process input_line here ...
      process_data (input_line);
      
      // reset buffer for next time
      input_pos = 0;  
      break;

    case '\r':   // discard carriage return
      break;

    default:
      // keep adding if not full ... allow for terminating null byte
      if (input_pos < (MAX_INPUT - 1))
        input_line[input_pos++] = inByte;
      break;   
      

    }  // end of switch
    
  }// end of processIncomingByte  
  
  void loop() {
  // if serial data available, process it
  while (Serial.available () > 0)
    processIncomingByte (Serial.read());
    
   // getPin("A0");
   // delay(5000);
    
  // do other stuff here like testing digital input (button presses) ...

  }  // end of loop

void setPin(char *pin, char *val) { 
  
  if ((pin[0] == 'D') || (pin[0] == 'd')) {
    
    byte numPin = strtol(pin+1, NULL, 10);
    
      //O valor do pino digital pode ser HIGH 1 ou LOW 0
      //e a funcao atoi converte a string contida no ponteiro
      //val em um inteiro (ou byte)
    
      //The value of the digital pins can be HIGH 1 or LOW 0
      //and the function atoi converts the string contained 
      //in the pointer val to an integer (or byte)

    byte state = atoi(val);
    
    if ( state > 1) {
      analogWrite(numPin, state);
    } else {    
      digitalWrite(numPin, state);
    }
    
    //Outra opcao seria
    //if ( (strcmp(val, "HIGH") == 0) || (strcmp(val, "high") == 0) ) {
    //digitalWrite(numPin, HIGH);
    //} else if ( (strcmp(val, "LOW") == 0) || (strcmp(val, "low") == 0) ) {
    //digitalWrite(numPin, LOW);
    //}
    
    //Serial.print("pin:");  
    //Serial.print(pin);  
    //Serial.print(";");
    //Serial.print("set:");
    //Serial.println(state);
    
  }
      
}
  
  
void getPin(char *pin) {
 
   byte numPin = strtol(pin+1, NULL, 10);
   
 if ((pin[0] == 'D') || (pin[0] == 'd')) {
  
   byte state = digitalRead(numPin);
  
    Serial.print("pin:");
    Serial.print(pin);  
    Serial.print(";");
    Serial.print("state:");
    Serial.println(state);
   
 } else if ((pin[0] == 'A') || (pin[0] == 'a')) {
  
 //int readout = analogRead(numPin);
 int i = 0;
 float soma = 0;
 float media = 0;

 //Faz 500 leituras analógicas com intervalo de 1ms
 
 while(i < 100) {
  // ler sinal analogico no pino 0:
  int readout = analogRead(numPin);
  soma = soma + readout;
  i++;
  delay(1);
  }
  
  media = soma/100;
 
    Serial.print("pin:");  
    Serial.print(pin);  
    Serial.print(";");
    Serial.print("media:");
    Serial.println(media);


// Converter leitura analogica (0 - 1023) em voltagem (0 - 5V):
 // float voltage = media * (5.0 / 1023.0);
 // voltage = voltage * 1000;
   
 // Serial.print(voltage, 0);
 // Serial.println(" mV");
   
 }
   
}

Nesse Sketch configuramos os pinos digitais de 2-5 como pinos de entrada, e os pinos digitais de 9-12 como pinos de saída. Mas isso pode ser reconfigurado para outras montagens.

Para padronizar a comunicação, implementamos apenas 2 funções: setPin(pin) e getPin(pin), para padronizar a comunicação serial com o PC com o uso dos comandos SET e GET respectivamente, com a sintaxe: [comando];[pino];[valor]

  • [comando]: SET (digital (0-1) ou analog (PWM 0-255)) ou GET (analogRead or digitalRead)

  • [pino]: D2, D3 ... D12, A0, A1 ... A5

  • [valor]: 0-1, 0-255

O comando SET é usado nos pinos configurados como OUTPUT. E o comando GET é usado com os pinos analógicos e digitais configuradas como INPUT.

Exemplos de comando:

  • SET;D2;0 - definir o pino digital D2 como 0 (LOW)

  • SET;D3;255 - definir o pino digital D3 com o valor 255 no modo PWM

  • GET;A0 - retorna a leitura do pino analógico A0

  • GET;D1 - retorna a leitura do pino digital D1

16.3. Programa de Controle do Irrigador

Gostaria de esclarecer o(a) prezado(a) leitor(a) que o código da interface de controle não ficou tão organizado como eu gostaria devido a mudanças que foram feitas durante o desenvolvimento.

Comecei a escrever o código com uma abordagem Procedural mas no meio do caminho comecei a usar Orientação à Objeto e Máquina de Estados Hierárquicos e por isso o código ficou um pouco confuso.

Ao longo das etapas de operação é necessário estabelecer métodos para lidar com as exceções tais como o usuário tentar iniciar a irrigação antes de calibrar o sistema. Com uma abordagem Procedural essa situação poderia ser evitada de duas formas: bloqueio dos botões da interface gráfica ou a inserção de inúmeros testes if para verificar a compatibilidade dos eventos com o estado.

Optei pelo uso de Orientação à Objeto e Máquina de Estados Hierárquicos para facilitar a manutenção do código e tornar mais robusto o tratamento das exceções.

O envio das leituras da célula de carga feitas pelo Arduino poderia ser contínuo ou mediante o envio do comando GET.

Para ser contínuo, teríamos que implementar uma rotina no Arduino para iniciar leituras contínuas de um determinado pino com intervalos de tempo definidos.

Por exemplo, poderia ter o formato [comando];[pino];[intervalo] (Ex: start;A0;500 - iniciar leituras analógicas no pino A0 com intervalos de 500 ms). E o comando STOP para interromper o envio de dados, no formato stop;A0.

Se as leituras fossem feitas pelo envio do comando GET teríamos que implementar a temporização na própria interface com envio temporizado do comando GET pela porta serial. Mas se implementássemo os comandos START e STOP no Arduino iríamos simplificar a interface gráfica no PC e aumentar a complexidade no código do Arduino.

Acabei optando por adiar a implementação dos comandos START e STOP no Arduino e usar apenas o comando GET em intevalos regulares.

Pretendo, futuramente, implementar os comandos START e STOP no Arduino para padronizar a comunicação com o Arduino.

As leituras poderiam ser enviadas na escala ADC (0-1023) ou previamente convertida em Volts (0-5V), mas a unidade de interesse é o g (grama) ou Kg, e portanto é necessário uma equação de calibração (para converter ADC em g) que deverá ficar armazenada na interface de controle rodando no PC.

Identificamos a necessidade de estabelecer as seguintes procedimentos operacionais para o irrigador:

  • Conexão - inicar e monitorar a conexão entre o PC e a placa Arduino

  • Calibração - definir uma equação para conversão das leituras feitas pelo Arduino (ADC) em unidade de massa (g)

  • Configuração - definir do peso final do vaso que deve ser alcançado no final da irrigação

  • Irrigação - acionar a bomba para adição de água e monitorar o peso até atingir o peso final (set point)

Figura 139. Procedimentos operacionais para a irrigação

Procedimentos operacionais para a irrigação

16.4. Máquina de Estados Hierárquicos

Para modelar esse comportamento decidimos usar a técnica de Máquina de Estados.

O conceito de Estado abstrai todos os eventos irrelevantes e se concentra apenas nos eventos relevantes. Nos diagramas de estado os nós são os estados e os conectores representam as transições.

O Estado, em uma máquina de estados, especifica um contexto de comportamento, enquanto que um bloco em um diagrama de fluxo representa uma etapa de processamento em um fluxo. (Fonte: A Crash Course in UML State Machines)

Importante distinguir máquina de estados de diagrama de fluxos pois cada qual representa um paradigma de programação. Máquina de estados é uma programação orientada a eventos e o diagrama de fluxo é uma programação transformacional.

Máquina de estados está ligada ao conceito de evento enquanto que na programação em fluxo os eventos são elementos secundários.

Um evento, após gerado passa por basicamente 3 etapas:

  1. após recebido entra em uma fila de execução

  2. é enviado para a máquina de estados e processado

  3. é consumido e retirado da fila de execução

As etapas operacionais do irrigador foram modeladas como estados e subestados em uma máquina de estados hierárquicos conforme a figura 140, seguindo o formalismo dos Diagramas de Harel (David Harel, 1987).

Figura 140. Diagrama de Estados Hierárquicos para o Irrigador

Diagrama de Estados Hierárquicos para o Irrigador

O diagrama de estados da figura 140 foi representado como uma Árvore de Comportamento (Behaviour Tree - BT) conforme a figura 141.

Figura 141. Árvore de Comportamento (Behaviour Tree - BT) representando o diagrama de estados hierárquicos da figura 140.

“Árvore de Comportamento” (Behaviour Tree - BT) representando o diagrama de estados hierárquicos da figura 140.

Na árvore da figura 141 os estados (e subestados) e as transições são representados indistintamente como nós da árvore.

A Árvore de Comportamento (figura 141) foi implementada em uma variável do tipo dicionário (dict) que foi chamada state_tree.

O tipo dicionário (dict), em Tcl, é semelhantea ao array pois também possui a relação entre uma chave (key) e um valor (value), mas um dicionário é tratado como um objeto e além disso permitem a construção de estruturas de dados multi-dimensionais, além de outras vantagens.

A estrutura da variável state_tree, que contém toda a representação dos possíveis estados do irrigador, pode ser parcialmente visualizada pelo diagrama da figura 142.

Figura 142. Diagrama esquemático parcial da estrutura da variável state_tree do tipo dict.

Diagrama esquemático parcial da estrutura da variável state_tree do tipo dict.

Testamos dois diferentes comandos para montar a estrutura da árvore de comportamento na variável state_tree: dict create e dict set.

Para facilitar a visualização, indicamos em 9 etapas o uso do comando dict create para a montagem da árvore:

#Etapa 1
#set state_tree [dict create desconectado conectar conectado desconectar]

#Etapa 2
#set state_tree [dict create desconectado [dict create conectar conectado] conectado [dict create desconectar desconectado]]

#Etapa 3
#set state_tree [dict create desconectado [dict create conectar [dict create conectado descalibrado] ] conectado [dict create desconectar desconectado]]

#Etapa 4
#set state_tree [dict create desconectado [dict create conectar [dict create conectado descalibrado ] ] conectado [dict create desconectar desconectado]]

#Etapa 5
#set state_tree [dict create desconectado [dict create conectar [dict create conectado [dict create descalibrado calibrar calibrando done calibrado recalibrar] ] ] conectado [dict create desconectar desconectado]]

#Etapa 6
#set state_tree [dict create desconectado [dict create conectar [dict create conectado [dict create descalibrado [dict create calibrar calibrando] calibrado [dict create recalibrar calibrando] calibrando [dict create done calibrado ] ] ] ] conectado [dict create desconectar desconectado]]

#Etapa 7
#set state_tree [dict create desconectado [dict create conectar [dict create conectado [dict create descalibrado [dict create calibrar calibrando] calibrado [dict create recalibrar calibrando] calibrando [dict create done [dict create calibrado [dict create desconfigurado configurar configurando done configurado reconfigurar] ] ] ] ] ]  conectado [dict create desconectar desconectado]]

#Etapa 8
#set state_tree [dict create desconectado [dict create conectar [dict create conectado [dict create descalibrado [dict create calibrar calibrando] calibrado [dict create recalibrar calibrando] calibrando [dict create done [dict create calibrado [dict create desconfigurado [dict create configurar configurando] configurando [dict create done configurado] configurado [dict create reconfigurar configurando] ] ] ] ] ] ]  conectado [dict create desconectar desconectado]]

#Etapa 9
set state_tree [dict create desconectado [dict create conectar [dict create conectado [dict create descalibrado [dict create calibrar calibrando] calibrado [dict create recalibrar calibrando] calibrando [dict create done [dict create calibrado [dict create desconfigurado [dict create configurar configurando] configurando [dict create done [dict create configurado [dict create ocioso [dict create irrigar irrigando] irrigando [dict create realizado irrigado] irrigado [ dict create fim ocioso ] irrigando [dict create parar ocioso] ocioso [dict create reconfigurar configurando ] ] ] ] configurado [dict create reconfigurar configurando] ] ] ] ] ] ]  conectado [dict create desconectar desconectado]]

E a seguir mostramos como usar o comando dict set para montar a variável state_tree, usando letra maiúscula para os nomes dos estados e letra minúscula para os eventos:

  dict set state_tree CONECTADO desconectar DESCONECTADO

dict set state_tree DESCONECTADO conectar CONECTADO \
    DESCALIBRADO calibrar CALIBRANDO

dict set state_tree DESCONECTADO conectar CONECTADO \
    CALIBRADO recalibrar CALIBRANDO

dict set state_tree DESCONECTADO conectar CONECTADO \
    CALIBRANDO realizado CALIBRADO \
    DESCONFIGURADO configurar CONFIGURANDO

dict set state_tree DESCONECTADO conectar CONECTADO \
    CALIBRANDO realizado CALIBRADO \
    CONFIGURADO reconfigurar CONFIGURANDO

dict set state_tree DESCONECTADO conectar CONECTADO \
    CALIBRANDO realizado CALIBRADO \
    CONFIGURANDO realizado CONFIGURADO \
    OCIOSO irrigar IRRIGANDO realizado

dict set state_tree DESCONECTADO conectar CONECTADO \
    CALIBRANDO realizado CALIBRADO \
    CONFIGURANDO realizado CONFIGURADO \
    IRRIGANDO realizado IRRIGADO

dict set state_tree DESCONECTADO conectar CONECTADO \
    CALIBRANDO realizado CALIBRADO \
    CONFIGURANDO realizado CONFIGURADO \
    IRRIGADO fim OCIOSO \
    reconfigurar CONFIGURANDO

Cada chamada do comando dict set representa um ramo completo da árvore desde a raiz até a última folha como indicado na figura 143.

Figura 143. Exemplo de um comando dict set representando um ramo completo da árvore de comportamento.

Exemplo de um comando dict set representando um ramo completo da árvore de comportamento.

Os nós de state_tree podem representar estados ou eventos e para permitir a identificação do nó foi criada a variável node_type contendo atributos dos dados contidos em state_tree com as seguintes informações:

  • type - estado ou evento

  • default - 0 ou 1 (no caso de ser um subestado indica se é o subestado default

  • level - indica o nível de profundidade na árvore, onde a raiz possui o nível 0

  • entry_action - ações que devem ser executadas ao entrar em um estado

  • exit_action - ações que devem ser executadas ao sair de um estado

O atributo nível de um evento permite identificar eventos com diferentes níveis de prioridade e dessa forma evitar a necessidade de mapear todas as possíveis combinações de eventos com estados.

Foram definidos os seguintes níveis:

  1. Conexão

  2. Calibração

  3. Configuração

  4. Irrigação

Um evento de nível 1 tem a maior prioridade e portanto o evento desconectar irá levar qualquer estado dos níveis 2, 3 ou 4 para o estado CONECTANDO.

Comandos para a criação da variável node_type:

#Node type
dict set node_type DISCONNECTED { type state default 1 level 1 entry_action stopIrrigation}
dict set node_type CONNECTED { type state default 0 level 1  entry_action activateButtons}
dict set node_type connect { type event level 1 }
dict set node_type disconnect { type event level 1 }
dict set node_type UNCALIBRATED { type state default 1 level 2 }
dict set node_type CALIBRATING { type state default 0  level 2 }
dict set node_type CALIBRATED { type state default 0 level 2 }
dict set node_type calibrate { type event level 2 } 
dict set node_type recalibrate { type event level 2 }
dict set node_type end_calibration {type event level 2 } 
dict set node_type UNCONFIGURED { type state default 1 level 3 }
dict set node_type CONFIGURING { type state default 0 level 3 }
dict set node_type CONFIGURED { type state default 0 level 3 }
dict set node_type configure { type event level 3 }
dict set node_type reconfigure { type event level 3 }
dict set node_type end_configuration {type event level 3 } 
dict set node_type IDLE { type state default 1 level 4 }
dict set node_type IRRIGATING { type state default 0 level 4 }
dict set node_type start_irrigation { type event level 4 }
dict set node_type stop_irrigation { type event level 4 }
dict set node_type end_irrigation { type event level 4 }

Para resgatar as informações sobre um determinado nó podemos usar os comandos:

  % dict get  $node_type DISCONNECTED type
  state
  % dict get  $node_type DISCONNECTED default
  1
  % dict get  $node_type DISCONNECTED level
  1
  % dict get  $node_type DISCONNECTED entry_action
  stopIrrigation

Como estratégia de busca na árvore armazenada na variável state_tree implementamos dois procedimentos (getChildNode e getNextState) usando método recursivo.

O procedimento getChildNode recebe como argumentos a variável state_tree e um nó (node) e usando a técnica de recursão procura na árvore state_tree pelo respectivo nó. Se encontrar o nó, e não for uma folha, armazena na variável global child_node_global o nó filho de node.

###############################################################
#Procedure getChildNode { state_tree node }
###############################################################

proc getChildNode { state_tree node } {
    
    global node_type child_node_global
    
    if { [ catch { set k [dict keys $state_tree] } ] } {

	return
    }

    
    foreach sub_node [dict keys $state_tree] {

	if {$sub_node == $node } {
	    
	    if { ![ catch { set child_node [dict keys [dict get $state_tree $sub_node]] } ] } {
	
		set child_node_global $child_node

	   #Command foreach because node may have many child nodes
		foreach c_n $child_node {
#		    puts "Info about $c_n \[dict get \$node_type $c_n\]:[dict get $node_type $c_n]"
		}
	    } else {
		set child_node [dict get $state_tree $sub_node]
		set child_node_global $child_node
#		puts "ChildNode of $node ($child_node) é uma folha do ramo."
#		puts "Info about $child_node \[dict get \$node_type \$child_node\]:[dict get $node_type $child_node]"
	    }
	    
	}
	
	if {[dict exists $state_tree $sub_node]} {

	    set sub_state_tree [dict get $state_tree $sub_node]

	    #puts "\nNew call of getChildNode: sub_state_tree:$sub_state_tree\n"
	    
	    getChildNode $sub_state_tree $node
	}
    }

}

E o procedimento getNextState recebe como argumentos a variável state_tree, um estado (state) e um evento (event) e usando também a técnica de recursão localiza na árvore o nó correspondente ao estado state e verifica se o evento event é um nó filho de state. Em caso positivo, armazena na variável global next_state_global o(s) nó(s) filho(s) do nó event, e em caso negativo retorna sem alterar o conteúda da variável global next_state_global.

###############################################################
#Procedure getNextState { state_tree state event }
###############################################################

proc getNextState { state_tree state event } {
    
    global node_type next_state_global
    
    if { [ catch { set k [dict keys $state_tree] } ] } {
	
	return
    }
    
    
    foreach sub_state [dict keys $state_tree] {
	
	if {$sub_state == $state } {
   
	    if { ![ catch { set child_node [dict keys [dict get $state_tree $sub_state]] } ] } {

		foreach c_n $child_node {
		    
		    if { $c_n == $event } {

			if { [ catch { set next_state [dict get $state_tree $sub_state $c_n] } ] } {
			    
			    set next_state [dict keys [dict get $state_tree $sub_state $c_n]]
			    
			}
			
			if { [llength $next_state] > 1 } { set next_state [dict keys $next_state] }
			
			#Store in a global variable the next state
			set next_state_global $next_state
			
		    } else {
			
			#puts "sub_state:$sub_state child_node:$child_node c_n:$c_n event:$event"
			#puts "$event NÃO é um evento compatível com $sub_state ($c_n é compatível)\n"
			
		    }
		    
		}
		
	    } else {
		
		set child_node [dict get $state_tree $sub_state]
		puts "ChildNode of $state ($child_node) é uma folha do ramo."
	    }
	    
	}
	
	if {[dict exists $state_tree $sub_state ]} {
	    
	    set sub_state_tree [dict get $state_tree $sub_state]
	    
	    getNextState $sub_state_tree $state $event
	}
    }
    
}

Nota

Essa implementação é mais complexa inicialmente mas facilita alterações futuras no comportamento do programa, pois basta modificar a estrutura da árvore armazenada na variável state_tree, e a atualização de node_type, inserindo ou removendo estados ou subestados.

16.5. Interface Gráfica

Com base nos procedimentos operacionais (conexão, calibração, configuração e irrigação) descritos na figura 139, projetamos uma interface gráfica inicial com os seguintes elementos:

  • ID do vaso - entrada (entry) para o identificador do vaso (variável: id_vaso)

  • Peso final - entrada (entry) para o peso que deve ser atingido (variável: p_f)

  • Peso atual - exibição do peso atual (variável: p_a)

  • botões de controle: Conectar, Tarar, Calibrar, Definir Peso Final, Iniciar Irrigação, Parar Irrigação e Sair)

Para criar a interface usamos os seguintes comandos:

  wm title . "Irrigador Automático"

global fonte_grande fonte_media 

set fonte_grande {Times 18}

set fonte_media {Times 14}

set status_conexao 0

label .id_vaso_l -text "ID do vaso:" -anchor w -font $fonte_grande

entry .id_vaso_e -textvariable id_vaso -relief sunken -width 10 -font $fonte_grande

label .peso_final_l -text "Peso final:" -anchor w -font $fonte_grande
label .peso_final_v -textvariable p_f -font $fonte_grande

label .peso_atual_l -text "Peso atual:" -anchor w -font $fonte_grande
label .peso_atual -textvariable p_a  -anchor w -font $fonte_grande

grid .id_vaso_l -row 0 -column 0 -sticky w
grid .id_vaso_e -row 0 -column 1
grid .peso_final_l -row 1 -column 0 -sticky w
grid .peso_final_v -row 1 -column 1
grid .peso_atual_l -row 2 -column 0 -sticky w
grid .peso_atual -row 2 -column 1

frame .plot

grid .plot -row 3 -column 0 -columnspan 4

button .conectar -text "Conectar" -command "objController connect" -font $fonte_grande
button .tarar -text "Tarar" -width 6 -command tarar -state disable -font $fonte_grande
button .calibrar -text "Calibrar" -width 6 -command "objController calibrate" -state disable -font $fonte_grande
button .configurar -text "Definir Peso Final" -command "objController configure" -state disable -font $fonte_grande
button .iniciar -text "Iniciar Irrigação"  -width 12 -command "objController startIrrigation" -state disable -font $fonte_grande
button .parar -text "Parar Irrigação"  -width 10 -command "objController stopIrrigation" -state disable -font $fonte_grande

button .sair -text "Sair" -font $fonte_grande -width 6 -command {
    catch {objCalib destroy}
    catch {objController destroy}
    exit
} 

grid .conectar -row 4 -column 0 -sticky we
grid .tarar -row 4 -column 1 -sticky we
grid .calibrar -row 4 -column 2 -sticky we
grid .configurar -row 4 -column 3 -sticky we
grid .iniciar -row 4 -column 4 -sticky we
grid .parar -row 4 -column 5 -sticky we
grid .sair -row 4 -column 6 -sticky we

Gerando uma interface gráfica (GUI) como mostra a figura 144.

Figura 144. Interface inicial do programa de controle do irrigador

Interface inicial do programa de controle do irrigador

Comecei a escrever o código com uma abordagem Procedural mas no meio do caminho passei a usar Programação Orientada a Objetos, por isso você vai encontrar alguns métodos que apenas chamam procedimentos definidos fora da classe.

Foram definidas as seguintes classes: Controller, Connector, Calibrator, CalibratorInterface, Irrigator, Instrument e Equipment, que interagem durante a irrigação conforme o Diagrama de Sequência da figura 145.

Figura 145. Diagrama de Sequência das interações entre os objetos durante uma operação de irrigação.

Diagrama de Sequência das interações entre os objetos durante uma operação de irrigação.

Implementamos a classe Controller para centralizar as etapas da irrigação. Observe que a maioria dos botões da interface gráfica chamam métodos da classe Controller através do objeto objController.

16.6. Classe Controller

Na classe Controller foram definidos os seguintes métodos:

  • connect: apenas chama o procedimento conectar

  • calibrate: se o irrigador não estiver no estado IRRIGATING, atualiza o estado atual para CALIBRATING e chama o procedimento calibrar

    É importante observar que esse método implementa uma exceção para o comportamento do estado IRRIGATING para evitar o início de uma calibração com o sistema irrigando. Mas segundo o diagrama de estados (Figura 140), qualquer subestado de CALIBRATED poderia ser modificado pelo comando calibrar. Para evitar o uso dessa exceção poderia ser incluído, na variável node_type, o comando stop_irrigation como um evento de saída (exit_action) do estado IRRIGATING. Ou seja, o sistema iria interromper a irrigação automaticamente e passar para o estado IDLE antes de entrar nos estado CALIBRATING.

  • setState: verifica se o novo estado possui múltiplos subestados e atualiza automaticamente a variável current_state para o subestado com o atributo default.

  • getState: retorna o estado atual

  • nextState: limpa o conteúdo da variável next_state_global, recebe como argumento um evento e chama o procedimento getNextState passando como argumento o estado atual e o evento, e retorna o próximo estado armazenado na variável global next_state_global.

    A variável global next_state_global é acessada apenas por esse método e pelo procedimento getNextState, o qual é chamado somente dentro deste método (nextState).

  • setPoint: armazena o set point na variável da classe set_point

  • getSetPoint: retorna o set point

  • configure: inicialmente verifica se o estado atual é UNCONFIGURED ou IDLE e chama o método nextState, passando como argumento o evento correspondente ao estado atual. Em seguida atualiza o estado para CONFIGURING, chama o procedimento configurar e após o retorno de configurar atualiza o estado atual para CONFIGURADO.

  • setIrrigator: armazena o nome do objeto da classe Irrigator na variável da classe obj_irrigator

  • startIrrigation: se o evento start_irrigation for compatível com o estado atual, atualiza o estado com o método setState e chama o método start do objeto da classe Irrigator, armazenado na variável da classe obj_irrigator, passando como argumento o set point. Se o estado atual não for compatível com o evento start_irrigation exibe mensagem para o usuário.

  • stopIrrigation: se o evento stop_irrigation for compatível com o estado atual, atualiza o estado com método setState e interrompe a irrigação chamando o método stop do objeto da classe Irrigator cujo nome está armazenado na variável da classe obj_irrigator. E se o estado atual não for compatível com o evento stop_irrigation exibe mensagem para o usuário.

  • endIrrigation: método que é chamado pelo objeto da classe Irrigator apenas notificando o fim da irrigação para o objeto da classe Controller atualizar o estado atual.

Classe Controller:

###############################################################
#Controller class
###############################################################

oo::class create controller {

    variable set_point current_state input next_state obj_irrigator

    constructor {} {
	set current_state "DISCONNECTED"
    }
    
    method connect {} {
	conectar
    }

    method calibrate {} {

	if { [[self] getState] != "IRRIGATING" } {
	    my setState "CALIBRATING"
	    calibrar
	} else {
	    #http://wiki.tcl.tk/1062
	    option add *Dialog.msg.font {Times 18}
	    tk_messageBox -message "O estado [[self] getState] não permite esta ação!"
	}
    }

    method setState {s} {

	#To check if $s is a superstate composed of many substates
	#To set the global variable child_node_global to the child node of new state $s
	
	getChildNode $::state_tree $s

	puts "[self] setState -> $::child_node_global:[llength $::child_node_global] new state solicitado s:$s"

	#Checks whether the requested state consists of substates and if YES replaces by the default substate.
	
	if { [llength $::child_node_global] > 1 } {

	    foreach cn $::child_node_global {

		if { ([dict get $::node_type $cn type] == "state") && ([dict get $::node_type $cn default] == 1) } {

		    puts "Current state -> [[self] getState]"
		    set current_state $cn
		    puts "Changing state of [self] to a SUBSTATE [[self] getState]"
		    return
		}

		if { [dict get $::node_type $cn type] == "event" } {

		    puts "method setState EVENTO s:$s cn:$cn"
		    puts "Current state -> [[self] getState]"
		    set current_state $s
		    puts "Changing state of [self] to a SUBSTATE [[self] getState]"
		    return
		}

	    }

	} else {

	    puts "Current state -> [[self] getState]"
	    set current_state $s
	    puts "Changing state of [self] to [[self] getState]"
	    
	}

    }

    method getState {} {
	return $current_state
    }    

    method nextState { event } {

	puts "method nextState event:$event"
	set ::next_state_global ""
	getNextState $::state_tree [[self] getState] $event
	return $::next_state_global
	
    }
    
    method setPoint { p } {
	set set_point $p
	puts "setPoint of [self] set_point:$set_point"
    }

    method getSetPoint {} {

	return $set_point

    }
    
    method configure {} {
	
	puts "method configure state:[[self] getState]"
	
	if { [[self] getState] == "UNCONFIGURED" } {
	    
	    set next_state [[self] nextState configure]
	    
	    puts "method configure primeiro if next_state:$next_state"
	    
	    if { $next_state != "" } {
		
		tk_messageBox -message "Transição do estado [[self] getState] para $next_state!"
		
		[self] setState $next_state
		
		configurar
		
		set next_state [[self] nextState end_configuration]
		
		[self] setState $next_state
		
		
	    }
	    
	} elseif { [[self] getState] == "IDLE" } {
	    
	    set next_state [[self] nextState reconfigure]
	    
	    puts "method configure segundo if next_state:$next_state"
	    
	    if { $next_state != "" } {
		
		tk_messageBox -message "Transição do estado [[self] getState] para $next_state!"
		
		[self] setState $next_state
		
		configurar
		
		set next_state [[self] nextState end_configuration]
		[self] setState $next_state
		
	    }

	} else {
	    #    #http://wiki.tcl.tk/1062
	    option add *Dialog.msg.font {Times 18}
	    tk_messageBox -message "O estado [[self] getState] não permite esta ação!"
	    
	}
    }    
    
    method setIrrigator { obj } {
	set obj_irrigator $obj
    }

    method startIrrigation {} {

	#Check if the requested event (start_irrigation) is compatible with the current state.
	
	set next_state [objController nextState start_irrigation]

	puts "startIrrigation next_state:$next_state"

	if { $next_state != "" } {

	    tk_messageBox -message "Transição do estado [objController getState] para $next_state!"

	    objController setState $next_state

	    #Call the method start and pass the set_point
	    $obj_irrigator start [my getSetPoint]

	    
	} else {
	    
	    tk_messageBox -message "O estado [objController getState] não permite esta ação!"

	}
    }

    method stopIrrigation {} {
	
	#Check if the requested event (stop_irrigation) is compatible with the current state.
	
	set next_state [objController nextState stop_irrigation]
	
	puts "stopIrrigation next_state:$next_state"
	
	if { $next_state != "" } {
	    
	    tk_messageBox -message "Transição do estado [objController getState] para $next_state!"
	    
	    objController setState $next_state

	    $obj_irrigator stop
	    
	} else {
	    
	    tk_messageBox -message "O estado [objController getState] não permite esta ação!"
	    
	}
	
    }

    method endIrrigation {} {

	#Check if the requested event (end_irrigation) is compatible with the current state.
	
	
	set next_state [objController nextState end_irrigation]
	
	puts "endIrrigation next_state:$next_state"
	
	if { $next_state != "" } {
	    
	    tk_messageBox -message "Transição do estado [objController getState] para $next_state!"
	    
	    objController setState $next_state

	} else {
	    
	    tk_messageBox -message "O estado [objController getState] não permite esta ação!"
	    
	}
	
    }
	
}

16.7. Conexão

O botão Conectar deve abrir a porta serial, iniciar uma rotina de solicitação de leituras de massa, verificar se a conexão está aberta e se as leituras estão sendo enviadas. Para isso criamos os procedimentos: conectar, iniciar_leitura, parar_leitura, ler_peso e ler_porta. Mais tarde definimos a classe Connector.

O objeto da classe Connector é criado com o comando:

connector create objConnector

E os respectivos métodos são usados pelo procedimento conectar:

  proc conectar { } {
    
    global id_porta status_conexao

    puts "proc conectar: status_conexao $status_conexao"
    
    if { $status_conexao == 0 } {

	   set id_porta [objConnector openPort]
	   
	   if {$id_porta != "SERIAL_ERRO_OPEN"} {

	       .conectar configure -text "Desconectar" -foreground red
	       .tarar configure -state normal
	       .calibrar configure -state normal
	       .configurar configure -state normal
	       .iniciar configure -state normal
	       .parar configure -state normal
	       
	       set status_conexao 1


	       #Set the state of objController
	       objController setState "CONNECTED"

	       iniciar_leitura
	       
#Cria um objeto da classe Calibrator com o nome objCalib
#que irá carregar o arquivo de calibração.
#Se não existir arquivo de calibração então irá chamar automaticamente
#o método buildInterface da classe calibratorInterface, herdada como "mixin"

	       calibrator create objCalib ./calIrrigator.txt

	       #info about the instance of calibrator
	       #puts "\[info commands objCalib\]:[info commands objCalib]"
	       #puts "\[info class instances calibrator\]:[info class instances calibrator]"

	      }
	      
       } elseif { $status_conexao == 1 } {

	   objController stopIrrigation
	   
	   parar_leitura

	   objConnector closePort
	   
	   set status_conexao 0
	   
	   .conectar configure -text "Conectar" -foreground blue
	   .tarar configure -state disable
	   .calibrar configure -state disable
	   .configurar configure -state disable
	   .iniciar configure -state disable
	   .parar configure -state disable

	   objCalib destroy


	   puts "interrompendo conexao dentro de conectar status_conexao: \
$status_conexao"

	   #Set the state of objController
	   objController setState "DISCONNECTED"
	   
       }
}

E dos procedimentos acessórios: iniciar_leitura, parar_leitura, ler_peso e ler_porta:

  
proc ler_peso { canal } {
    
    global status_leitura id_ler_peso

    #set rtn [catch { puts $canal "get;a0" } resultado]

    set rtn [objConnector writeChannel "get;a0"]
    
    if {[lindex $rtn 0]} {
	puts "Erro ler_peso: [lindex $rtn 1]"
	conectar
	return
    }
    
    if {$status_leitura == "OPERATION"} {
	    
	    set id_ler_peso [after 1000 [list ler_peso $canal]]
	    
	}
	
}

proc iniciar_leitura {} {

global id_porta status_leitura

#Verifica se a variável já foi definida e se o valor atual é 1
if { [info exists status_leitura] && ($status_leitura == "OPERATION") } {

       puts "************************leitura em andamento*************"
       return
   }

fileevent $id_porta readable [list ler_porta $id_porta]

set status_leitura "OPERATION"

set status_conexao 1

ler_peso $id_porta

#Start the execListCommands method of Connector class

objConnector execListCommands $id_porta


}

proc parar_leitura {} {
    
    global id_porta status_leitura id_ler_peso status_conexao
    
    puts [info vars]
    if { [info exists id_porta] } {
	puts [file channels $id_porta]
	puts [string length [file channels $id_porta]]
    }
    #puts [tell $id_porta]
    
    #if { [string length [file channels $id_porta]] == 0 } {
    #	  puts "porta fechada"
    #	  return
    #   }
    
    if { [info exists status_leitura] && $status_leitura == "STOPPED" } {
	puts "*****************Leitura interrompida*****************"
	return
    }
    
    #Essa alternativa não funcionou
    #if { [catch {[tell $id_porta]}]  != 0 } {
    #       puts "porta fechada"
    #       return
    #   }
    
    
    if { [catch {fileevent $id_porta readable ""} result] } {
	puts "Erro parar_leitura: $result"
    }
    
    set status_leitura "STOPPED"

    #puts "parar_leitura id_ler_peso: $id_ler_peso"
    
    catch {after cancel $id_ler_peso}

    objConnector cancelExecListCommands
    
}

proc ler_porta { canal } {

#p_a - peso atual
    global p_a
    
    if { [eof $canal ] } {
	puts stderr "Fechando $canal"
	catch { close $canal }
	return }
    

    set dados [objConnector readChannel $canal]
    
       #puts $dados       

#Remove alguns caracteres não imprimíveis enviados pelo Arduino
    if {$dados == ""} {
	return
    }
	  
#Divide a string "dados" (Ex: pin:a0;media:611)
#usando o caracter ";" como separador de campos 
#e retorna a lista com 2 elementos {pin:a0 media:611}
	  set lista [split $dados ";"]
	  
#Retorna o segundo elemento da lista (media:611)
	  set leitura [lindex $lista 1]
	  
#Divide novamente a string "leitura" (media:611)
#em uma lista {readout 611}
	  set lista [split $leitura ":"]
	  
#Retorna apenas o segundo elemento da lista (611)
	  set numero [lindex $lista 1]

#Check the state of objController to 

    if {([objController getState ] == "CALIBRATED") || \
	    ([objController getState ] == "UNCONFIGURED") || \
	    ([objController getState ] == "CONFIGURED")   || \
	    ([objController getState ] == "CONFIGURING")  || \
	    ([objController getState ] == "IRRIGATING")   || \
	    ([objController getState ] == "IDLE")            } {
	
	set p_a [format "%.2f" [objCalib convert $numero]]
	
    } else {

	set p_a $numero
    }

}

E criação da classe Connector com os métodos:

  • openPort

  • closePort

  • readChannel

  • writeChannel

  • editListCommands

  • execListCommands

  • cancelExecListCommands

###############################################################
#Class Connector
###############################################################

oo::class create connector {

    variable dev id_dev list_commands id_after_execListCommands 

    constructor {} {
	set dev "/dev/ttyACM0"
    }

    method openPort {} {

#Do módulo wtw_multi340i.tcl do programa multipar_02.tcl

	set baud 9600
	set paridade n
	set bit_dados 8
	set bit_parada 1
	
	
    set resultado_1 [catch { set id_dev [open $dev r+] }]
    set resultado_2 [catch {fconfigure $id_dev -mode "$baud,$paridade,$bit_dados,$bit_parada" }]

    #Modificação no código para poder usar um simulador da balança    
#    set resultado_1 [catch { set id_dev [open "|tclsh simu_scale.tcl" r+] }]
#    set resultado_2 [catch { fconfigure $id_dev -buffering line }]
    
    if { ($resultado_1 != 0) || ($resultado_2 != 0) } {
	   set id_dev "SERIAL_ERRO_OPEN"
	   return $id_dev
       } else {
	   #A opção -blocking 0 fez uma grande diferença na atualização das telas
	   fconfigure $id_dev -blocking 0 -buffering none
	   
	   return $id_dev
       }
    }

    method closePort {} {
	set falha [catch {close $id_dev} result]
	
	if {$falha} {
	    puts "Erro conectar/desconectar: $result"
	}
    }


    method readChannel { ch } {

	set dado [gets $ch]
	
	#puts "Method readChannel ch:$ch dado:$dado"
	
	return $dado
       
	
    }

    #ch - channel to be used
    #cmd - command to be sent by channel ch
    
    method writeChannel { cmd } {

        [self] editListCommands append_cmd $cmd
	
	return {0 0}
    }

    method editListCommands { op {cmd {}} } {
	
	if {$op == "append_cmd" } {
	    lappend list_commands $cmd
	    #puts "Method editListCommands APPEND_CMD cmd:$cmd list_commands:$list_commands"
	    
	} elseif { $op == "get_cmd" } {
	    set command_to_write [lindex $list_commands 0]
	    set list_commands [lrange $list_commands 1 end]
	    #puts "Method editListCommands GET_CMD cmd:$cmd list_commands:$list_commands"
	    return $command_to_write
	}
	
    }

    method execListCommands { ch } {

	set command_to_write [my editListCommands get_cmd]

	if {$command_to_write != ""} {
	    #puts "Method execListCommands command_to_write:$command_to_write"
	    
	    
	    set rtn [catch { puts $ch $command_to_write } resultado]
	    
	    if { $rtn } {
		puts "Erro: $resultado"
		conectar
		return
	    }

	} else {
	    
	    #puts "Method execListCommands NOCOMMAND to write"
	    
	}
	
	set id_after_execListCommands [after 500 [list [self] execListCommands $ch]]
	
    }
    
    method cancelExecListCommands {} {
	
	#puts "method cancelExecListCommands id_after_execListCommands:$id_after_execListCommands"
	
	after cancel $id_after_execListCommands
	
    }
}

O envio de comandos pela porta serial é centralizado pelos métodos writeChannel, editListCommands e execListCommands da classe Connector.

O método writeChannel chama o método editListCommands para adicionar um comando na lista de comandos (list_commands) que devem ser enviados pela porta serial.

O método editListCommands permite adicionar um novo comando no final da lista (list_commands) ou remover o primeiro comando da lista dependendo do primeiro parâmetro argumento (op).

E o método execListCommands envia os comandos armazenados na variável do tipo lista list_commands através do canal especificado no argumento ch. Ele é executado em intervalos de 500 ms com o uso do comando after:

  set id_after_execListCommands [after 500 [list [self] execListCommands $ch]]

Essa estratégia foi adotada para controlar o intervalo de tempo entre as mensagens enviadas pela porta USB para o Arduino.

Para facilitar o desenvolvimento sem ter o irrigador (com o Arduino e balança) conectado ao Laptop, usei um programa para simular o envio dos dados enviados pelo Arduino (simu_scale.tcl) para poder testar o código. E para usar o simulador incluimos os seguintes comandos no método openPort:

#Modificação no código para poder usar um simulador da balança    
#    set resultado_1 [catch { set id_dev [open "|tclsh simu_scale.tcl" r+] }]
#    set resultado_2 [catch { fconfigure $id_dev -buffering line }]

Para usar o simulador bastava descomentar essas duas linhas e comentar os comandos de abertura da porta serial:

  set resultado_1 [catch { set id_dev [open $dev r+] }]
  set resultado_2 [catch {fconfigure $id_dev -mode "$baud,$paridade,$bit_dados,$bit_parada" }]

Código do simulador simu_scale.tcl:

  #Simulador da placa Arduino enviando as leituras
#analógicas do pino a0 no formato:
#pin:a0;media:611

set weight 0
set i 1

proc read_input {} {

    global weight i

    gets stdin ic

    if {($weight < 0) || ($weight > 100)} { set i [expr $i * -1] }
	    
    incr weight $i
    
    puts "pin:a0;media:$weight"

}


fileevent stdin readable "read_input"

vwait forever

16.8. Calibração

Para a etapa de calibração criamos o procedimento calibrar e as classes Calibrator e calibratorInterface.

Métodos da classe Calibrator:

  • loadFile: carrega o arquivo de calibração no diretório local

  • saveFile: grava o arquivo de calibração no diretório local

  • convert: converte uma leitura em unidades ADC em [g|Kg] aplicando a função de calibração (linear)

  • saveCalData: adiciona um par (r, w - leitura, padrão) á variável lista cal_data

  • calibrate: aplica a função estatística ::math::statistics::linear-model do pacote math::statistics da Tcllib

  • getN_std: retorna o número de padrões usados na calibração

  • getUnity: retorna a unidade [g|Kg]

  • setStd: armazena a massa do padrão na variável readout

  • getStd: retorna a massa do padrão armazenado na variável readout

###############################################################
#Class Calibrator
###############################################################

oo::class create calibrator {

    mixin calibratorInterface
    
    #cal_file stores the parameters of linear regression (a and b)
    #https://en.wikipedia.org/wiki/Simple_linear_regression
    #slope - slope of regression line
    #intercept - intercept of regression line
    #readout - readout of load cell in ADC units
    #unity - unity (mg, g or Kg)
    #n_std - número de padrões para calibração

    variable cal_file readout slope intercept unity n_std cal_data

    #http://wiki.tcl.tk/3469 - persistent array
    constructor { cal_file } {
	if {[file exists $cal_file]} {
	    puts "constructor calibrator file exists: $cal_file"
	    my loadFile $cal_file
	    objController setState "CALIBRATED"
	} else {
	    #puts "constructor calibrator file doesn't exist: $cal_file."
	    #call de method buildInterface from the mixin class calibratorInterface
	    objController setState "CALIBRATING"
	    calibrar
	    objController setState "CALIBRATED"
	    
	}
	
    }
   
    method loadFile { cal_file } {

	set channel [open $cal_file r]
	set par_eq [read $channel]
	close $channel
	set slope [dict get $par_eq slope]
	set intercept [dict get $par_eq intercept]
	set unity [dict get $par_eq unity]

	puts "slope:$slope intercept:$intercept unity:$unity"
	
    }

    method saveFile {} {

	#save cal_file
	set par_eq_calib [ dict create slope $slope intercept $intercept unity $unity]
	
	set channel [open calIrrigator.txt w]

	puts $channel $par_eq_calib
	
	close $channel

    }
    
    method convert { r } {
	return [expr $slope * $r + $intercept]
    }

    #r: readout of standard (ADC)
    #w: weight of standard (g or Kg)

    method saveCalData { r w } {

	lappend cal_data $r $w

	puts "method saveCalData cal_data:$cal_data"
    }

    method calibrate {} {
	
	foreach {r w} $cal_data {
	    lappend r_data $r
	    lappend w_data $w
	}

	puts "$r_data $w_data"
	
	set linear_model [::math::statistics::linear-model $r_data $w_data]
	set intercept [lindex $linear_model 0]
	set slope [lindex $linear_model 1]

	my saveFile
	
    }

    method getN_std {} {
	return $n_std
    }

    method getUnity {} {
	return $unity
    }

    method setStd {w} {
	set readout $w
    }

    method getStd {} {
	return $readout
    }
    
}

Métodos da classe calibratorInterface:

  • buildInterface

  • getInterface

  • closeInterface

###############################################################
#Class calibratorInterface
###############################################################


oo::class create calibratorInterface {

    variable cal_interface n_std 
    
    constructor {args} {
	puts "constructor of calibratorInterface args:$args"
	next $args
    }

    method buildInterface {} {
	
    }

    method getInterface {} {
	if {[info exists cal_interface]} {
	    return $cal_interface
	} else {
	    return ""
	}
    }
    method closeInterface {} {
	destroy .c_i
    }
}

Observei que ao incluir a classe calibratorInterface como Mixin da classe calibrator, passei a ter alguns erros. Descobri que era necessário incluir um argumento args no constructor de calibratorInterface e um next $args para chamar o constructor de calibrator e passar o nome do arquivo de calibração para o constructor de calibrator.

O método buildInterface é definido na classe calibratorInterface mas é modificado com o comando oo::objdefine dentro do procedimento calibrar ao longo das etapas de calibração.

Ao executar o programa, é criado o objeto objCal, da classe Calibrator, e carrega a equação de calibração (calIrrigator.txt) automaticamente do diretório local.

Mas se não houver um arquivo de calibração no diretório local, o objeto objCal chama o procedimento calibrar e abre uma janela toplevel para executar a calibração.

O botão calibrar será usado apenas para realizar uma recalibração e atualizar o arquivo de calibração calIrrigator.txt.

Na interface gráfica, o botão calibrar chama o método calibrate da classe Controller, dentro do qual é feita uma verificação se a transição é possível e em caso afirmativo o procedimento calibrar é executado.

E o procedimento calibrar:

  proc calibrar {} {

    global p_a
    
    .calibrar configure -state disable

    objController setState "CALIBRATING"
    
    oo::objdefine objCalib {

	method buildInterface {} {
	    set [my varname cal_data] {}
	    set cal_interface .c_i
	    destroy $cal_interface
	    toplevel $cal_interface
	    wm title $cal_interface "Calibração"
	    wm resizable $cal_interface 0 0
	    
	    label $cal_interface.l_std -text "No. Padrões" -anchor w
	    entry $cal_interface.e_std -textvariable [my varname n_std] -relief sunken -width 5
	    
	    label $cal_interface.l_u -text "Unidade:" -anchor w
	    radiobutton $cal_interface.g -text "(g)" -variable [my varname unity] -relief flat -value g
	    radiobutton $cal_interface.kg -text "(Kg)" -variable [my varname unity] -relief flat -value kg
	    
	    button $cal_interface.cont -text "Continuar" -command "set cont 1"
 	    
	    grid $cal_interface.l_std -row 0 -column 0 -sticky w -pady 5
	    grid $cal_interface.e_std -row 0 -column 1
	    grid $cal_interface.l_u -rowspan 2 -column 0
	    grid $cal_interface.g -row 1 -column 1 -sticky w 
	    grid $cal_interface.kg -row 2 -column 1 -sticky w 
	    grid $cal_interface.cont -row 3 -columnspan 2 -pady 5
	    
	    set cont 0
	    vwait cont

	    #After replace at entry:
	    #-textvariable n_std
	    #to
	    #-textvariable [my varname n_std]
	    #and at radiobutton:
	    #-variable unity
	    #to
	    #-variable [my varname unity]
	    #And I didn't have to implement
	    #setN_std and setUnity method and the commands:   
	    #[self] setN_std [$cal_interface.e_std get]
	    #[self] setUnity $::unity
	    
	    
	}
    }

#If user press button "calibrar" while calibratorInterface exists
#https://groups.google.com/forum/#!topic/comp.lang.tcl/7_berBkDVng    
#    if { ![winfo exists [objCalib getInterface]] } {
#	objCalib  buildInterface
#	puts "buildInterface"
#    }

    #Call method buildInterface to configure the number of standards
    #and the unity of calibration
    objCalib  buildInterface
    
    oo::objdefine objCalib {

	method buildInterface { p } {
	    set cal_interface .c_i
	    destroy $cal_interface
	    toplevel $cal_interface
	    wm title $cal_interface "Calibração"
	    wm resizable $cal_interface 0 0

	    label $cal_interface.l_std -text "Padrão $p ([set [my varname unity]])" -anchor w
	    entry $cal_interface.e_std -textvariable std -relief sunken -width 8
	    button $cal_interface.cont -text "Continuar" -command "set cont 1"

	    grid $cal_interface.l_std -row 0 -column 0 -pady 5
	    grid $cal_interface.e_std -row 0 -column 1
	    grid $cal_interface.cont -row 1 -columnspan 2 -pady 5

	    set cont 0
	    vwait cont

	    [self] setStd [$cal_interface.e_std get]
	}

    }

    #Loop to store readings of standards for calibration
    set n_std [objCalib getN_std]
    
    for {set i 1} {$i <= $n_std} {incr i} {
	
	#Call method buildInterface to saveCalData
	objCalib  buildInterface $i
	objCalib  saveCalData $p_a [objCalib getStd]
	
    }

    objCalib calibrate
    
    puts "calibrar n_std:[objCalib getN_std] unity:[objCalib getUnity] "

    objCalib closeInterface

    objController setState "CALIBRATED"
    
    .calibrar configure -state normal

}

16.8.1. Construindo uma equação de calibração

Para construir uma equação de calibração utilizamos como padrões de massa algumas pedras cujos pesos foram medidos em outra balança,usada como referência, e foi obtida a seguinte equação de calibração:

Equação 20. Equação de calibração da balança com 3 padrões

massa (g) = 6,7058 × leitura (ADC) - 396,45


Tabela 14. Tabela de calibração

Massa padrão (g)Leitura (ADC)Massa calculada (g)Massa após correção da tara (g)Erro (g)Erro (%)
0 49 -67,8658 0 - -
1627,4 307,6 1666,25 1734,12 106,72 6,55
3344,2 580,3 3494,92 3562,79 218,59 6,53
5019,5 789,5 4897,78 4965,64 -53,85 -1,07

A figura 146 mostra o gráfico e a equação de calibração utilizando todos os padrões de calibração.

Figura 146. Gráfico da calibração com a equação e o coeficiente de correlação (R2)

Gráfico da calibração com a equação e o coeficiente de correlação (R2)

Os dados da tabela 14 mostram que para leituras de massa acima da faixa de ~3Kg a célula de carga demonstra um desvio da linearidade.

Dependendo da exatidão exigida esses desvios podem ser toleráveis mas se for necessário maior exatidão pode-se utilizar uma faixa de calibração menor descartando o padrão de de ~5Kg e gerando a equação 21 com os dados da tabela 15.

Equação 21. Equação de calibração da balança com 2 padrões

massa (g) = 6,2944 × leitura (ADC) - 308,54


Tabela 15. Tabela de calibração

Massa padrão (g)Leitura (ADC)Massa calculada (g)Massa após correção da tara (g)Erro (g)Erro (%)
0 49 -0,11 0 - -
1627,4 307,6 1627,62 1627,73 0,3318 0,02%
3344,2 580,3 3344,10 3344,21 0,0147 4,4 × 10-4%

A figura 147 mostra o gráfico e a equação de calibração utilizando apenas 2 padrões de calibração.

Figura 147. Gráfico da calibração com a equação e o coeficiente de correlação (R2)

Gráfico da calibração com a equação e o coeficiente de correlação (R2)

16.9. Configuração

Na etapa de configuração o usuário deve definir o peso que deve ser obtido no final da irrigação. Ao clicar no botão Definir Peso Final é executado o método configure da classe Controller, o qual verifica a compatilidade do evento com o estado atual e se for compatível chama o procedimento configurar.

Procedimento configurar:

  proc configurar {} {

    global fonte_grande
    
    set conf_interface .conf_int
    destroy $conf_interface
    toplevel $conf_interface
    wm title $conf_interface "Definir o Peso Final"
    wm resizable $conf_interface 0 0

    label $conf_interface.l -text "Peso Final" -anchor w -font $fonte_grande
    entry $conf_interface.e -textvariable p_f -relief sunken -width 5 -font $fonte_grande

    button $conf_interface.cont -text "Continuar" -font $fonte_grande -command {
	objController setPoint $p_f
	set cont 1
    }
    
    grid $conf_interface.l -row 0 -column 0 -sticky w -pady 5
    grid $conf_interface.e -row 0 -column 1
    grid $conf_interface.cont -row 1 -columnspan 2 -pady 5

    set cont 0
    vwait cont
    destroy $conf_interface
}

O comando vwait cont interrompe a execução do programa até o usuário clicar no botão Continuar. A partir de então o objeto objController chama o método setPoint passando como argumento a variável p_f contendo o valor digitado pelo usuário, fecha a interface de configuração e retorna para o método configure.

16.10. Irrigação

Para realizar a irrigação foram implementadas as classes Irrigator, Instrument e Equipment.

###############################################################
#Class Irrigator
###############################################################

oo::class create irrigator {

    variable instrument equipment set_point
    
    method start { sp } {

	set set_point $sp
	puts "method start of [self] class Irrigator for set point $set_point"
	trace add variable ::p_a write [list [self] checkWeight]

	#tk_messageBox -message "Fim da irrigação!"
	#objController endIrrigation
    }

    method stop {} {
	
	puts "method stop of [self] class Irrigator"

	trace remove variable ::p_a write [list [self] checkWeight]

	$equipment(pump) turnOFF
    }
    
    method setInstrument { n } {

	set instrument(name) $n
	puts "[self] method setInstrument"

    }

    method setEquipment { n } {

	set equipment(pump) $n
	puts "[self] method setEquipment equipment(pump):$equipment(pump)"
    }

    method checkWeight {name index operation} {

	upvar $name current_weight

	if { $current_weight < $set_point } {
	    $equipment(pump) turnON
	} else {
	    $equipment(pump) turnOFF
	    my stop
	    objController endIrrigation
	}
	
	puts "Method checkWeight name:$name current_weight:$current_weight index:$index operation:$operation"

    }
    
    
}

###############################################################
#Class Instrument
###############################################################

oo::class create instrument {

    variable  state

    constructor { n } {

	set state(name) $n
    }

}


###############################################################
#Class Equipment
###############################################################

oo::class create equipment {

    variable state

    constructor { n } {

	set state(name) $n
    }

    method turnON {} {

	puts "Turning ON the equipment $state(name)"

	#objConnector writeChannel "set;d11;1"

	objConnector writeChannel "set;d11;100"
    }

    method turnOFF {} {

	puts "Turning OFF the equipment $state(name)"
	objConnector writeChannel "set;d11;0"
    }
    
}

Usando o conceito de agregação o objeto da classe Irrigator (objIrrigator) possui (ou se conecta) com o objeto da classe Instrument (objInstrument) que representa a balança e o objeto da classe Equipment (objEquipment) que representa a bomba.


###############################################################################################
#Cria um objeto da classe Instrument com o nome scale e agrega ao objeto da classe Irrigator
###############################################################################################

instrument create objInstrument scale

objIrrigator setInstrument objInstrument

###############################################################################################
#Cria um objeto da classe Equipment com o nome pump e agrega ao objeto da classe Irrigator
###############################################################################################

equipment create objEquipment pump

objIrrigator setEquipment objEquipment

E da mesma forma, o objeto da classe Irrigator é agregado ao objeto da classe Controller.

  
###############################################################################################
#Cria um objeto da classe Irrigator que é agregado ao objeto da classe Controller
###############################################################################################

irrigator create objIrrigator

objController setIrrigator objIrrigator

O objeto da classe Controller (objController) chama os métodos start e stop da classe Irrigator, para iniciar e interromper a irrigação, e no final da irrigação recebe como retorno a chamada do método endIrrigation por parte do objeto da classe objIrrigator.

O método start, da classe Irrigator, utiliza o comando:

trace add variable ::p_a write [list [self] checkWeight]

para associar a chamada do método checkWeight sempre que a variável global p_a for atualizada (escrita).

E o método stop da classe Irrigator remove essa associação com o comando:

trace remove variable ::p_a write [list [self] checkWeight]

O método checkWeight compara o conteúdo da variável current_weight (peso atual) com o conteúdo da variável set_point e atua conforme o caso:

  if { $current_weight < $set_point } {
	    $equipment(pump) turnON
	} else {
	    $equipment(pump) turnOFF
	    my stop
	    objController endIrrigation
	}

No final da irrigação, se encarrega de desligar a bomba e chama o método endIrrigation do objeto objController para notificar o fim da irrigação e a respectiva mudança de estado.