22. Programa para Controle do MultiFotômetro com LEDs usando Máquina de Estados Orientada a Objeto

Resolvi reescrever o código do programa de controle do MultiFotômetro modificando a estratégia para implementar uma Máquina de Estados Hierárquicos.

Na seção anterior eu deixei registrada a estratégia na qual usei duas variáveis do tipo dicionário para representar a estrutura dos estados e rotinas recursivas para realizar as transições.

Dessa forma os estados eram representados como atributos dos diferentes intrumentos.

Mas em certo momento, percebi que essa estratégia permitiria que os diferentes instrumentos estivessem em diferentes estados. Isso se mostrou incompatível na prática pois os diferentes instrumentos são apenas diferentes modos de operação de um único instrumento físico.

Depois de quebrar a cabeça resolvi mudar de estratégia e implementar a estrutura dos estados hierárquicos como diferentes classes

22.1. Diagrama de Estados Hierárquicos para o MultiFotômetro

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

Figura 273. Diagrama de Estados Hierárquicos para o Fotômetro

Diagrama de Estados Hierárquicos para o Fotômetro

Os métodos das diferentes classes correspondem aos eventos compatíveis com os diferentes estados.

Nessa abordagem de Máquina de Estados Orientada a Objeto é criada inicialmente uma instância da classe stateUNCONNECTED, correspondente ao estado inicial (Ex: UNCONNECTED).

E os objetos correspondentes aos diferentes modos de operação do fotômetro são incorporados ao objeto da classe stateUNCONNECTED.

Essa classe contém o método connect que, ao ser chamado, executa o comando que modifica dinâmicamente a classe à qual pertence o objeto, da classe stateUNCONNECTED para a classe stateCONNECTING.

Essa alteração é feita com o comando oo::objdefine [self] class stateCONNECTING dentro do método connect:

oo::class create stateUNCONNECTED {
    ...
    method connect {} {

		oo::objdefine [self] class stateCONNECTING
		
    }
    ...

O comando oo::objdefine é um recurso muito versátil da biblioteca TclOO que permite modificar dinâmicamente a classe à qual pertence um objeto.

O construtor não é chamado quando um objeto muda de classe, mas todos os métodos e variáveis antigos são substituídos pelos métodos e variáveis da nova classe.

Com essa estratégia procuramos mais consistência ao reproduzir na estrutura das classes a realidade física do instrumento fotômetro e os seus diferentes modos de operação.

Ao ser iniciado o programa, o usuário poderia verificar a conexão com a placa Arduino enviando um comando solicitando uma mensagem para confirmar a conexão (Ex: ACK). E em caso afirmativo entraria no estado CONNECTED e automaticamente no subestado default UNCONFIGURED.

Nota

A necessidade de estar conectado para entrar em outras instâncias do programa impediria o uso do programa, para outras finalidades (Ex: análise de dados), no modo DISCONNECTED.

Isso pode ser modificado futuramente incluindo outro estado (Ex: DATA_ANALYSIS) a partir de DISCONNECTED, mas independente do estado CONNECTED.

Entrando no estado CONNECTED, o usuário poderia selecionar um arquivo de configuração com informações sobre os LEDs Emissores e Sensores instalados fisicamente.

Essas informações seriam exibidas para o usuário permitino selecionar os LEDs a serem usados nas leituras de Absorbância, Fluorescência e/ou Turbidez, através dos respectivos eventos: start_configuration_abs, start_configuration_fluor e/ou start_configuration_turb. com consequente carregamento dos módulos (drivers) correspondentes.

Em cada um dos estados CONFIGURED (ABS, FLUOR ou TURB) o estado default é IDLE, ou seja, o instrumento está diponível para iniciar leituras contínuas (READING) sem uma equação de calibração ao receber o evento start_reading. Nesse estado as leituras retornam apenas os valores no intervalo de 0-1023 do conversor AD do Arduino.

Mas com o evento start_calibration o instrumento entra no estado CALIBRATING (Figura 274) no qual pode ser carregada uma equação de calibração já armazenada ou entrar em subestados de uma rotina de calibração.

Figura 274. Diagrama de subestados do estado CONFIGURED (ABS, FLUOR e/ou TURB)

Diagrama de subestados do estado CONFIGURED (ABS, FLUOR e/ou TURB)

Para implementar a estrutura da Máquina de Estados Hierárquicos escolhemos a estratégia de definir os diferentes estados como diferentes classes reproduzindo a mesma estrutura hierárquica do modelo. E os eventos para a transição entre os estados foram implementados como os métodos das respectivas classes.

A seguir o código das classes estado:

###############################################################
#UNCONNECTED class
###############################################################

oo::class create stateUNCONNECTED {

    variable state_attributes
    
    constructor { name } {

	dict set state_attributes name_object $name
    }

    method startConnection {} {

	#Method exitAction is defined in the current state
	[self] exitAction
	oo::objdefine [self] class stateCONNECTING
	#Method entryAction is defined in the next state
	[self] entryAction
	
    }

    method exitAction {} {

	puts "Leaving state current [info object class [self]]"

    }

    method getName {} {

	return [dict get $state_attributes name_object]
    }
    
}

###############################################################
#CONNECTING class
###############################################################

oo::class create stateCONNECTING {

    variable state_attributes

    method connectionAccepted {} {

	oo::objdefine [self] class stateCONNECTED

	oo::objdefine [self] class stateUNCONFIGURED
    }

    method errorConnection {} {

	oo::objdefine [self] class stateUNCONNECTED

    }

    method entryAction {} {

	puts "Entering state current [info object class [self object]]"

    }

    method getName {} {

	return [dict get $state_attributes name_object]
    }
   
}

###############################################################
#CONNECTED class
###############################################################

oo::class create stateCONNECTED {

    variable state_attributes
    
    constructor { name } {

    }

    method startDisconnection {} {

	oo::objdefine [self] class stateDISCONNECTING
    }

    method getName {} {

	return [dict get $state_attributes name_object]
    }

}

###############################################################
#DISCONNECTING class
###############################################################

oo::class create stateDISCONNECTING {

    variable state_attributes
    
    method endDisconnection {} {

	oo::objdefine [self] class stateUNCONNECTED
    }

    method getName {} {

	return [dict get $state_attributes name_object]
    }
}

###############################################################
#UNCONFIGURED class
###############################################################

oo::class create stateUNCONFIGURED {

    superclass stateCONNECTED

    variable state_attributes

    constructor { name } {

    }

    method startConfiguration { par } {

	oo::objdefine [self] class stateCONFIGURING_$par

    }

    

        
}


###############################################################
#CONFIGRING for ABS class
###############################################################

oo::class create stateCONFIGURING_ABS {

    superclass stateCONNECTED

    method endConfiguration {} {

	oo::objdefine [self] class stateCONFIGURED_ABS

	oo::objdefine [self] class stateIDLE_ABS
    }

    method stopConfiguration {} {

	oo::objdefine [self] class stateUNCONFIGURED

    }

}

    
###############################################################
#CONFIGURED for ABS class
###############################################################

oo::class create stateCONFIGURED_ABS {

    superclass stateCONNECTED

    variable state_attributes

    constructor { name } {

    }

    method startUnconfiguration {} {

	oo::objdefine [self] class stateUNCONFIGURING_ABS
    }

}



###############################################################
#UNCONFIGURING ABS class
###############################################################

oo::class create stateUNCONFIGURING_ABS {

    superclass stateCONNECTED
    
    method endUnconfiguration {} {

	oo::objdefine [self] class stateUNCONFIGURED

    }

}

###############################################################
#IDLE ABS class
###############################################################

oo::class create stateIDLE_ABS {

    superclass stateCONFIGURED_ABS
    
    method startReading {} {

	oo::objdefine [self] class stateREADING_ABS

    }

    method startCalibration {} {

	oo::objdefine [self] class stateCALIBRATING_ABS

    }

}


###############################################################
#READING ABS class
###############################################################

oo::class create stateREADING_ABS {

    superclass stateCONFIGURED_ABS
    
    method stopReading {} {

	oo::objdefine [self] class stateIDLE_ABS

    }

}

###############################################################
#CALIBRATING ABS class
###############################################################

oo::class create stateCALIBRATING_ABS {

    superclass stateCONFIGURED_ABS
    
    method stopCalibration {} {

	oo::objdefine [self] class stateIDLE_ABS

    }

    method endCalibration {} {

	oo::objdefine [self] class stateCALIBRATED_ABS

	oo::objdefine [self] class stateIDLE_CALIBRATED_ABS

    }

}


###############################################################
#CALIBRATED ABS class
###############################################################

oo::class create stateCALIBRATED_ABS {

    superclass stateCONFIGURED_ABS
    
}

###############################################################
#IDLE CALIBRATED ABS class
###############################################################

oo::class create stateIDLE_CALIBRATED_ABS {

    superclass stateCALIBRATED_ABS

    method startReading {} {

	oo::objdefine [self] class stateREADING_CALIBRATED_ABS

    }

    #Event for REcalibration
    
    method startCalibration {} {

	oo::objdefine [self] class stateCALIBRATING_ABS

    }

}

###############################################################
#READING CALIBRATED ABS class
###############################################################

oo::class create stateREADING_CALIBRATED_ABS {

    superclass stateCALIBRATED_ABS

    method stopReading {} {

	oo::objdefine [self] class stateIDLE_CALIBRATED_ABS

    }    

}

Nota

Documentamos apenas as classes para o modo Absorbância (ABS) pois os outros modos são equivalentes e mutuamente exclusivos. Ou seja, alguns LEDs podem ser usados por mais de um tipo de medição e por isso as medições não podem ser feitas simultâneamente, mas de forma sequencial.

22.2. Classes Photometer e Led

Após a definição da estrutura básica da máquina de estados hierárquicos passamos a implementar as unidades lógicas para o funcionamento da máquina utilizando o paradigma da Programação Orientada a Objetos (POO).

Dentro do paradigma da Programação Orientada a Objetos (POO) um programa pode ser modelado (estruturado) com o uso de entidades chamadas classes. Essa abordagem visa facilitar a organização e a manutenção do código.

Uma classe permite a criação de uma coleção de objetos semelhantes que possuem características que definem a identidade de cada objeto (atributos), e as próprias operações que são capazes de realizar (métodos).

Os objetos podem representar objetos físicos reais (Ex: instrumentos, equipamentos etc), mas também podem representar entidades abstratas ou virtuais (Ex: equação de calibração, medida, estados de uma máquina de estados etc).

Por isso vamos criar as classes instPhotometer e led, para dar início ao desenvolvimento do código.

Os objetos da classe Photometer vão representar o fotômetro nos seus diferentes modos de operação: absorbância, fluorescência e turbidez.

Podemos incluir como atributos um identificador (id), o nome do instrumento (name), os LEDs (leds) instalados, a Propriedade Óptica (optical_property) medida (abs, fluor e/ou turb) e as associações de LEDs emissores com LEDs detectores (pair_led) (Figura 275)

Figura 275. Diagrama da classe instPhotometer com os respectivos atributos e métodos.

Diagrama da classe instPhotometer com os respectivos atributos e métodos.

Para os objetos da classe instPhotometer podemos ter um método chamado setLed que receberá como argumento uma instância da classe led com o respectivo método getLeds para retornar todos os LEDs que serão usados pelo fotômetro.

Também podemos ter os métodos setPairLed e getPairLed para definir e resgatar respectivamente os pares de LEDs emissores e detectores.

###############################################################
#instPhotometer class
###############################################################

oo::class create instPhotometer {
    
    variable photo_attributes
    
    constructor { id name optical_property } {

	dict set photo_attributes id $id
	dict set photo_attributes name $name
	dict set photo_attributes optical_property $optical_property

	puts "Constructor of instPhotometer with name $name"
    }

    method getId {} {

	return [dict get $photo_attributes id]

    }

    method getName {} {

	return [dict get $photo_attributes name]

    }

    method getOpticalProperty {} {

	return [dict get $photo_attributes optical_property]

    }

    method setLed { l } {

	puts "Install led $l in instrument [self] with name [[self] getName]"
	dict lappend photo_attributes leds $l

    }

    method getLeds { } {

	
       if { [catch { dict get $photo_attributes leds }] } {
         return {}
       } else {   
         return [dict get $photo_attributes leds]
       }
    }
    
    #The arguments of this method is the instances of class Led
    method setPairLeds { led_emitter led_detector } {

	puts "Method setPairLeds"

	if { [$led_emitter getFunction] == "emitter"  && [$led_detector getFunction] == "detector" } {
	    
	    set id_led_emitter [$led_emitter getId]

	    set id_led_detector [$led_detector getId]
	
	    dict lappend photo_attributes pair_leds "$id_led_emitter $id_led_detector"

	} else {

	    puts "ERROR - $led_emitter or $led_detector is not in correct function"

	}
	
    }

    method getPairLeds {} {
	
	puts "Instrument [[self] getName] use pair_leds :  [dict get $photo_attributes pair_leds]"

	return [dict get $photo_attributes pair_leds]
	
    }
    
			
}

E a classe led para representar os LEDs como mostra a figura 276.

Figura 276. Diagrama da classe led como seus métodos e atributos.

Diagrama da classe led como seus métodos e atributos.

Para identificar cada instância da classe led definimos os seguintes atributos: id, color, function (emissor ou detector) e pinout (número do pino na placa Arduino).

E os respectivos métodos: getId, getColor, getFunction, setPinout e getPinout.

###############################################################
#LED class
###############################################################

oo::class create led {

    variable led_attributes

    constructor { id c f } {

	puts "Constructor of class Led with color $c function $f"
	dict set led_attributes id $id
	dict set led_attributes color $c
	dict set led_attributes function $f
	
    }

    method getColor {} {

	return [dict get $led_attributes color]
  
    }

    method getFunction {} {

	return [dict get $led_attributes function]
	
    }

    method getId {} {

	return [dict get $led_attributes id]

    }

    method setPinout { pin } {

	dict set led_attributes pinout $pin

    }
    
    method getPinout {} {

	return [dict get $led_attributes pinout]

    }

}

Os LEDs emissores estarão ligados aos pinos digitais para ligar ou desligar, e os LEDs sensores estarão ligados aos pinos analógicos.

As instâncias da classe instPhotometer incorporam por agregação os objetos da classe LED necessários.

22.3. Classe controllerStateInstrument

Criamos a classe controllerStateInstrument como atuar como uma interface entre os eventos gerados pelo usuário (e pelo instrumento) e as transições correspondentes e as respectivas consequências.

A idéia básica é gerar as instâncias da classe Led, correspondente a cada LED físico, e em seguida as instâncias da classe Photometer, que correspondem aos diversos modos modos de operação do fotômetro físico.

As instâncias da classe Led são então agregadas às respectivias instâncias da classe Photometer conforme o tipo de medição a ser feita.

É então criada uma instância inicial da classe stateUNCONNECTED, na qual são agregadas todas as instâncias da classe Photometer.

E finalmente é criada uma instância da classe controllerStateInstrument e agregada a instância da classe stateUNCONNECTED, como mostra o diagrama da figura 277.

Figura 277. Diagrama descritivo da sequênia de agregação: Leds > Photometer > statePhotometer > controllStatePhotometer

Diagrama descritivo da sequênia de agregação: Leds > Photometer > statePhotometer > controllStatePhotometer

Os LEDs da figura 277 são apenas exemplos ilustrativos que podem ser combinados com os diferentes instrumentos fotométricos gerando diversas configurações que são armazenadas em arquivos XML para reutilização.

Essa estratégia permite que diferentes instrumentos (potenciômetros, potenciostatos, condutivímetros etc) sejam incorporados a diferentes instâncias da classe stateUNCONNECTED, e todas elas sejam agregadas a uma única instância da classe controllerStateInstrument, garantindo a escalabilidade do código.

22.4. Interface Gráfica

Para a implementação da interface gráfica foram criadas as seguintes janelas.

22.4.1. Seleção do Atividade

A primeira janela (window_00) permite ao usuário escolher a Análise de Amostras ou a Análise de Dados (para implementação futura), como mostra a figura 278

Figura 278. Tela inicial para escolha do tipo de atividade

Tela inicial para escolha do tipo de atividade

###############################################################
#Graphical User Interface
###############################################################

wm title . "Multifotometro"

#To prevent resizing of windows

wm resizable . 0 0

global big_font medium_font 

set big_font {Times 22}

set medium_font {Times 16}

###############################################################
#Window 00
#Select Sample Analysis or Data Analysis
###############################################################

proc window_00 {} {

    global big_font medium_font w_00

    set w_00 [frame .f_w_00]
    
    set message_00 [label $w_00.msg_00 -font $big_font -text "  Por favor, selecione a atividade:  "]

    set activity_00 [radiobutton $w_00.act_00 -text "Análise de Amostras" -font $big_font -relief flat \
			 -variable activity -value sample_analysis]

    set activity_01 [radiobutton $w_00.act_01 -text "Análise de Dados" -font $big_font -relief flat \
			 -variable activity -value data_analysis]

    set button_cont [button $w_00.btn_cont -text "Continuar" -font $medium_font -command window_01_B]

    set button_exit [button $w_00.btn_exit -text "  Sair  " -font $medium_font -command exit]
    
    pack $message_00
    pack $activity_00 -anchor w -pady 10
    pack $activity_01 -anchor w -pady 10
    pack $button_cont $button_exit -side right -expand yes -pady 10
    pack $w_00

}

22.4.2. Seleção do Tipo de Configuração

A janela window_01_B permite ao usuário escolher a criação de uma nova configuração ou o uso de uma configuração pronta, como mostra a figura 279

Figura 279. Tela inicial para a criação de uma nova configuração ou uma configuração pronta

Tela inicial para a criação de uma nova configuração ou uma configuração pronta

###############################################################
#Window 01_B
#To choose make a new configuration
#or a load a configuration file
###############################################################

proc window_01_B {} {
    
    global big_font medium_font w_00 w_01_B activity type_config

    if { $activity == "data_analysis" } { exit }
    
    destroy $w_00
    
    set w_01_B [frame .f_w_01_B]
    
    set message_00 [label $w_01_B.msg_00 -font $big_font -text "  Por favor, você vai fazer uma nova configuração \n \
ou usar uma configuração pronta? "]
    
    set activity_00 [radiobutton $w_01_B.act_00 -text "Fazer nova configuração" -font $big_font -relief flat \
			 -variable type_config -value new_config]
    
    set activity_01 [radiobutton $w_01_B.act_01 -text "Usar configuração pronta" -font $big_font -relief flat \
			 -variable type_config -value old_config]
    
    set button_cont [button $w_01_B.btn_cont -text "Continuar" -font $medium_font -command selectTypeConfig]
    
    set button_exit [button $w_01_B.btn_exit -text "  Sair  " -font $medium_font -command exit]

    pack $message_00
    pack $activity_00 -anchor w -pady 10
    pack $activity_01 -anchor w -pady 10
    pack $button_cont $button_exit -side right -expand yes -pady 10
    pack $w_01_B
    
}

###############################################################
#Select Type of Config
###############################################################

proc selectTypeConfig {} {

    global type_config

    if { $type_config == "new_config" } {

	window_01

    } else {

	window_01_C

    }
}

Se for escolhida a opção de criar uma nova configuração, é carregado o código para a janela window_01, caso contrário é executado o código para a janela window_01_C, figura 280.

Figura 280. Tela para escolha do arquivo de configuração a ser carregado

Tela para escolha do arquivo de configuração a ser carregado

Código para a janela window_01_C:

###############################################################
#Window 01_C
#To select the configuration file
###############################################################

proc window_01_C {} {

    global big_font medium_font w_01_B w_01_C

    destroy $w_01_B
    
    set w_01_C [frame .f_w_01_C]
    
    #Entry and button to select the directory for config file
    

    set labelframe_dir [labelframe $w_01_C.dir -text "Seleção do arquivo de configuração" -font $big_font]
    
    set entry_dir [entry $labelframe_dir.e -width 30 -textvariable config_file_name -font $big_font]
    
    set button_dir [button $labelframe_dir.b -pady 0 -padx 2m -text "Selecione o arquivo:" -font $big_font \
			-command "selectAndLoadConfigFile"]
    
    set button_cont [button $w_01_C.btn_cont -text "Continuar" -font $medium_font -command "window_04 \$config_file_name"]

#    set button_cont [button $w_01_C.btn_cont -text "Continuar" -font $medium_font -command "window_01_D \$config_file_name"]
    
    set button_exit [button $w_01_C.btn_exit -text "  Sair  " -font $medium_font -command exit]

    pack $entry_dir $button_dir -side right -expand yes -padx 5 -pady 10
    pack $labelframe_dir
    pack $button_cont $button_exit -side right -expand yes -pady 10
    pack $w_01_C

    
}


proc selectAndLoadConfigFile {} {

    global config_file_name

    set file_types {
	{ {XML} {.xml} }
	{ {All Files} * }
    }
    
    set config_file_name [tk_getOpenFile -filetypes $file_types]
}

Se for escolhido a criação de uma nova configuração será exibida a janela window_01, figura 281.

22.4.3. Seleção do Modo de Operação

Figura 281. Tela para escolha do modo de operação do fotômetro e o arquivo contendo informações sobre os LEDs disponíveis

Tela para escolha do modo de operação do fotômetro e o arquivo contendo informações sobre os LEDs disponíveis

Código para a janela window_01.

###############################################################
#Window 01
#Select types of photometry: Absorbance, Fluorescence and/or
#Turbidity
###############################################################


proc window_01 {} {
    
    global big_font medium_font w_01_B w_01 activity
    
    destroy $w_01_B

    puts "Selected $activity"

    set w_01 [frame .f_w_01]

    set message_01 [label $w_01.msg_00 -font $big_font -text "  Por favor, selecione a(s) técnicas(s):  "]

    
    set check_abs   [checkbutton $w_01.chk_abs -text "Absorbância" -font $big_font -relief flat \
			 -variable use_tech_abs \
			 -onvalue "abs" \
			 -offvalue 0
		    ]

    set check_fluor [checkbutton $w_01.chk_fluor -text "Fluorescência" -font $big_font -relief flat \
			 -variable use_tech_fluor \
			 -onvalue "fluor" \
			 -offvalue 0
		    ]
    
    set check_turb [checkbutton $w_01.chk_turb -text "Turbidez" -font $big_font -relief flat \
			-variable use_tech_turb \
			-onvalue "turb" \
			-offvalue 0
		   ]	

    #Entry and button to select the directory for leds

    set labelframe_dir [labelframe $w_01.dir -text "Seleção do arquivo de LEDs (leds.xml)" -font $big_font]
    
    set entry_dir [entry $labelframe_dir.e -width 30 -textvariable led_file_name -font $big_font]
    
    set button_dir [button $labelframe_dir.b -pady 0 -padx 2m -text "Selecione o arquivo:" -font $medium_font \
			-command "selectAndLoadLedFile"]
    
    set button_cont [button $w_01.btn_cont -text "Continuar" -font $medium_font -command window_02]

    set button_exit [button $w_01.btn_exit -text "  Sair  " -font $medium_font -command exit]

    pack $message_01
    pack $check_abs -anchor w -pady 10
    pack $check_fluor -anchor w -pady 10
    pack $check_turb -anchor w -pady 10
    pack $entry_dir $button_dir -side right -expand yes -padx 5 -pady 10
    pack $labelframe_dir
    pack $button_cont $button_exit -side right -expand yes -pady 10
    pack $w_01
    
}

proc selectAndLoadLedFile {} {

    global led_file_name

    set file_types {
	{ {XML} {.xml} }
	{ {All Files} * }
    }
    
    set led_file_name [tk_getOpenFile -filetypes $file_types]
}

22.4.4. Seleção dos LEDs para os Modos de Operação

Na tela seguinte o usuário pode selecionar quais leds serão usados nos diferentes modos de operação do fotômetro, figura 282.

Figura 282. Tela para escolha dos leds que serão usados nos diferentes modos de operação do fotômetro.

Tela para escolha dos leds que serão usados nos diferentes “modos de operação” do fotômetro.

Nessa etapa são criadas as instâncias das subclasses da classe instPhotometer que foram selecionadas na tela anterior e o arquivo XML com informações sobre os LEDs disponíveis é processado com auxílio da biblioteca tDOM:

proc window_02 {} {
    
    global big_font medium_font w_01 w_02 use_tech_abs use_tech_fluor use_tech_turb led_file_name list_inst list_leds_available doc_leds
    
    destroy $w_01

    #Open the xml file with tDOM to get info about LEDs available
    
    set channel [open $led_file_name]
    fconfigure $channel -encoding utf-8
    set doc_leds [dom parse [read $channel]]
    #set doc_leds [dom parse -channel $channel]
    close $channel

    set root [$doc_leds documentElement]

    puts "The document has the root: $root"

    puts "And the name of root: [$root nodeName]"

    set list_leds_available {}
    
    foreach root_node [$root childNodes] {
	foreach node_info [$root_node childNodes] {
	    #puts "[$node_info nodeName] : [[$node_info firstChild] nodeValue]"
	    if {[$node_info nodeName] == "id"} {
		lappend list_leds_available [[$node_info firstChild] nodeValue]
	    }
	}
    }

    #Create the instances of photometers selected
    
    set list_inst {}
    
    if { $use_tech_abs != 0 } {
	absorbanceMeter create photoAbs 01 "Medidor de Absorção" abs
	lappend list_inst photoAbs
    }
    
    if { $use_tech_fluor != 0 } {
	fluorescenceMeter create photoFluor 02 "Medidor de Fluorescência" fluor
	lappend list_inst photoFluor
    }

    if { $use_tech_turb != 0 } {
	turbidityMeter create photoTurb 03 "Medidor de Turbidez" turb
	lappend list_inst photoTurb
    }

    puts "use_tech_abs: $use_tech_abs - use_tech_fluor: $use_tech_fluor - use_tech_turb: $use_tech_turb"
    
    puts $list_inst


    set w_02 [frame .f_w_02]
    
    set message_02 [label $w_02.msg_02 -font $medium_font -text "  Por favor, selecione os leds que serão usados por cada instrumento:  "]

    pack $message_02


    #Create the labelframes and checkbuttons to select the LEDs that will be used by the insturments
    #The variable of checkbuttons are led_$id_led\_$op
    
    foreach inst $list_inst {
	
	puts "$inst -> [$inst getName]"

	#Variable id_inst store the id of the instrument
        #We may have two or more instruments for Absorbance, Fluorescence or Turbidity
	#But each one has an unique id
	
	set id_inst [$inst getId]
	
	#set op [$inst getOpticalProperty]

	set labelframe_$id_inst [labelframe $w_02.lf_$id_inst -text [$inst getName] -font $medium_font]
	
	pack [set labelframe_$id_inst] -expand yes -fill both -pady 1

	set labelframe_$id_inst\_emitter [labelframe [set labelframe\_$id_inst].emitter -text "Emissor" -font $medium_font]
	set labelframe_$id_inst\_detector [labelframe [set labelframe\_$id_inst].detector -text "Detector" -font $medium_font]

	pack [set labelframe\_$id_inst\_emitter] [set labelframe\_$id_inst\_detector] -side left -expand yes -fill both -pady 1 -padx 1
	
	foreach root_node [$root childNodes] {
	    
	    foreach node_info [$root_node childNodes] {
		puts "[$node_info nodeName] : [[$node_info firstChild] nodeValue]"
		if {[$node_info nodeName] == "id"} { set id_led [[$node_info firstChild] nodeValue] }
		if {[$node_info nodeName] == "function"} { set function_led [[$node_info firstChild] nodeValue] }
		if {[$node_info nodeName] == "color"} { set color_led [[$node_info firstChild] nodeValue] }
	    }

	    if { $function_led == "emitter" } {
		set check_led_$id_led\_$id_inst [checkbutton [set labelframe_$id_inst\_emitter].chk_$id_led\_$id_inst \
						-text $color_led -font $medium_font \
						-variable led_$id_led\_inst\_$id_inst \
						-onvalue $id_led\_$id_inst \
						-offvalue 0]

		pack [set check_led_$id_led\_$id_inst] -anchor w
		
	    } elseif { $function_led == "detector" } {

		set check_led_$id_led\_$id_inst [checkbutton [set labelframe_$id_inst\_detector].chk_$id_led\_$id_inst \
						-text $color_led -font $medium_font \
						-variable led_$id_led\_inst\_$id_inst \
						-onvalue $id_led\_$id_inst \
						-offvalue 0]
		
		pack [set check_led_$id_led\_$id_inst] -anchor w
	    }
	
	}
	
	   
    }

    set button_cont [button $w_02.btn_cont -text "Continuar" -font $medium_font -command window_03]
    
    set button_exit [button $w_02.btn_exit -text "  Sair  " -font $medium_font -command exit]

    pack $button_exit $button_cont -side left -expand yes -pady 2
    
    pack $w_02

}

O comando retorna um objeto (armazenado em doc_leds) que disponibiliza vários métodos que permitem a interação com o objeto de documento DOM que foi criado com o comando set doc_led [dom parse [read $channel]].

Os métodos podem ser chamados com o padrão: $doc_config [methodName] [Arg ...?]

Um objeto do tipo DOM possui uma estrutura do tipo árvove, formada por nós (nodes). Basicamente as tags XML são mapeadas como os nós no documento DOM.

Os LEDs operam aos pares, ou seja, precisamos de um LED emissor e outro LED operando como detector para o comprimento de onda de interesse.

Essa é a configuração usual nas medidas de absorbância, mas nas medidas de fluorescência podemos usar um único LED emissor e usarmos dois ou mais LEDs detectores para medir a intensidade de luz emitida por fluorescência em diferentes comprimentos de onda. Por exemplo, emitir no UV e detectar a fluorescência na região do azul (usando o LED detector verde) e no verde (usando o LED detector vermelho).

Portanto é necessário definir os tipos de vínculo entre os diferentes LEDs, ou seja, como serão formados os pares e quem vai ser o detector de quem?

22.4.5. Pareamento dos LEDs

Por isso criamos a tela da figura 283 para fazer essa configuração

Figura 283. Tela para configurar o pareamento dos leds que serão usados nos diferentes modos de operação do fotômetro.

Tela para configurar o “pareamento” dos leds que serão usados nos diferentes “modos de operação” do fotômetro.

Comandos para a criação da tela da figura 272 e rotinas complementares.

proc window_03 {} {

    global big_font medium_font w_02 w_03 list_inst doc_leds

    destroy $w_02

    #Var list_inst - list of instances of class Photometer
    #Var list_leds_available - list of IDs os Leds available
    #Var doc_leds is a  DOM document object
    
    puts ""
    puts "==============Window_03==============="
    puts ""

    set led_nodes [ $doc_leds getElementsByTagName leds ]

    set id_nodes [ $led_nodes getElementsByTagName id ]

    foreach id_node $id_nodes {

	lappend list_leds_available [[$id_node firstChild] nodeValue]

    }

    #puts "List of LEDs available -> $list_leds_available"

    foreach inst $list_inst {

	set id_inst [$inst getId]

    	foreach id_led $list_leds_available {

    	    #puts "LED $id_led and Inst $id_inst -> led_$id_led\_inst\_$id_inst : [set ::led_$id_led\_inst\_$id_inst]"

	    #Check if led was selected
	    
    	    if { [set ::led_$id_led\_inst\_$id_inst] != 0} {
		
		foreach led_node [$led_nodes childNodes] {
		    
		    set led_node_id [ $led_node getElementsByTagName id ]
		    
		    #puts "ID of the node $led_node is [[$led_node_id firstChild] nodeValue]"
		    
		    if { [[$led_node_id firstChild] nodeValue] == $id_led } {
			
			foreach led_info [$led_node childNodes] {
			    #if {[$led_info nodeName] == "id"} { set id_led [[$led_info firstChild] nodeValue] }
			    if {[$led_info nodeName] == "function"} { set function_led [[$led_info firstChild] nodeValue] }
			    if {[$led_info nodeName] == "color"} { set color_led [[$led_info firstChild] nodeValue] }
			    if {[$led_info nodeName] == "pinout"} { set pinout_led [[$led_info firstChild] nodeValue] }
			}

			puts "SELECTED led $color_led as $function_led with id $id_led is connected to instrument $inst by pin $pinout_led"

			#Create the instance of class Led and install in instrument

			#But check if the Led object already exist before to create a new one

			set led_exist 0
			
			foreach led_instance  [info class instances led] {
			    
			    if { [$led_instance getId] == $id_led } {
				puts "Led $led_instance already exist"
				set led_exist 1
			
				puts "Install $led_instance in instrument $inst"
				$inst setLed $led_instance
				puts "****************************************"
				puts "led $led_instance with id [$led_instance getId] exist"
				puts [$inst getLeds]
				puts "****************************************"
				
			    }
			    
			}

			if {!$led_exist} {

			    led create obj_led_$id_led $id_led $color_led $function_led

			    #Include the pin at Arduino Board connected to this LED
			    obj_led_$id_led setPinout $pinout_led
			    
			    $inst setLed obj_led_$id_led
			    #$inst setLed [led new $id_led $color_led $function_led]
			    puts "****************************************"
			    puts "obj_led_$id_led with id [obj_led_$id_led getId] don't exist"
			    puts [$inst getLeds]
			    puts "****************************************"
			}

			$inst getLeds

			puts "List os instances of class Led [info class instances led]"

			
		    }
		    
		}
    	    }
    	}
	
	
    }

    set w_03 [frame .f_w_03]

    set f_m_03 [frame $w_03.f_m_03]
    
    set message_03 [label $w_03.msg_03 -font $big_font -text "  Por favor, configure o \"pareamento\" dos leds em cada instrumento:  "]
    
    pack $message_03

    foreach inst $list_inst {

	set id_inst [$inst getId]

	set labelframe_$id_inst [labelframe $w_03.lf_$id_inst -text [$inst getName] -font $medium_font]

	pack [set labelframe_$id_inst] -pady 5

	set list_inst_leds [$inst getLeds]

	set list_emitter_leds_$id_inst [labelframe [set labelframe_$id_inst].l_e -text "Emissor" -font $medium_font]

	set list_detector_leds_$id_inst [labelframe [set labelframe_$id_inst].l_d -text "Detector" -font $medium_font]

	puts "-------------------------------------------"
	puts "The leds of inst : $inst is [$inst getLeds]"
	puts "-------------------------------------------"
	puts "After create labelframe for inst $inst \[set list_emitter_leds_\$id_inst\] : [set list_emitter_leds_$id_inst]"

	puts "After create labelframe for inst $inst \[set list_detector_leds_\$id_inst\] : [set list_detector_leds_$id_inst]"
	
	#set list_emitter_leds_$id_inst [listbox [set labelframe_$id_inst].l_e]

	#set list_detector_leds_$id_inst [listbox [set labelframe_$id_inst].l_d]
		
	foreach led $list_inst_leds {

	    puts ""
	    puts "list_inst_leds : $list_inst_leds"
	    puts "Loop foreach inst : $inst led : $led function [$led getFunction]"

	    #To avoid the error:
	    #can't set "radio_emitter_led_::obj_led_00_photoFluor": parent namespace doesn't exist
            #can't set "radio_emitter_led_::obj_led_00_photoFluor": parent namespace doesn't exist
            #while executing
            #"set radio_emitter_led_$led\_$inst [radiobutton [set list_emitter_leds_$id_inst].rb_$led
	    #We included this command to remove the leading "::" in instances of Led
	    
	    set led [string trim $led ::]

	    if {[$led getFunction] == "emitter"} {

		dict set dict_$inst\_emitter_leds [$led getColor] $led
		#[set list_emitter_leds_$id_inst] insert end [$led getColor]

		set radio_emitter_led_$led\_$inst [radiobutton [set list_emitter_leds_$id_inst].rb_$led \
						       -text "[$led getColor]" -variable emitter_led_color \
						       -value [$led getColor] ]

		puts "\[set list_emitter_leds_\$id_inst\] : [set list_emitter_leds_$id_inst]"
		puts "\[set radio_emitter_led_$led\_$inst\] : [set radio_emitter_led_$led\_$inst]"
		
		pack [set radio_emitter_led_$led\_$inst] -anchor w				  
		puts ""
		puts "Show led [$led getColor] in inst [$inst getName]"

	    } elseif {[$led getFunction] == "detector"} {

		dict set list_$inst\_detector_leds [$led getColor] $led
		#[set list_detector_leds_$id_inst] insert end [$led getColor]
		set radio_detector_led_$led\_$inst [radiobutton [set list_detector_leds_$id_inst].rb_$led \
						       -text "[$led getColor]" -variable detector_led_color \
							-value [$led getColor] ]
		pack [set radio_detector_led_$led\_$inst] -anchor w
		puts ""
		puts "Show led [$led getColor] in inst [$inst getName]"
	    }

	    puts ""

	}

	puts "END of loop foreach led ..."
	
	puts ""
	puts "For inst [$inst getName] list_inst_leds: $list_inst_leds"
	#puts "Listboxes [set list_emitter_leds_$id_inst] and [set list_detector_leds_$id_inst]"
	puts ""

	set list_pair_leds_$id_inst [listbox [set labelframe_$id_inst].pair_led]

	
	set frame_button [frame [set labelframe_$id_inst].f_b]

	set button_set_pair [ button $frame_button.set_pair -text "  Incluir ->" -font $medium_font \
				  -command "insertLedPairListbox [set list_pair_leds_$id_inst]"]
	
	set button_unset_pair [ button $frame_button.unset_pair -text "<- Remover" -font $medium_font \
				    -command "removeLedPairListbox [set list_pair_leds_$id_inst]"]

	set button_install_pair [ button $frame_button.install_pair -text "Instalar" -font $medium_font]

	#To be able to include the name of button widget, just created before, as an argument to command
	$button_install_pair configure -command "installLedPairInstrument [set list_pair_leds_$id_inst] $id_inst $button_install_pair"
	
	pack $button_set_pair -pady 5
	pack $button_unset_pair -pady 5
	pack $button_install_pair -pady 5
	
	pack [set list_emitter_leds_$id_inst] [set list_detector_leds_$id_inst] $frame_button -side left -padx 5
	
	pack [set list_pair_leds_$id_inst] -side right
	
    }


    #set button_cont [button $w_03.btn_cont -text "Continuar" -font $medium_font -command window_04]

    set button_cont [button $w_03.btn_cont -text "Continuar" -font $medium_font -command saveConfig]

    #set button_save_config [button $w_03.btn_save_config -text "Salvar Configuração" -font $medium_font -command saveConfig]
    
    set button_exit [button $w_03.btn_exit -text "  Sair  " -font $medium_font -command exit]

    pack $button_exit $button_cont -side left -expand yes -pady 2
        
    pack $f_m_03
    pack $w_03   
}

proc insertLedPairListbox { listbox_to_insert } {

    puts "-Command insertLedPair $::emitter_led_color $::detector_led_color $listbox_to_insert"

    $listbox_to_insert insert end "$::emitter_led_color:$::detector_led_color"
    
}

proc removeLedPairListbox { listbox_to_remove } {

    if {[$listbox_to_remove curselection] != ""} {
    
	$listbox_to_remove delete [$listbox_to_remove curselection]

    }
    
}

proc installLedPairInstrument { listbox_with_leds id_instrument button_install} {

    global list_inst

    puts "Command installLedPairInstrument install leds [$listbox_with_leds get 0 end] at $id_instrument with button_install:$button_install"

    puts "All instruments $list_inst"

    foreach inst $list_inst {

	if { [$inst getId] == $id_instrument } {

	    foreach pair_led [$listbox_with_leds get 0 end] {

		puts "Install pair [split $pair_led :] at instrument $inst"
		
		set pair_emitter_detector [split $pair_led :]

		set led_emitter_color [lindex $pair_emitter_detector 0]

		set led_detector_color [lindex $pair_emitter_detector 1]
		
		foreach led_instance [info class instances led] {

		    puts "led_instance:$led_instance color:[$led_instance getColor] function:[$led_instance getFunction]"

		    if { [$led_instance getColor] == $led_emitter_color && [$led_instance getFunction] == "emitter" } {

			puts "And is selected as the emitter"			
			
			set object_led_emitter $led_instance

		    } elseif { [$led_instance getColor] == $led_detector_color && [$led_instance getFunction] == "detector" } {

			puts "And is selected as the detector"

			set object_led_detector $led_instance

		    } else { puts "*The led $led_instance is NOT selected*"}

		}

		#$inst setPairLeds {*}$pair_emitter_detector

		puts "Install leds $object_led_emitter and $object_led_detector at inst $inst"

		$inst setPairLeds $object_led_emitter $object_led_detector

	    }

	    puts "Instrument $inst id:[$inst getId] name:[$inst getName] pair_leds:[$inst getPairLeds]"
	}

    }

    $listbox_with_leds delete 0 end

    #To disable the button
    $button_install configure -state disabled
}

#Procedure to save the all configuration in a XML file

proc saveConfig {} {

    global list_inst
    
    puts "Method saveConfig: "

    set list_obj_leds [info class instances led]
    
    set doc_config [dom createDocument instruments]

    set root [$doc_config documentElement]

    foreach inst $list_inst {
	
	set instrument [$doc_config createElement instrument]
	
	
	set id_instrument [$doc_config createElement id_instrument]
	
	$id_instrument appendChild [$doc_config createTextNode [$inst getId]]
	
	
	set property [$doc_config createElement property]
	
	$property appendChild [$doc_config createTextNode [$inst getOpticalProperty]]
	
	
	set name [$doc_config createElement name]
	
	$name appendChild [$doc_config createTextNode [$inst getName]]
	
	
	set led_pairs [$doc_config createElement led_pairs]
	
	set list_pair_leds [$inst getPairLeds]
	
	puts "List of pair_leds $list_pair_leds"
	
	foreach pair $list_pair_leds {

	    set pair_node [$doc_config createElement pair]
	    
	    puts "inst:$inst pair:$pair emitter:[lindex $pair 0] detector:[lindex $pair 1]"

	    foreach led $pair {

		puts "Creating the element for led:$led"

		
		set led_node [$doc_config createElement led]

		$pair_node appendChild $led_node

		set id_led_node [$doc_config createElement id_led]

		#The variable led contains the id_led
		
		$id_led_node appendChild [$doc_config createTextNode $led]

		$led_node appendChild $id_led_node
		
		foreach obj_led $list_obj_leds {

		    set id_led [$obj_led getId]

		    if {$id_led == $led} {

			#Insert color
			set color [$obj_led getColor]

			set color_node [$doc_config createElement color]

			$color_node appendChild [$doc_config createTextNode $color]

			$led_node appendChild $color_node

			#Insert function

			set function [$obj_led getFunction]

			set function_node [$doc_config createElement function]

			$function_node appendChild [$doc_config createTextNode $function]

			$led_node appendChild $function_node

			#Insert pinout

			set pinout [$obj_led getPinout]

			set pinout_node [$doc_config createElement pinout]

			$pinout_node appendChild [$doc_config createTextNode $pinout]

			$led_node appendChild $pinout_node

		    }
		    
		}

		$pair_node appendChild $led_node
	    }
	    
	    $led_pairs appendChild $pair_node
	}

	$instrument appendChild $id_instrument

	$instrument appendChild $property
	
	$instrument appendChild $name

	$instrument appendChild $led_pairs
	

	#Append a new instrument
	$root appendChild $instrument
    }

    set channel_config [open auto_save_config.xml w]

    fconfigure $channel_config -encoding utf-8

    $doc_config asXML -channel $channel_config

    close $channel_config
    
    puts "doc_config: [$doc_config asXML]"

    set user_save_config [tk_getSaveFile]

    if { $user_save_config != ""} {
	
	set channel_config [open $user_save_config w]
	
	fconfigure $channel_config -encoding utf-8
	
	$doc_config asXML -channel $channel_config

	close $channel_config

	window_04 $user_save_config

    } else {

	tk_messageBox -icon warning -type ok -title "Alerta" \
	    -message "Prezado(a) \n Por favor, salve antes um arquivo de configuração para poder continuar!"

	saveConfig
    }
}

Mas antes de passar para a tela seguinte o usuário é solicitado a salvar a configuração feita para ser usada posteriormente.

22.5. Monitoramento e Controle do Multifotômetro

A janela 04 é a tela principal, através da qual será possível controlar e monitorar as leituras do fotômetro. Por isso pensamos em frames dedicados para cada modo de operação e um frame para controle geral, figura 284.

Figura 284. Projeto inicial para a tela 04 do fotômetro.

Projeto inicial para a tela 04 do fotômetro.

O(s) frame(s) de cada modo de operação é dividido em 3 frames filhos: controle das leituras e calibração de cada cor, lista de leituras e gráfico, figura 285.

Figura 285. Projeto inicial para a tela 04 do fotômetro.

Projeto inicial para a tela 04 do fotômetro.

E detalhando um pouco mais na figura 286.

Figura 286. Principais componentes da tela 04 para controle dos diferentes modos de operação do fotômetro e acompanhamento das leituras.

Principais componentes da tela 04 para controle dos diferentes modos de operação do fotômetro e acompanhamento das leituras.

Antes da exibição da tela 04 (window_04) o programa executa a seguinte sequência de comandos:

  1. criação as instâncias da classe led, correspondentes aos LEDs instalados, com o comando:

    set name_obj_led [led create obj_led_$id_led $id_led $color_led $function_led]

  2. criação das instâncias da classe instPhotometer, correspondentes aos modos de operação do fotômetro, com os comandos:

    set id [idCustody createID ::photoAbs]

    instPhotometer create photoAbs $id "Medidor de Absorção" abs

    set id [idCustody createID ::photoFluor]

    instPhotometer create photoFluor $id "Medidor de Fluorescência" fluor

    set id [idCustody createID ::photoTurb]

    instPhotometer create photoTurb $id "Medidor de Turbidez" turb

  3. agregação dos leds aos respectivos modos de operação do fotômetro (Absorção, Fluorescência e Turbidez), com o comando:

    $inst setLed $led_instance

  4. criação da instância inicial do estado stateUNCONNECTED com o comando:

    stateUNCONNECTED create statePhotometer multi_photometer

  5. agregação dos modos de operação do fotômetro (instâncias da classe instPhotometer) ao objeto da classe stateUNCONNECTED, com os comandos:

    foreach mode_photometer [info class instances instPhotometer] {
                                statePhotometer setInstrument $mode_photometer
                        }
        

Dessa forma, teremos sempre um único objeto, contendo os diversos modos de operação do fotômetro, que alterna entre os diversos possíveis estados ao serem executados os métodos adequados.

O objeto da classe controllerStateInstrumento foi criado para permitir maior organização do código ao isolar as chamadas da interface gráfica (mais externo) dos comandos das classes que garantem o funcionamento do multifotômetro (mais interno).

Um dos métodos da classe controllerStateInstrumento é o configure, o qual chama automaticamente os eventos startUnconfiguration e endUnconfiguration para automatizar a reconfiguração do instrumento entre os diferentes modos de operação.

Foram definidos dentro dos métodos entryAction e exitAction as ações necessárias para as respectivas transições, como por exemplo registrar para qual parâmetro o instrumento foi configurado.

Ao receber a chamada startReading com os argumentos optical_parameter e color deveríamos verificar se o estado vigente é compatível com o parâmetro solicitado.

Para isso criamos o atributo active_instrument na variável state_attributes para indicar qual instrumento está ativo, ou seja, que pode estar sendo configurado, em leitura ou em calibração.

Esse atributo é definido com o id do instrumento que será ativado, dentro do método exitAction da classe stateUNCONFIGURED. E dessa forma, quando o objeto sai do estado UNCONFIGURED para ser configurado por qualquer outro estado, ele armazena o id do instrumento que ficará ativo como valor da chave active_instrument.

Foi necessário modificar os comandos de criação da tela 04 para incluir o argumento color_detector em todos os métodos startReading_xxx (xxx é o modo de operação: abs, fluor ou turb) para especificar qual LED será usado para as leituras.

E a chave reading_detector na variável state_attributes para indicar qual led detector está sendo usado e permitir que o botão parar leitura atue somente no LED que está sendo usado.

Lembrar que os LEDs só podem ser lidos sequencialmente, mesmo sendo leituras de uma mesma propriedade óptica, pois a luz emitida por um led emissor pode interferir no sinal de outros leds detectores.

Mesmo quando for ativada a leitura de todos os LEDs para todos os parâmetros, no monitoramento de um experimento, ainda assim as leituras serão, de fato, sequenciais e não simultâneas.

No início de uma sequência de leitura, o controlador dispara uma chamada para a instância do estado do instrumento iniciar as leituras. E essa chamada é repassada para a respectiva instância de instPhotometer correspondente à propriedade óptica.

Em seguida a instância do instrumento faz uma chamada para a instância da classe Led correspondente à cor que será analisada. Que, por sua vez, é responsável pelo disparo do comando de leitura para a placa Arduino, a partir do respectivo driver, e, após alguns instantes, faz uma solicitação para a instância da classe dataKeeper para resgatar os dados recebidos.

Para organizar e tornar mais previsível o fluxo de dados entre o programa e a placa Arduino, planejamos a implementação de classes especializadas nas operações de leitura (dataReader), interpretação (dataParser), processamento (dataProcessor), armazenamento persistente (dataStorage), armazenamento temporário (dataKeeper), análise (dataAnalyser) e exibição (dataDisplay e dataPlotter).

Nota

Mas nem todas foram implementadas (ainda) neste projeto, até a data de 19/08/2021.

As chamadas para o início do fluxo de dados de leitura poderia ser feita pelo objeto da classe controllerStateInstrumento, pois está mais próxima (mais vinculada) à interface gráfica (GUI). E pode simplificar a passagem de argumentos indicando, por exemplo, quais janelas devem exibir quais dados.

Criamos a classe idCreator para gerenciar a criação de chaves primárias para as instâncias das diversas classes. Mas percebemos que os LEDs físicos já tinham um ID armazenado no arquivo leds.xml.

Para evitar conflitos entre os identificadores, ficou estabelecido que o ID criado pelo idCustody (objeto da classe idCreator) corresponde a um identificador para a instância da classe Led. Mas cada LED físico possui um ID previamente definido no arquivo leds.xml, o qual é um atributo interno da instância da classe Led.

Ou seja, os LEDs possuem um ID interno e outro ID externo.

Essas chaves primárias criadas e armazenadas por idCustody são exclusivas para cada objeto e permitem mapear os dados recebidos pelo objeto dataReaderPhotometer (objeto da classe dataReader) para as instâncias solicitantes.

O objeto idCustody foi criado com o comando:

idCreator create idCustody

O objeto dataReaderPhotometer, vinculado ao instrumento Photomter, foi criado com o comando:

set dataReaderPhotometer [dataReader new $id_obj_instrument]

Onde a variável id_obj_instrument armazena o ID do objeto representativo do Fotômetro cujo ID está armazenado por idCustody.

E o objeto representativo do Fotômetro é criado com o comando:

stateUNCONNECTED create statePhotometer multi_photometer

Ele é criado inicialmente como uma instância da classe stateUNCONNECTED, mas com o comando oo::objdefine é transferido para outras classes segundo as transições de estado.

Ou seja, o objeto statePhotometer muda entre as diferentes classes que representam a estrutura hirárquica dos possíveis estados para o Fotômetro.

Nota

Repetindo, os IDs criados e armazenados por idCustody são identidades únicas externas dos objetos criados durante a execução do programa.

São acessíveis para todos os objetos e servem para identificar os objetos entre si.

Mas cada objeto pode ter um ID interno para controle das interações específicas entre alguns objetos.

As instâncias de dataReader correspondem a uma única porta de comunicação ligada a um único instrumento com o respectivo ID. Mas um único instrumento pode apresentar diferentes modos de operação, cada qual sendo representado por um objeto.

Por exemplo, o fotômetro que pode operar nos modos: Absorção, Fluorescência e Turbidez. Mas o instrumento físico Fotômetro é representado por um instrumento virtual chamado de statePhotometer, que é a entidade representativa do Fotômetro nos diferentes estados.

Enquanto que as instâncias da classe instPhotometer são representantes virtuais dos modos de operação do fotômetro físico, compartilhando alguns leds.

O objeto dataReader está vinculado a uma única porta física (que pode mudar ao longo da execução do programa) e a um único instrumento (statePhotometer) que permanece fixo ao longo da execução do programa.

O método connect do objeto controllStatePhotometer chama o método startConnection e avalia o retorno da chamada. Se o retorno for SERIAL_ERROR_OPEN ou NO_DEVICE, é feita a chamada do método errorConnection, senão chama o método connectionAccepted para dar prosseguimento aos eventos necessários.

E para fazer a desconexão, é preciso realizar as seguintes etapas:

  1. Fechar a porta serial

          #Close port
          $obj_dataReader closePort
        
  2. Remover o ID da instância de dataReader da tabela de idCustody

          #Remove the id of obj_dataReader from idCustody
          set id_obj_dataReader [ idCustody getID $obj_dataReader ]
          idCustody unsetID $id_obj_dataReader
        
  3. Remover a instância da classe dataReader

          #Destroy the obj of dataReader
          $obj_dataReader destroy
        

As quais foram implementadas no método entryAction da classe stateDISCONNECTING.

Após estabelecer a conexão, é importante ativar um loop de verificação para verificar se a conexão está ativa e disparar ações se a conexão cair. Para isso criamos inicialmente os métodos startCheckConnection e stopCheckConnection na classe controllerStateInstrument.

  method startCheckConnection {} {

  puts "Method startCheckConnection"

  

	set id_after_check_connection [after 5000 [list [self] startCheckConnection]]

	dict set controller_attributes id_after_check_connection $id_after_check_connection
    }

    method stopCheckConnection {} {

	puts "Method stopCheckConnection"

	after cancel [dict get $controller_attributes id_after_check_connection]
	}

Atenção

Acabamos criando um atributo que pode gerar confusão no programa. Os IDs do instrumento armazenados no arquivo xml foram criados pelo método createID da classe idCreator quando o arquivo de configuração foi criado pela primeira vez.

Mas ao usar um arquivo de configuração pronto, acabamos armazenando esse id original como atributo do objeto e criamos um novo ID do objeto instrumento que fica armazenado pelo objeto idCustody. Isso porque o idCreator não recebe IDs externos, mas cria os IDs internamente e de forma sequencial.

Além disso, os IDs do arquivo leds.xml não correspondem aos IDs criados por idCustody. É importante ter clareza dessa distinção para evitar confusão.

Por isso fizemos a seguinte correção no método setInstrument da classe stateUNCONNECTED para que o ID do instrumento a ser agregado seja o ID armazenado por idCustody e não mais do arquivo XML.

#The instrument is stored as a pair (ID object) with the key instruments
    method setInstrument { inst } {

	puts "Agregate instrument $inst in the state [self]"
	#27/10/2020
	#Replaced this command where the id comes from the xml file
	#puts "The ID of instrument $inst to be agregated is [$inst getId]"
	#dict set state_attributes instruments [$inst getId] $inst
	#to this command where the ID of inst comes from idCustody
	puts "The ID of instrument $inst to be agregated is [idCustody getID $inst]"
	dict set state_attributes instruments [idCustody getID $inst] $inst
	
}

A etapa seguinte foi a implementação da sequência de eventos para o início das leituras. Decidimos usar os IDs armazenados no objeto idCustody para identificar o solicitante e armazenar os dados recebidos nas respectivas caixas (ação executada pelo dataKeeper) para que o solicitante retire os dados conforme a disponibilidade.

Os dados recebidos são identificados pelo último campo referente ao ID do solicitante.

Exemplos de comando:

  • SET;D2;0;6 - definir o pino digital D2 como 0 (LOW) para o solicitando com ID=6

  • SET;D3;255;10 - definir o pino digital D3 com o valor 255 no modo PWM; para o solicitante com ID=10

  • GET;A0;8 - retorna a leitura do pino analógico A0 para o solicitante com ID=8

Para iniciar uma leitura, o usuário clica no botão Iniciar Leitura, o qual está vinculado ao método startReading do objeto controllStatePhotometer (que pertence à classe controllerStateInstrument).

Dica

No futuro podemos ter uma interface controlando diferentes instrumentos (fotômetro, potenciostato etc), cada qual com um controlador (controllStatePhotometer, controllStatePotentiostat).

O controlador chama o método startReading do respectivo stateInstrument, e essa chamada vai chamar o comando para o respectivo instrumento ativo (stateIDLE_abs, stateIDLE_fluor e stateIDLE_turb).

Cada objeto da classe instPhotometer repassa a respectiva chamada para os LEDs que estão agregados e cada LED, por sua vez, inicia o modo de leitura contínua na placa Arduino usando os respectivos drivers e as informações como o número do pino.

Mas cada led detector só pode fazer uma leitura se um led emissor estiver acionado. E as informação sobre os pares está armazenada no objeto instrumento, e portanto somente essa instância contém a informação para sincronizar o par emissor-detector.

Para que o Arduino pudesse fazer essa leitura sozinho seria necessário que essa informação estivesse armazenada no Arduino, ou enviada pelo objeto instrumento (através do objeto LED) a cada leitura.

Também poderíamos considerar a inclusão de mais um campo no comando para especificar quantas repetições de leitura deveriam ser feitas. Isso evitaria que o programa de controle enviasse as requisições de leitura repetidamente,

Para isso teríamos que editar o código do Arduino para implementar mais esse recurso. Mas diante do volume de trabalho que ainda temos pela frente decidimos implementar a lógica de leitura da sequinte forma:

Cosiderando uma sequência de leitura de 2 pares de leds (LED_emissor_1, LED_detector_1, LED_emissor_2 e LED_detector_2)

  • 1 - Instrumento envia comando ON para LED_emissor_1

    1.1 - LED_emissor_1 envia comando SET (set;[pino_digital];1) para dataReader correspondente

  • 2 - Instrumento envia comando(s) GET (leitura) para LED_detector_1

    2.1 - LED_detector_1 envia comando get;pino_analogico;id para o dataReader correspondente à porta serial

    2.2 - Ao final do envio das solicitações de leitura do par 1, é agendado, pelo LED_detector_1, com o comando after, o envio do comando readData(id_led_detector) para o dataKeeper, que estará armazenando a lista de leituras para o LED_detector_1 na caixa identificada pelo valor de id.

    2.3 - LED_detector_1 envia comando readData(id) para dataKeeper, que estará armazenando a lista de leituras para o LED_detector_1

  • 3 - Instrumento envia comando emitterTurnOFF( obj_led_emitter active_obj_dataReader ) para LED_emissor_1

    3.1 - LED_emissor_1 envia comando set (set;[pino_digital];0;[id]) para o dataReader correspondente

  • 4 - Instrumento envia comando emitterTurnON ( obj_led_emitter active_obj_dataReader ) para LED_emissor_2

    4.1 - LED_emissor_2 envia comando set (set;[pino_digital];1;[id]) para dataReader correspondente

  • 5 - Instrumento envia comando get (leitura) para LED_detector_2

    5.1 - LED_detector_2 envia comando get no formato get;pino_analogico;id para o dataReader correspondente

    5.2 - Ao final do envio das solicitações de leitura do par 2 é agendado pelo LED_detector_2, com o comando after o envio do comando readData(id) para o dataKeeper, que estará armazenando a lista de leituras para o LED_detector_2 na caixa correspondente.

  • 6 - Instrumento envia comando emitterTurnOFF para LED_emissor_2

    6.1 - LED_emissor_2 envia comando set no formato (set;[pino_digital];0;[id]) para dataReader correspondente

Dica

Lições aprendidas:

A prática de fornecer um nome para o objeto a ser criado, ao invés de deixar o sistema criar um nome, gera alguns inconvenientes, como por exemplo a inclusão de :: antes do nome do objeto. O que pode criar problemas quando se faz uma varredura nas instâncias de uma classe fazendo comparações com os nomes fornecidos com os nomes criados automaticamente.

O recomendável seria criar uma variável global e armazenar nessa variável o nome do objeto gerado automaticamente com o comando new.

Outro inconveniente foi a inclusão do ID como um dos argumentos do constructor da classe instPhotometer. Pois para criar um ID com o idCreator, é preciso passar um nome do objeto, mas para criar o objeto precisamos de um ID!

O ideal seria criar o objeto sem o ID, em seguida gerar um ID com o idCreator e repassar esse ID para o objeto com um método do tipo setID.

Mas só percebemos esse problema quando modificamos o método setInstrumento da classe stateUNCONNECTED, alterando de:

dict set state_attributes instruments [$inst getId] $inst

para:

dict set state_attributes instruments [idCustody getID $inst] $inst

A lógica da sequência de leitura foi implementada considerando a execução de leituras sucessivas de uma única cor por um único instrumento, da seguinte forma:

  1. Ao clicar o botão Iniciar leitura o objeto controllStatePhotometer executa método startReading para uma cor definida

  2. O método startReading de controllStatePhotometer executa uma mudança de estado do objeto statePhotometer, de stateIDLE_[optical_property] para stateREADING_[optical_property], o qual executa os comandos definidos no método entryAction da nova classe stateReading_[optical_property].

  3. É identificado o instrumento ativo (instPhotometer) e chama o método startReading desse método, o qual inicia uma sequência de eventos repetitivos para realizar leituras de uma determinada cor.

  4. Dentro do método startReading é localizado o led emissor e é agendada a execução da sequência: emitterTurnON(obj_led_emitter active_obj_dataReader), $obj_led_detector getReadout $active_obj_dataReader, emitterTurnOFF(obj_led_emitter active_obj_dataReader).

  5. Ao clicar o botão Parar leitura é executado o método stopReading da classe controllerStatePhotometer que, por sua vez, chama o método stopReading da classe stateReading_[optical_property], na qual é executado o método do mesmo nome (stopReading) da classe instPhotomter para cancelar os agendamentos e interromper a sequência de leituras.

Dessa forma, para alterar a cor monitorada seria necessário primeiro interromper a leitura para iniciar a leitura de outra cor, de qualquer propriedade óptica.

Os comandos enviados do instrumento para os LEDs são repassados para dataReader que envia para o Arduino pela porta serial. E os dados enviados pelo Arduino são lidos pelo dataReader e repassados para a instância de dataKeeper (keepData).

Os próprios LEDs recuperam esses dados com o comando getReadout(data_reader).

As ações para a exibição visual dos dados (em lista ou gráfico) são feitas pelo objeto controllStatePhotometer, o qual está mais próximo da interface.

Os dados ficam armazenados provisoriamente por dataKeeper e indexadas pelo ID do solicitante. E são removidos da lista após cada consulta dos dados com o método readData(id) da classe led.

Se o objeto controllStatePhotometer vai controlar a exibição dos dados na interface, optamos por implementar o método updateReading no objeto: controllStatePhotometer, dentro do qual é feita a chamada do método readData, implementado no objeto instPhotometer, passando como argumento a instância do correspondente led detector. Os LEDs fazem a leitura dos dados acumulados em dataKeeper, os quais retornam ao final para o objeto controllStatePhotometer que providencia a exibição dos dados na interface gráfica.

Mas antes de dar início ao envio de mensagens para a placa Arduino é necessário registrar um método da classe dataReader que deve ser chamado para transferir os dados enviados pelo Arduino para o objeto de dataKeeper.

#Register the event readable to call the method receiveReadout of dataReader

chan event $id_port readable [list $active_obj_dataReader receiveReadout $id_port]

Criamos uma instância única de dataKeeper (keepData) logo no início do programa com o comando:

dataKeeper create keepData

O qual vai ser responsável por armazenar as leituras indexadas pelo ID da entidade solicitante.

 method keepReadout { readout } {

	#Check to see if the readout is a string with the caracter ";"

	if { [string first ";" $readout] < 0 } {
	    puts "Received unknown message: $readout"
	    return
	}
	
	set readout [split $readout ";"]
	set field_average [lindex $readout 1]
	set average [lindex [split $field_average ":"] 1]
	
	set field_id [lindex $readout 2]
	set id [lindex [split $field_id ":"] 1]

	puts "Method keepReadout readout: $readout average: $average and ID: $id]"
	
	dict lappend data_keeper_attributes $id $average
	
    }

    method readData { id } {

	puts "Method readData of [info object class [self]] receives id:$id"
	
	set rtn [dict get $data_keeper_attributes $id]
	dict set data_keeper_attributes $id {}
	return $rtn
	
}

O método receiveReadout, da classe dataReader, recebe a leitura de readout e chama o método keepReadout do objeto keepData (classe dataKeeper).

22.6. Desconexão

Para tornar mais robusto o funcionamento do programa é necessário considerar como deve ser feito o tratamento dos eventos de desconexão acidental.

Por exemplo, o cabo pode estar desconectado antes mesmo do programa tentar abrir uma porta serial, a desconexão pode acontecer após a abertura da porta serial e a desconexão pode acontecer após o início das leituras.

É importante consederar essas 3 possíveis situações e implementar as rotinas para tratamento dessas exceções.

Nota

É necessário diferenciar a sequência de eventos do comando stopReading por solicitação do usuário, mas com o instrumento conectado, dos eventos necessários para o comando stopReading após uma queda acidental da conexão.

Por exemplo, o comando stopReading por solicitação do usuário pode enviar o comando turnOFF para o LED. Mas se a conexão cair acidentalmente, o envido do comando turnOFF vai gerar um erro.

Por isso foi incluído no método receiveReadout o comando:

set ::status_connection "DISCONNECTED"

que funciona como um semáforo para indicar que se trata de uma parada de emergência.

E no método stopReading foi incluído o teste:


if { $::status_connection == "Conectado" } {

	    [self] emitterTurnOFF $obj_led_emitter $active_obj_dataReader
	    
} else {
	    
	    puts "************************************************************************************"
	    puts "ERROR - emergency stop"
	    puts "************************************************************************************"

}

22.7. Fluxo de Dados

Como modelo para o fluxo de dados, aproveitamos o modelo conceitual proposto para o projeto do Pluviômetro descrito no diagrama de fluxo da figura 287. No qual identificamos os seguintes blocos funcionais que podem ser úteis para descrever o fluxo de dados de um sistema de monitoramento fotométrico:

  1. data read - lê os dados que chegam por um canal (serial ou TCP/IP)

  2. data parse - interpreta os dados e identifica o fotômetro, LED, leitura e unidade

  3. data storage - armazena os dados em um banco de dados

  4. data process - processa os dados (convertendo em concentração, normalizando, totalizando, calculando médias etc)

  5. data report - gera relatórios

  6. data show - exibe os dados em gráficos ou listagens

  7. data analysis - faz análises mais elaboradas podendo combinar outros dados

Essas funcionalidades podem ser visualizadas no diagrama de fluxo da figura 287.

Figura 287. Diagrama do fluxo dos dados no sistema fotométrico

Diagrama do fluxo dos dados no sistema fotométrico

O diagrama da figura 287 foi pensado inicialmente para o projeto de um pluviômetro, mas, em princípio, pode ser reutilizado para qualquer sistema de aquisição de dados.

Com base nessa proposta inicial, montamos uma sequência que funciona conforme a animação da figura 288.

Figura 288. Animação ilustrativa da sequência de eventos e o fluxo dos dados de leitura do multifotômetro.

Animação ilustrativa da sequência de eventos e o fluxo dos dados de leitura do multifotômetro.

Dica

Comando para a criação da animação com o programa ImageMagick: convert -delay 200 -loop 0 -dispose previous data_flow_* data_flow.gif

E para redimensionar:

convert data_flow_multiphotometer.gif -coalesce data_flow_multiphotometer_temp.gif
convert -size 2235x722 data_flow_multiphotometer_temp.gif -resize 1100x360 data_flow_multiphotometer.gif

22.8. Alguns links de interesse no desenvolvimento do programa de controle: