10. Sistema Didático - Programação em Tcl/Tk.

Agora vamos mostrar a programação do Arduino e o uso da linguagem Tcl/Tk no desenvolvimento de um programa (com interface gráfica - GUI) para controlar o funcionamento da bomba centrífuga e da válvula solenóide, e monitorar a vazão gravimétrica no reservatório superior, através da comunicação serial com o Arduino.

10.1. Programação do Arduino (Sketch)

Implementamos o código no Arduino para permitir a comunicação serial com o PC através de comandos com o formato: [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

Versão final do Sketch: (Fonte: Building a Respirometer using Finite State Machine (FSM), Tcl/Tk and Arduino)

/*
Final version of the sketch to interpret the commands SET and GET
07/02/2014
*/

char message[15];
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 9-12 are configured as digital output pins.

  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 
}

void loop() {

  for (int i = 0; i < 15; i++) {
    message[i] = 0;
    }     
  
  if (Serial.available() > 0) {
    
   
    //Wait to receive the message
    //with 9600 baud (9600 bits/s = (9600/8) bytes / sec
    //1200 bytes/s = 1200 characters/s = 1200 caractere/1000ms
    //therefore each character takes approx. 1.2 ms to be transmitted
    //50 ms are therefore sufficient for receiving approx. 40 characters
 
    delay(50); 
    
    
    length_message = Serial.available();
    
    if (length_message >= 15) {
      
      Serial.print("message larger than the limit (15)!");
      Serial.println(length_message);
      
            
      for (int i = 0; i < length_message; i++) {
          c = Serial.read();
	 }
                 
    } else {
      
      for (int i = 0; i < length_message; i++) {
	c = Serial.read();
        if (c != '\n' && c != '\r') {
	  message[i] = c;
        }
      }
      
      
      //function which returns the array size
      //but does not count the null '\0' end-of-string character

      length = strlen(message); 
      
      message[length] = '\0'; // message[length] = 0
      
      cmd = strtok(message, ";");

         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");

      }
          
	
      
    
    }
  }
  
}


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);
 
    Serial.print("pin:");  
    Serial.print(pin);  
    Serial.print(";");
    Serial.print("readout:");
    Serial.println(readout);
   
 }
   
}

10.2. Programa em Tcl/Tk

Tcl (pronuncia-se ticol, a abreviação de Tool Comand Language) é uma linguagem de script muito útil que roda na maioria das plataformas UNIX e Windows. Combinada com a biblioteca gráfica Tk (pronuncia-se tikei), a qual fornece os Widgets (elementos gráficos), a Tcl pode ser usada para gerar programas com interfaces gráficas com o usuário.

Vamos aproveitar os recursos da Tcl/Tk e criar inicialmente uma interface gráfica com apenas dois botões para ligar e desligar a bomba centrífuga.

Figura 65. Interface contendo os botões para ligar e desligar a bomba.

Interface contendo os botões para ligar e desligar a bomba.

10.2.1. Abrindo a porta serial

Para abrir um arquivo: open [nome_do_dispositivo]

No caso da primeira porta serial do Linux: open /dev/ttyS0

E no Windows: open com1:

Mas para identificar qual o nome do dispositivo serial que o sistema associa ao Arduino observe a saída do comando dmesg:

New USB device found, idVendor=2341, idProduct=0043
usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=220
usb 2-1: Manufacturer: Arduino (www.arduino.cc)
usb 2-1: SerialNumber: 952333531313518012F1
usb 2-1: configuration #1 chosen from 1 choice
cdc_acm 2-1:1.0: ttyACM1: USB ACM device

A linha

cdc_acm 2-1:1.0: ttyACM1: USB ACM device

Indica que o dispositivo serial está associado ao arquivo /dev/ttyACM1 e portanto o comando seria open /dev/ttyACM1

Mas ao abrir um canal de comunicação com o comando open é preciso especificar o modo de acesso, ou seja, se o arquivo será aberto apenas para leitura r, escrita w ou leitura e escrita r+.

E portanto o comando fica:

open /dev/ttyACM1 r+

10.2.1.1. O comando set

O canal aberto pelo comando open recebe um nome gerado "aleatoriamente" pela Tcl. Para facilitar a manipulação deste canal, associamos o nome do canal aberto a uma variável de fácil identificação com o comando set:

set [nome_do_canal] [ open nome_do_arquivo modo_de_acesso]

set porta [ open /dev/ttyACM1 r+ ]

E para se comunicar corretamente com o dispositivo é necessário configurar a porta serial com os mesmos parâmetros de comunicação serial usados pelo Arduino, ou seja:

9600 bps, 8 bits de dados, sem paridade e 1 bit de parada.

Para fazer isso usamos o comando fconfigure.

set porta [ open /dev/ttyACM1 r+ ]
fconfigure $porta -mode 9600,n,8,1

Nota

É possível usar velocidades maiores do que 9600 bps.

10.2.1.2. O comando puts

Para escrever dados em arquivos usamos o comando puts.

Por exemplo, o comando:

puts $porta "SET;d10;1"

envia a string SET;d10;1 pela porta USB na qual está conectado o Arduino.

O comando SET;d10;1 é executado pelo Arduino e define o valor 1 (HIGH) para o pino 10, no qual está ligado o gate do MOSFET.(Ver figura).

10.2.2. Criando um script executável

Para rodar o programa podemos digitar linha por linha em um interpretador interativo ou, usando um editor de texto puro, criar um arquivo com a extensão tcl para executar todos os comandos do programa quando for chamado:

set porta [ open /dev/ttyACM1 r+ ]
fconfigure $porta -mode 9600,n,8,1
puts $porta "SET;d10;1"

Para isso devemos incluir nas primeiras linhas do arquivo as seguintes linhas:

#!/bin/sh
#A próxima linha reinicia usando o wish \
    exec wish "$0" "$@"

Nota

Existem outras opções de cabeçalho.

E em seguida podemos salvar o arquivo com o nome controle_sistema.tcl, para ficar fácil de lembrar.

No Linux é necessário tornar o arquivo executável e para isso podemos usar o comando:

chmod +x controle_sistema.tcl

E o script executável controle_sistema.tcl poderá ser executado a partir da linha de comando como qualquer outro programa.

Conteúdo do programa até o momento:

#!/bin/sh
#A próxima linha reinicia usando o wish \
    exec wish "$0" "$@"

set porta [ open /dev/ttyACM1 r+ ]
fconfigure $porta -mode 9600,n,8,1
puts $porta "SET;d10;1"

10.2.3. Botões para Controle da Bomba

Agora vamos criar uma interface gráfica para o programa controle_sistema.tcl com apenas dois botões para ligar e desligar a bomba.

Figura 66. Interface contendo os botões para ligar e desligar a bomba.

Interface contendo os botões para ligar e desligar a bomba.

Agora vamos usar o Toolkit (Kit de Ferramentas/Utilitários) Tk que é um complemento da Tcl, permitindo criar os Widgets (elementos gráficos) da interface gráfica com o usuário (GUI).

Para usar o Tk é importante entender quatro aspectos básicos:

  1. Interpretador

  2. Hierarquia

  3. Eventos

  4. Posicionamento dos widgets na tela

10.2.3.1. Interpretador

Um script que use apenas códigos Tcl pode ser executado com um interpretador tcl ou tclsh, dependendo da distribuição. Para usar Tk usamos outro interpretador, chamado wish. Por simplicidade já incluímos a chamada ao interpretador wish na primeira linha do nosso script. Se lembra?

10.2.3.2. Hierarquia

Em Tk existe uma ordem para criar os objetos gráficos. O primeiro objeto gráfico é representado pelo caractere ponto ..

Esse primeiro objeto gráfico é a sua janela principal, ou janela pai, dentro da qual são colocados todos ou outros objetos filhos.

O comando seguinte cria um elemento gráfico do tipo button (botão) com o nome .ligar:

button .ligar -text "Ligar" -command ligar

Nota

Repare no . antes do nome .ligar.

Em seguida é definido o texto Ligar que vai ser exibido no botão usando a opção -text.

10.2.3.3. Eventos

São ações que devem ser executadas quando o usuário clicar no botão, pressionar uma tecla ou interagir com outros elementos gráficos.

Nota

As ações também podem ser desencadeadas por eventos externos, independente do usuário.

Com a opção -command definimos que o procedimento chamado ligar deverá ser executado quando o usuário clicar no botão.

button .ligar -text "Ligar" -command ligar

10.2.3.4. Posicionamento dos widgets na tela

Em Tk, existem três gerenciadores de posição: pack, grid e place. Cada qual posicionando o widget de forma diferenciada.

Vamos usar o gerenciador pack para exibir o botão .ligar com o comando:

pack .ligar

10.2.3.5. Criando os botões para ligar e desligar a bomba
button .ligar -text "Ligar" -command ligar
button .desligar -text "Desligar" -command desligar
pack .ligar .desligar -side left

Com a inclusão dos comandos para a criação dos botões o programa, por enquanto, está assim:

#!/bin/sh
#A próxima linha reinicia usando o wish \
    exec wish "$0" "$@"

set porta [ open /dev/ttyACM1 r+ ]
fconfigure $porta -mode 9600,n,8,1
puts $porta "SET;d10;1"

button .ligar -text "Ligar" -command ligar
button .desligar -text "Desligar" -command desligar
pack .ligar .desligar -side left
10.2.3.6. Procedimentos ligar e desligar

Agora vamos criar os procedimentos ligar e desligar que serão executados quando os respectivos botões forem clicados.

O comando proc registra um procedimento (também chamada de função ou sub-rotina) por nome e permite que ele seja executado como qualquer outro comando em Tcl. A sintaxe é:

proc nome_do_procedimento {argumentos} {
instruções
}

Procedimento ligar:

proc ligar {} {
    
    global porta
    
    puts $porta "SET;d10;1"

    flush $porta
}

Procediment desligar:

proc desligar {} {
    
    global porta
    
    puts $porta "SET;d10;0"

    flush $porta    
}

A variável porta foi definida como uma variável global mas antes vamos entender o conceito de Escopo de Variáveis.

10.2.3.6.1. Escopo de Variáveis

Em linguagens de programação, escopo se refere ao alcance que uma variável possui, ou seja, de onde a variável pode ser acessada, trata da visibilidade de uma variável.

O escopo pode ser Global, Local ou definido entro de um Namespace.

Uma variável definida com o escopo global pode ser lida e modificada em qualquer parte do programa.

Uma variável com o escopo local está confinada em um bloco de código (procedimento/função/sub-rotina) e só pode ser manipulada dentro do bloco de código.

Conforme o diagrama seguinte a variável A é uma variável Global pois pode ser acessada em qualquer parte do programa enquanto que as variáveis B e C são variáveis Locais e podem ser manipuladas apenas dentro dos respectivos procedimentos (1 e 2).

Figura 67. Diagrama esquemático da visibilidade da variável global (A) e das variáveis locais (B e C).

Diagrama esquemático da visibilidade da variável global (A) e das variáveis locais (B e C).

A variável porta foi criada no código principal do nosso programa e por isso é uma variável global. E para ser acessada pelos procedimentos ligar e desligar precisa ser declarada como global dentro do procedimento (global porta).

10.2.3.6.2. Enviando o Comando

O comando:

puts $porta "SET;d10;1"

envia a string SET;d10;1 pela porta USB na qual está conectado o Arduino.

E finalmente o comando flush $porta envia todos os dados que estiverem no buffer de saída.

Dica

Mas podemos dispensar o comando flush e acrescentar no código principal, logo após comando de abertura da porta serial, o comando:

fconfigure $porta -buffering none

Como está o programa até o momento?

#!/bin/sh
#A próxima linha reinicia usando o wish \
    exec wish "$0" "$@"

set porta [ open /dev/ttyACM0 r+ ]
fconfigure $porta -mode 9600,n,8,1
fconfigure $porta -buffering none

button .ligar -text "Ligar" -command ligar
button .desligar -text "Desligar" -command desligar
pack .ligar .desligar -side left

proc ligar {} {
        global porta
        puts $porta "SET;d10;1"
     }

proc desligar {} {
        global porta
        puts $porta "SET;d10;0"
     }

10.2.4. Botões para Controle da Válvula

Vamos fazer algumas modificações no programa e incluir botões para o controle da válvula solenóide:

button .ligar_bomba -text "Ligar Bomba" -command ligar_bomba
button .desligar_bomba -text "Desligar Bomba" -command desligar_bomba
button .ligar_valvula -text "Ligar Vávula" -command ligar_valvula
button .desligar_valvula -text "Desligar Válvula" -command desligar_valvula

pack .ligar_bomba .desligar_bomba .ligar_valvula .desligar_valvula -side left

E nos procedimentos para ligar/desligar a bomba e a válvula.

proc ligar_bomba {} {
     global porta
     puts $porta "SET;d10;67"
 }

proc desligar_bomba {} {
     global porta
     puts $porta "SET;d10;0"
 }

proc ligar_valvula {} {
global porta
puts $porta "SET;d9;1"
}

proc desligar_valvula {} {
global porta
puts $porta "SET;d9;0"
}

Atenção

Qual a diferença entre os procedimentos para a bomba e a válvula?

Como está o programa até o momento?

#!/bin/sh
#A próxima linha reinicia usando o wish \
    exec wish "$0" "$@"

set porta [ open /dev/ttyACM0 r+ ]
fconfigure $porta -mode 9600,n,8,1
fconfigure $porta -buffering none

button .ligar_bomba -text "Ligar Bomba" -command ligar_bomba
button .desligar_bomba -text "Desligar Bomba" -command desligar_bomba
button .ligar_valvula -text "Ligar Vávula" -command ligar_valvula
button .desligar_valvula -text "Desligar Válvula" -command desligar_valvula

pack .ligar_bomba .desligar_bomba .ligar_valvula .desligar_valvula -side left

proc ligar_bomba {} {
     global porta
     puts $porta "SET;d10;1"
}

proc desligar_bomba {} {
     global porta
     puts $porta "SET;d10;0"
}

proc ligar_valvula {} {
     global porta
     puts $porta "SET;d9;1"
}

proc desligar_valvula {} {
     global porta
     puts $porta "SET;d9;0"
}

E como está a interface gráfica?

Figura 68. Interface gráfica contendo os botões para ligar/desligar a bomba e a válvula.

Interface gráfica contendo os botões para ligar/desligar a bomba e a válvula.

10.2.4.1. Botão para Fechar o Programa

Programa após incluir comandos para criar o botão de fechamento do programa.

#!/bin/sh
#A próxima linha reinicia usando o wish \
     exec wish "$0" "$@"

set porta [ open /dev/ttyACM0 r+ ]
fconfigure $porta -mode 9600,n,8,1
fconfigure $porta -buffering none

button .ligar_bomba -text "Ligar Bomba" -command ligar_bomba
button .desligar_bomba -text "Desligar Bomba" -command desligar_bomba
button .ligar_valvula -text "Ligar Vávula" -command ligar_valvula
button .desligar_valvula -text "Desligar Válvula" -command desligar_valvula
button .fechar -text "Fechar Programa" -command fechar_programa

pack .ligar_bomba .desligar_bomba .ligar_valvula .desligar_valvula .fechar -side left

proc ligar_bomba {} {
    global porta   
    puts $porta "SET;d10;1"
}

proc desligar_bomba {} {
    global porta
    puts $porta "SET;d10;0"
}

proc ligar_valvula {} {
    global porta
    puts $porta "SET;d9;1"
}

proc desligar_valvula {} {
    global porta
    puts $porta "SET;d9;0"
}

proc fechar_programa {} {
    global porta
    close $porta
    exit
}

Figura 69. Interface gráfica contendo os botões para Ligar, Desligar e Fechar o programa.

Interface gráfica contendo os botões para Ligar, Desligar e Fechar o programa.

10.2.5. Exibindo as Medidas de Vazão

As linhas seguintes criam o widget do tipo text chamado .tela.dados, do tipo texto dentro do frame .tela onde serão exibidos os resultados das leituras do pino A0.

frame .tela
pack .tela
text .tela.dados -relief sunken -yscrollcommand {.rolagem set} -height 15 -width 110
pack .tela.dados -side left -fill y 

A opção relief define o tipo de relevo do widget. Os valores possíveis são: flat, groove, raised, ridgen e sunken.

A opção yscrollcommand define qual o nome da barra de rolagem (scrollbar) vertical que será usada para rolar a tela do texto no sentido vertical. A opção height apenas define a altura da janela de texto.

Em seguida incluímos os comandos para criar um scrollbar (barra de rolagem) dentro do frame .tela chamada .tela.rolagem que permitirá visualizar (rolar) todo o texto.

scrollbar .tela.rolagem -command {.tela yview}
pack .tela.rolagem -expand yes -fill both

Salvar os novos comandos e chamar o programa de monitor_arduino_03.tcl.

Figura 70. Tela do programa monitor_arduino_03.tcl incluindo os botões de controle e o texto para exibição das leituras do sensor.

Tela do programa monitor_arduino_03.tcl incluindo os botões de controle e o texto para exibição das leituras do sensor.

10.2.5.1. Entendendo a relação entre text e scrollbar

O comando set dentro da opção "yscrollcommand" serve para passar para a barra de rolagem a área visível atual do widget que está associado a ela.

text .meu_texto -height 20 -yscrollcommand { .minha_barra_de_rolagem set }
scrollbar .minha_barra_de_rolagem -command { .meu_texto yview }
pack .meu_text -side left
pack .minha_barra_de_rolagem -fill both -expand yes

A opção command do widget scrollbar define qual widget ele deve rolar e em qual sentido, yview (vertical) ou xview (horizontal).

10.2.6. Botões de Leitura

Incluindo os botões para iniciar e parar as leituras de vazão.

button .iniciar_leitura -text "Iniciar Leitura" -command iniciar_leitura
button .parar_leitura -text "Parar Leitura" -command parar_leitura

pack .ligar_bomba .desligar_bomba .ligar_valvula .desligar_valvula .iniciar_leitura .parar_leitura .fechar -side left

E os eventos iniciar_leitura e parar_leitura:

proc iniciar_leitura {} {

global porta

fileevent $porta readable [list ler_porta $porta]

}

proc parar_leitura {} {

global porta

fileevent $porta readable ""

}
10.2.6.1. O comando fileevent

Um canal na Tcl, por padrão, vai ficar bloqueado se você tentar ler mais dados do que existem no canal, e gerar uma mensagem de erro se você tentar ler ou escrever dados em um canal que já tenha sido fechado.

Para evitar isso, existe o comando fileevent que executa um comando (definido por você) quando um canal puder ser lido ou escrito. Ele recebe o nome de um comando (definido por você) e um parâmetro que informa em qual situação o comando deve ser executado: se quando o canal puder ser lido (readable) ou escrito (writable).

Estrutura geral do comando fileevent para executar o meu_procedimento quando meu_canal puder ser lido:

fileevent $meu_canal readable [list meu_procedimento]

Por exemplo, no procedimento iniciar_leitura a linha:

fileevent $porta readable [list ler_porta $porta]

significa que o procedimento ler_porta deve ser executado sempre que o canal porta estiver legível.

Agora precisamos criar o procedimento ler_porta.

10.2.6.2. Procedimento ler_porta

O procedimento ler_porta é chamado sempre que chegar algum dado pelo porta serial, e recebe como argumento a variável que armazena o identificar do canal para a porta serial (porta).

Dentro da sub-rotina ler_porta implementamos os comandos necessários para extrair a informação desejada.

proc ler_porta { canal } {       
   if { [eof $canal ] } {            <------ Testa se já chegou no fim

      puts stderr "Fechando $canal" <------ Se "é o fim", envia mensagem

      catch { close $canal }    <------ Fecha o canal

      return                           <----- Sai do procedimento
    }

    set dados [gets $canal]     <----- Mas se ainda não "é o fim",
                                       então lê uma linha do canal e 
                                       armazena na variável "dados".

    .tela.dados insert end "$dados\n"   <----- Insere a leitura na janelade texto

    .tela.dados see end      <---- Rola a janela e posiciona o cursor na
                                   última linha                                         
}

O conteúdo da variável dados é exibido na janela do tipo text chamada .tela.dados com o comando:

.tela.dados insert end "$dados\n"

No entanto vamos lembrar que o programa que foi gravado no Arduino só envia informações após receber um comando de solitação. (Ex: GET;A0 - retorna a leitura do pino analógico "A0")

Por isso vamos inclur uma rotina, que chamaremos de ler_peso e será responsável pelo envio de comandos para o Arduino solicitando o valor das leituras de tensão do pino analógico conectado ao circuito do SRF.

10.2.6.3. Procedimento ler_peso

O procedimento ler_peso envia o comando get;a0 para a placa arduino e, se avariável lendo for diferente de 0, usa o comando after para programar uma nova chamada da sub-rotina ler_peso 500 milissegundos mais tarde.

proc ler_peso { canal } {
    
    global lendo
    
    puts $canal "get;a0"
    
    if {$lendo} {
	    
	    after 500 [list ler_peso $canal]
	    
	}
	
}

A única finalidade da rotina ler_peso é solicitar à placa Arduino o envio de dados que serão processados pela rotina ler_porta.

10.2.6.3.1. Comando after

O comando after possui a seguinte sintaxe:

after tempo [comando1 comando2]

Ele é usado para atrasar a execução do programa durante o intervalo definido por tempo (em milissegundos) ou para executar uma lista de comandos ([comando1 comando2]) após o intervalo definido por tempo.

10.2.6.3.2. Modificações em iniciar_leitura e parar_leitura

Com a criação do procedimento (sub-rotina) ler_peso vamos incluir nas rotinas iniciar_leitura e parar_leitura alguns comandos para iniciar e encerrar o envio de dados respectivamente:

proc iniciar_leitura {} {

global porta lendo 

fileevent $porta readable [list ler_porta $porta]

set lendo 1

ler_peso $porta

}

proc parar_leitura {} {

global porta lendo

fileevent $porta readable ""

set lendo 0

}

10.2.7. Extraindo Informações

O código implementado no Arduino retorna uma string do tipo:

pin:a0;readout:611

Para extrair o valor numérico podemos usar o comando split que divide uma string em uma lista.

Nota

Isso também poderia ser feito com o uso de Expressões Regulares.

Rotina ler_porta com os comandos, e comentários, para extrair as leituras analógicas:

proc ler_porta { canal } {
    if { [eof $canal ] } {
	puts stderr "Fechando $canal"
	catch { close $canal }
	return }
    set dados [gets $canal]

#Remove alguns caracteres não imprimíveis enviados pelo Arduino
if {$dados == ""} { return }
 
#Divide a string "dados" (Ex: pin:a0;readout:611)
#usando o caracter ";" como separador de campos 
#e retorna a lista com 2 elementos {pin:a0 readout:611}
    set lista [split $dados ";"]

#Retorna o segundo elemento da lista (readout:611)
    set leitura [lindex $lista 1]

#Divide novamente a string "leitura" (readout:611)
#em uma lista {readout 611}
    set lista [split $leitura ":"]

#Retorna apenas o segundo elemento da lista (611)
    set numero [lindex $lista 1]

#E exibe na janela de texto
    .tela.dados insert end "$numero\n"

#Rola a janela de texto para exibir a última linha
    .tela.dados see end
}

Dica

Simplificando as linhas com o comando split

set leitura [lindex [split $dados ";"] 1]

set numero [lindex [split $leitura ":"] 1]

10.2.8. Incluindo o Tempo

Agora imagine que você queira registrar também o intervalo de tempo de cada leitura para poder calcular a vazão e produzir gráficos Peso X Tempo.

Neste caso vamos criar uma variável que vai armazenar o instante inicial da aquisição e os intervalos de tempo de cada leitura.

Dentro do procedimento iniciar_leitura vamos declarar a variável global t0 e inicializar esta variável com o tempo do sistema em milisegundos:

set t0 [clock clicks -milliseconds]

E no procedimento ler_porta vamos declarar a variável global t0, obter o tempo atual em milisegundos e armazenar o resultado na variável tn com o comando:

set tn [clock clicks -milliseconds]

Com o tempo inicial e final calculamos o intervalo de tempo (segundos) decorrido desde o início das leituras com o comando:

set tn [expr ($tn - $t0)/1000.0]

E inserimos na janela de texto os intervalos de tempo e as leituras do sensor:

.tela.dados insert end "$tn $numero\n"

Podemos também formatar as leituras de tempo definindo apenas 1 casa decimal com o comando:

set tn [format "%.1f" $tn]

Ou com 2 casas decimais:

set tn [format "%.2f" $tn]

Como ficou a rotina ler_porta:

proc ler_porta { canal } {
global t0
    if { [eof $canal ] } {
	puts stderr "Fechando $canal"
	catch { close $canal }
	return }
     set dados [gets $canal]
     if {$dados == ""} { return } 
     set tn [clock clicks -milliseconds]
     set tn [expr ($tn - $t0)/1000.0]
     set tn [format "%.1f" $tn]
     set leitura [lindex [split $dados ";"] 1]
     set numero [lindex [split $leitura ":"] 1]
    .tela.dados insert end "$tn $numero\n"
    .tela.dados see end
}

10.3. Calibração

Com os recursos, até então implementados, do programa de controle foi possível fazer os primeiros testes de calibração [7] do sensor SRF adicionando volumes conhecidos de água no reservatório superior e registrando as leituras do Arduino.

O conversor Analógico-Digital (AD) da placa Arduino possui uma resolução [8] de 10 bits, ou seja, consegue converter a escala de 0-5V com uma resolução de:

210 = 1024

resolução = 5 V / 1024 = 0,00488 ou 4,88 mV

Ou seja, o conversor AD do Arduino faz a leitura de tensão e converte em um número de 0 a 1023, e cada unidade de leitura corresponde a 4,88 mV. (0 = 0 V, 1023 = 5V)

O código que gravamos no Arduino está retornando apenas as leituras puras do conversor AD (0-1023) feitas pelo pino A0 conforme o curcuito para o SRF.

Usando apenas esses números, sem fazer a conversão em mV, tentei fazer uma calibração, bem simplificada, do SRF adicionando incrementos de 100 mL no reservatório superior e registrando as leituras do sensor.

Tabela 3. Tabela de Calibração

Volume(mL)Leituras
0 600
0 608
100 686
100 703
200 748
300 775
400 812
400 829
500 838
500 857
600 864
600 874

Atenção

Observei que após a adição de água as leituras demoram muito (alguns minutos) para estabilizar em torno do novo valor. Por isso, em alguns casos, fiz leituras em duplicata.

Esse grande tempo de resposta dificulta o uso desse tipo de sensor para medidas de vazão. Apesar dessa limitação resolvi continuar a usar o SRF para implementar as rotinas necessárias na interface em Tcl/Tk.

Mas ao mesmo tempo, comecei também a trabalhar na idéia de Hackear a célula de carga de uma balança para uso doméstico.

Ao colocar em um gráfico (usando o Calc do OpenOffice) é possível identificar uma relação exponencial.

Figura 71. Gráfico de Volume (mL) em função das leituras do SRF.

Gráfico de Volume (mL) em função das leituras do SRF.

Mas conforme o Datasheet deste sensor a relação de resistência em função da forçao é melhor representada por um gráfico log-log.

Tabela 4. Tabela de Calibração com os logaritmos dos Volumes e Leituras

Volume(mL)log(Volume)Leituralog(Leitura)
0 - 600 -
0 - 608 -
200 2,3010 748 2,8739
300 2,4771 775 2,8893
400 2,6021 812 2,9096
400 2,6020 829 2,9185
500 2,6990 838 2,9232
500 2,6990 857 2,9330
600 2,7781 864 2,9365
600 2,7781 874 2,9415

Figura 72. Gráfico de log(Volume) em função de log(Leitura) do SRF.

Gráfico de log(Volume) em função de log(Leitura) do SRF.

10.3.1. Cálculo da Vazão

Alguns instantes depois fiz outra calibração, mas dessa vez usando apenas as leituras iniciais sem esperar a estabilização do SRF, e obtive outra curva de calibração (log(volume) = 9,90 * log10(leitura) - 26,39).

Essa equação foi usada no procedimento convert_leitura_volume para o cálculo do volume a partir das leituras analógicas.

#Procedimento para converter a leitura analógica em
#em volume usando a equação de calibração
#log(volume) = 9,901 * log10(leitura) - 26,385 
#volume = 10^(9,901 * log10(leitura) - 26,385)
#[log base 10 log10 (float)]
#Ex: set two [expr log10(100)]
#Potenciação: [pow (float, float)]]
#Ex: set eight [expr pow(2, 3)]

proc convert_leitura_volume { leitura } {

set log_volume [expr 9.901 * [expr log10($leitura)] - 26.385]

set volume [expr pow(10, $log_volume)]

return $volume
}

Esse procedimento foi usado para o cálculo do volume dentro da rotina ler_porta com o comando:

set volume [convert_leitura_volume $numero]

E finalmente o cálculo da vazão é feito pela rotina calc_vazao:

#Calcula vazao como ml/s

proc calc_vazao { ult_volume volume } {

global int_leitura

set vazao [expr ($volume - $ult_volume)/($int_leitura/1000)]

return [format "%.1f" $vazao]

}

O procedimento calc_vazao também é chamado por ler_porta dentro do teste if:

...

if {[info exists ult_volume]} {
		    
		    set vazao [calc_vazao $ult_volume $volume]
		    
		    .tela.dados insert end "$tn $numero $volume $vazao\n"

		    .tela.dados see end

		} else {
		    
		    .tela.dados insert end "T(s) L V(mL) Q(mL/s)\n"
		    
		    .tela.dados insert end "$tn $numero $volume\n"
		    
		    .tela.dados see end

		}
		
		set ult_volume $volume
		
}

E para facilitar as medidas de vazão escrevi o procedimento medir_vazao:

proc medir_vazao {} {

ligar_bomba

set espera 1

after 30000 [list set espera 0]

vwait espera

desligar_bomba

}

E finalmente para testar a capacidade do SRF para medidas de vazão configurei a bomba para operar com baixa vazão (puts $porta "SET;d10;60") no modo PWM.

Nota

Deixei a bomba e válvula ligadas durante 1 hora para avaliar o eventual aquecimento da bomba, válvula e componentes do circuito, mas não observei mudança significativa de temperatura.

Figura 73. Interface do programa de controle exibindo as leituras.

Interface do programa de controle exibindo as leituras.

Leituras com as medidas de vazão.


Início medida de vazão
T(s) L V(mL) Q(mL/s)
124.1 815 274.4 1.6
128.1 817 281.1 1.7
132.1 819 288.0 1.7
136.1 820 291.5 0.9
140.1 823 302.3 2.7
144.1 826 313.3 2.8
148.1 828 320.9 1.9
152.1 831 332.6 2.9
156.1 833 340.6 2.0
160.1 836 353.0 3.1
164.1 838 361.4 2.1
168.1 839 365.7 1.1
172.1 841 374.5 2.2
176.1 843 383.4 2.2
180.1 845 392.5 2.3
184.1 847 401.8 2.3
188.1 849 411.3 2.4
192.1 850 416.1 1.2
196.1 852 425.9 2.4
200.1 852 425.9 0.0
204.1 854 435.9 2.5
208.1 856 446.1 2.6
212.1 857 451.3 1.3
216.1 858 456.5 1.3
220.1 859 461.8 1.3

Pode-se observar que as medidas de vazão oscilam muito. E portanto cheguei à conclusão que este tipo de transdutor não é adequado para medidas de vazão gravimétrica.

A partir de agora vou avaliar a possibilidade de hackear uma balança digital para uso doméstico e aproveitar a célula de carga para medidas de massa, e vazão, com maior precisão.

Essas atividades estão documentadas na seção Hackeando uma Balança Digital.

10.3.2. Versão provisória do programa de controle.

#!/bin/sh
#A próxima linha reinicia usando o wish \
    exec wish "$0" "$@"

set porta [ open /dev/ttyACM0 r+ ]
fconfigure $porta -mode 9600,n,8,1
fconfigure $porta -buffering none -blocking 0

#Intervalo de leituras
set int_leitura 4000

button .ligar_bomba -text "Ligar Bomba" -command ligar_bomba
button .desligar_bomba -text "Desligar Bomba" -command desligar_bomba
button .ligar_valvula -text "Ligar Vávula" -command ligar_valvula
button .desligar_valvula -text "Desligar Válvula" -command desligar_valvula
button .iniciar_leitura -text "Iniciar Leitura" -command iniciar_leitura
button .parar_leitura -text "Parar Leitura" -command parar_leitura
button .medir_vazao -text "Medir Vazao" -command medir_vazao

button .fechar -text "Fechar Programa" -command fechar_programa


frame .tela
pack .tela
text .tela.dados -relief sunken -yscrollcommand {.tela.rolagem set} -height 15 -width 125

pack .tela.dados -side left -fill y 

scrollbar .tela.rolagem -command {.tela.dados yview}
pack .tela.rolagem -expand yes -fill both

pack .ligar_bomba .desligar_bomba .ligar_valvula .desligar_valvula .iniciar_leitura .parar_leitura .medir_vazao .fechar -side left

proc ligar_bomba {} {
    
    global porta
    
    #puts $porta "SET;d10;1"
    puts $porta "SET;d10;60"
   
}

proc desligar_bomba {} {
    
    global porta
    
    puts $porta "SET;d10;0"
       
}

proc ligar_valvula {} {

global porta

puts $porta "SET;d9;1"

}

proc desligar_valvula {} {

global porta

puts $porta "SET;d9;0"

}

proc fechar_programa {} {

global porta

close $porta

exit

}

proc iniciar_leitura {} {

global porta lendo t0

set t0 [clock clicks -milliseconds]

fileevent $porta readable [list ler_porta $porta]

set lendo 1

ler_peso $porta

}

proc parar_leitura {} {

global porta lendo

fileevent $porta readable ""

set lendo 0

}

proc ler_porta { canal } {
    
    global t0 ult_volume
    
    if { [eof $canal ] } {
	    puts stderr "Fechando $canal"
	    catch { close $canal }
	    return }
	
	set dados [gets $canal]
	
	if {$dados == ""} { return } 
	    
	    set tn [clock clicks -milliseconds]
	    
	    set tn [expr ($tn - $t0)/1000.0]
	    
	    set tn [format "%.1f" $tn]
	    
	    set lista [split $dados ";"]
	    puts "lista -> $lista"
	    set leitura [lindex $lista 1]
	    puts "leitura -> $leitura"
	    set lista [split $leitura ":"]
	    puts "lista -> $lista"
	    set numero [lindex $lista 1]
	    puts "numero -> $numero"
#    set leitura [lindex [split $dados ";"] 1]
#    set numero [lindex [split $leitura ":"] 1]
	    
	    set volume [convert_leitura_volume $numero]
	    
	    set volume  [format "%.1f" $volume]
	    
	    if {[info exists ult_volume]} {
		    
		    set vazao [calc_vazao $ult_volume $volume]
		    
		    .tela.dados insert end "$tn $numero $volume $vazao\n"

		    .tela.dados see end

		} else {
		    
		    .tela.dados insert end "T(s) L V(mL) Q(mL/s)\n"
		    
		    .tela.dados insert end "$tn $numero $volume\n"
		    
		    .tela.dados see end

		}
		
		set ult_volume $volume
		
}

proc ler_peso { canal } {
    
    global lendo int_leitura
    
    puts $canal "get;a0"
    
    if {$lendo} {
	    
	    after $int_leitura [list ler_peso $canal]
	    
	}
	
}

#Calcula vazao como ml/s

proc calc_vazao { ult_volume volume } {

global int_leitura

set vazao [expr ($volume - $ult_volume)/($int_leitura/1000)]

return [format "%.1f" $vazao]

}

#Procedimento para converter a leitura analógica em
#em volume usando a equação de calibração
#log(volume) = 9,901 * log10(leitura) - 26,385 
#volume = 10^(9,901 * log10(leitura) - 26,385)
#log base 10 log10 (float)
#            set two [expr log10(100)]
#power pow (float, float)
#      set eight [expr pow(2, 3)]

proc convert_leitura_volume { leitura } {

set log_volume [expr 9.901 * [expr log10($leitura)] - 26.385]

set volume [expr pow(10, $log_volume)]

return $volume
}

proc medir_vazao {} {

ligar_bomba

set espera 1

after 30000 [list set espera 0]

vwait espera

desligar_bomba

}


[7] Calibração (ou aferição) Conjunto de operações que estabelece, sob condições especificadas, a relação entre os valores indicados por um instrumento de medição ou sistema de medição ou valores representados por uma medida materializada ou um material de referência, e os valores correspondentes das grandezas estabelecidas por padrões.(Fonte: glossario-de-metrologia.pdf)

[8] Para entender o conceito de resolução na conversão AD consulte o tutorial http://emc5710.lago.prof.ufsc.br/arquivos/5conversao AD 122.pdf