Quando se começa a aprender a programar, uma das primeiras coisas que as pessoas dizem é: desenvolver interfaces gráficas (GUI) é difícil. E de fato isto é uma grande verdade. Ainda mais quando performance é uma preocupação. Mas também é verdade que está ficando cada vez mais fácil construir interface gráficas, vistos os novos frameworks que estão surgindo.
O objetivo deste artigo é mostrar como eu, um estudante de Engenharia de Controle e Automação, criou uma interface de painel para automação industrial.
O framework escolhido para este projeto foi o TotalCross, seguindo o padrão de arquitetura que ele orienta (para melhorar a manutenção/ajustes no código no futuro).
Criando o projeto
O primeiro passo é efetuar o login usando o plug-in VSCode e criar um novo projeto. Para aqueles que ainda não tenham um registro neste framework, devem acessar o Get Started no site do TotalCross.
Os campos devem ser preenchidos da seguinte maneira:
- GroupId: com.totalcross.sample.showData
- Nome: showData
Dentro do projeto, deve-se acessar path>src> main> java> com> totalcross>sample>showData e criar as pastas ui e util. Sendo que:
- pasta ui: onde ficam todas as classes de interface do usuário (GUI)
- pasta util: onde vão as classes de constantes que serão usadas durante o desenvolvimento.
Em seguida, deve-se acessar projeto> src> main e criar as pastas de resources. Os arquivos que serão usados na interface gráfica (fontes, cores e imagens) estarão nesta pasta .
- Para baixar as fontes, clique aqui.
Util
Conforme comentado, nesta pasta vão as classes que serão usadas para criar as constantes que iremos usar no projeto, como fontes, imagens e cores. Criar uma classe para cada um desses tipos de constantes é uma prática que o próprio framework orienta (clique aqui para ler mais). O TotalCross orienta esse padrão para concentrar todas as imagens e cores em uma única classe. Desta forma, todas as imagens e cores são referenciadas em uma mesma classe, evitando ter várias classes onde a cor de background é afetada várias vezes. Isto facilita bastante a manutenção do código, pois uma simples mudança de cores de interface necessitará acessar apenas uma classe ao invés de várias.
De volta ao código, nesta pasta util deve-se criar 3 arquivos: Colors.java, Fonts.java e MaterialConstants.java. Eles adicionam as respectivas bibliotecas e bibliotecas adicionais para modificar as variáveis que precisam ser alteradas, devido a alguma especificidade da aplicação.
Por exemplo, o código Colors.java:
package com.totalcross.sample.showData.util;
import totalcross.ui.gfx.Color;
public class Colors {
public static int BLUE = 0x4a91e2;
public static int WHITE = Color.WHITE;
public static int GRAY = Color.getRGB(241, 241, 241);
public static int DARK_GRAY = Color.getRGB(50,50,50);
public static final int P_700 = 0x3e72c1;
public static final int P_200 = 0x98c7f1;
public static int BACK = Color.getRGB(254, 254, 254);
}
A constante BLUE, biblioteca nativa Colors, não possui o tom de azul que queremos e por isso vamos colocar o valor hexadecimal logo após o padrão 0x do TotalCross (0x<valor_hexadecimal>).
As constantes GREY e DARK_GRAY nesse tom, também não existem e como serão utilizadas (e muito!), deve-se usar o método Color.getRGB(r,g,b) para colocar a cor que queremos.
A cor branca já está na biblioteca Color, não necessitando nenhuma ação adicional.
Basicamente esta é a forma de setar cores utilizando TotalCross.
Para acessar o código dos outros dois arquivos (Fonts e Images), clique aqui.
(G)UI
Todas as classes de interface com o usuário devem ser criadas nesta pasta.
O arquivo ShowMainDataWindow.java, deve ser colocado dentro desta pasta, será o arquivo principal, onde colocamos o estilo de interface e, caso queira publicar sua aplicação para Android ou iOS, é nesta classe onde você estabelece os padrões. Nesta classe também que são chamadas a interface principal.
Estas duas classes também devem ser criadas:
- ShowDataContainer.java: : o arquivo onde se encontram os containers que serão exibidos na interface principal;
- TimerContainer.java: Parte da interface onde as informações do temporizador são mostradas
- ActuatorContainer.java: Parte da interface onde são exibidos o estado dos atuadores (outputs).
- SensorContainer.java: Parte da interface onde as informações de sensores.
Para saber melhor o que é container, basta clicar aqui.
Obs: Cada parte do aplicativo usa os componentes Hbox e Vbox, Edit, Label, Button e Scroll Container.
Timer Container
Esta é a classe onde vamos criar o container o container que ficará no meio da interface da sua aplicação onde você poderá definir o horário de início e término do confinamento para um dos pinos GPIO, acender um led em uma determinada saída e excluir qualquer uma dessas configurações.
Este tipo de aplicação é utilizada em diversos cenários, como controle de alimentadores de animais e programa de irrigação de plantações.
O código abaixo faz parte do código do TimerContainer. A mesma estrutura é usada em outras partes do código.
Primeiro, os componentes são criados e algumas características são definidas. Depois são adicionados a um Hbox que é adicionado à tela centralizada. Portanto, não é necessário adicionar cada componente individualmente, o que torna a interface mais limpa e facilita a alocação de componentes.
Edit pintEdit = new Edit(); // edit for recive pin for timer
pintEdit.caption = "GPIO"; // Edit text
pintEdit.setFont(Font.getFont(true, MaterialConstants.TEXT_SIZE));
pintEdit.setMode(Edit.CURRENCY); // set mode numeric
pintEdit.setKeyboard(Edit.KBD_NUMERIC); // lock all non-numeric keys for this edit
pintEdit.setBackForeColors(Colors.GRAY, Color.BLACK);
pintEdit.captionColor = Color.BLACK;
Button savetBt = new Button("Save"); // button for save configuation of timer
savetBt.setFont(Font.getFont(true, MaterialConstants.TEXT_SIZE));
// size
savetBt.setBackForeColors(Colors.BLUE, Colors.WHITE);
Edit starttEdit = new Edit("99" + Settings.timeSeparator + "99" + Settings.timeSeparator + "99");
starttEdit.caption = "Start";
starttEdit.setValidChars("0123456789AMP");
starttEdit.setMode(Edit.NORMAL, true);
starttEdit.setKeyboard(Edit.KBD_TIME);
starttEdit.setFont(Font.getFont(true, MaterialConstants.TEXT_SIZE));
starttEdit.setBackForeColors(Colors.GRAY, Color.BLACK);
starttEdit.captionColor = Color.BLACK;
Edit endtEdit = new Edit("99" + Settings.timeSeparator + "99" + Settings.timeSeparator + "99");
endtEdit.caption = "End";
endtEdit.setValidChars("0123456789AMP");
endtEdit.setMode(Edit.NORMAL, true);
endtEdit.setKeyboard(Edit.KBD_TIME);
endtEdit.setFont(Font.getFont(true, MaterialConstants.TEXT_SIZE));
endtEdit.setBackForeColors(Colors.GRAY, Color.BLACK);
endtEdit.captionColor = Color.BLACK;
HBox box = new HBox(HBox.LAYOUT_FILL, HBox.ALIGNMENT_STRETCH);
box.add(starttEdit);
box.add(endtEdit);
box.setSpacing(MaterialConstants.COMPONENT_SPACING);
// add components for timer
sc.add(timerLb, CENTER, TOP, sc.getWidth(), MaterialConstants.EDIT_HEIGHT);
sc.add(box, LEFT + MaterialConstants.BORDER_SPACING, AFTER + aterialConstants.COMPONENT_SPACING,FILL -MaterialConstants.BORDER_SPACING, MaterialConstants.EDIT_HEIGHT);
box = new HBox(HBox.LAYOUT_FILL, HBox.ALIGNMENT_STRETCH);
box.add(pintEdit);
box.add(savetBt);
box.setSpacing(MaterialConstants.COMPONENT_SPACING);
sc.add(box, LEFT + MaterialConstants.BORDER_SPACING, AFTER + MaterialConstants.COMPONENT_SPACING,FILL -MaterialConstants.BORDER_SPACING, MaterialConstants.EDIT_HEIGHT);
Acesse o código completo aqui.
Actuator Container
Este container mostra o estado atual dos atuadores, o valor e permite disparar e alterar os pinos de GPIO, que foram definidos como a saída.
Normalmente isso é usado para unidades digitais, como acender uma lâmpada ou abrir um portão. E com a alteração manual do valor é possível, por exemplo, definir a intensidade de iluminação de uma lâmpada ou a velocidade de rotação de um motor.
A maior mudança nesse container é que, usando o Vbox, é possível colocar todos os componentes com o mesmo tamanho em um único Vbox. Adicionar cada conjunto à tela fica mais fácil e o código ainda fica menor do que o do formulário usado no TimerContainer.
O código a seguir mostra melhor a diferença:
VBox boxi = new VBox(VBox.LAYOUT_FILL, VBox.ALIGNMENT_STRETCH);
boxi.add(indicator1);
boxi.add(indicator2);
boxi.add(indicator3);
boxi.add(indicator4);
boxi.setSpacing(MaterialConstants.COMPONENT_SPACING);
sc.add(boxi,RIGHT-MaterialConstants.BORDER_SPACING, AFTER+MaterialConstants.COMPONENT_SPACING,MaterialConstants.EDIT_HEIGHT,(MaterialConstants.COMPONENT_SPACING + MaterialConstants.EDIT_HEIGHT)*4);
VBox boxv = new VBox(VBox.LAYOUT_FILL, VBox.ALIGNMENT_STRETCH);
boxv.add(value1);
boxv.add(value2);
boxv.add(value3);
boxv.add(value4);
boxv.setSpacing(MaterialConstants.COMPONENT_SPACING);
sc.add(boxv,BEFORE - MaterialConstants.BORDER_SPACING, SAME, PREFERRED, boxi.getHeight());
VBox boxa = new VBox(VBox.LAYOUT_FILL, VBox.ALIGNMENT_STRETCH);
boxa.add(Actuator1);
boxa.add(Actuator2);
boxa.add(Actuator3);
boxa.add(Actuator4);
boxa.setSpacing(MaterialConstants.COMPONENT_SPACING);
sc.add(boxa,LEFT + MaterialConstants.BORDER_SPACING, SAME, boxv.getX() -MaterialConstants.COMPONENT_SPACING*3, boxv.getHeight());
Acesse o código completo clique aqui.
Sensor Container
O SensorContainer permite ver o estado/valor atual dos sensores, se estiver recebendo dados, mas pode também vinculá-lo a um atuador (output). O usuário pode definir um valor de acionamento e desacionamento para o pino e então, quando houver uma alguma entrada que atenda aos requisitos indicados pelo usuário, ative ou desative o atuador.
Este é um processo de trabalho comum usado para termostatos.
Essa tela tem as mesmas características que a anterior, com componentes verticais semelhantes no Vbox e componentes horizontais no Hbox.
Clique aqui para acessar o código completo.
Show data container
E por último, a classe que reúne todos os containers anteriores e os exibe para o usuário. Para chamar os containers, basta tratá-los como objeto da lib ScrollContainer, pois foram criados como extensão dela. Veja abaixo:
package com.totalcross.sample.showData.ui;
import com.totalcross.sample.showData.util.Colors;
import totalcross.sys.Settings;
import totalcross.ui.Container;
import totalcross.ui.Label;
import totalcross.ui.ScrollContainer;
import totalcross.ui.font.Font;
public class ShowDataContainer extends Container {
private ScrollContainer sc;
@Override
public void initUI() {
// add containers
sc = new ScrollContainer(true, true);
sc.setBackColor(Colors.GRAY);
add(sc, LEFT, TOP, FILL, FILL);
TimerContainer tcont = new TimerContainer();//
instantiating TimerContainer
ActuatorContainer Acont = new ActuatorContainer();//
instantiating ActuatorContainer
SensorContainer Scont = new SensorContainer();// instantiating SensorContainer
sc.add(Acont, CENTER, TOP + (int) (Settings.screenHeight * 0.15), (int) (Settings.screenWidth * 0.3),(int)(Settings.screenHeight * 0.8));// add Actuator container
sc.add(tcont, CENTER - (int) (Settings.screenWidth * 0.02) - Acont.getWidth(), SAME, Acont.getWidth(), Acont.getHeight());//add Timer container
sc.add(Scont, CENTER + (int) (Settings.screenWidth * 0.02) + Acont.getWidth(), SAME, Acont.getWidth(), Acont.getHeight());//add Sensor container
// APP Title
Label Title = new Label("Show Data");
Title.setFont(Font.getFont(true, (int) (0.05 * Settings.screenHeight)));// add Title label
sc.add(Title, CENTER, TOP + 10);
}
}
ShowDataMainWindow
Este arquivo é onde as características básicas do aplicativo são definidas, como pode ser visto no código a seguir.
Os componentes seguem o estilo FLAT_UI, pois devido ao seu design simples, reduz o consumo de RAM da aplicação. Se não houver limitações de consumo de memória,outros conjuntos de componentes com estilos mais elaborados poderão ser usados, como MATERIAL_UI e ANDROID_UI.
package com.totalcross.sample.showData.ui;
import totalcross.ui.MainWindow;
import totalcross.ui.font.Font;
import com.totalcross.sample.showData.util.Fonts;
import totalcross.sys.Settings;
public class ShowDataMainWindow extends MainWindow {
public static final String version = "1.0.0"; // app version
public ShowDataMainWindow() {
setUIStyle(Settings.FLAT_UI); // define design
setDefaultFont(Font.getFont(Fonts.FONT_DEFAULT_SIZE));//define font
}
static {
Settings.applicationId = "SDSA"; //app id
Settings.appVersion = version;
}
@Override
public void initUI() {
super.initUI();
ShowDataContainer container = new ShowDataContainer(); // get app screen
swap(container);// update screen
}
}
Essa foi a última classe necessária para montar a interface de painel para automação industrial.
Se deseja fazer o download da aplicação para testar, basta baixar no GitHub da TotalCross.
Resultados
O resultado final do projeto pode ser encontrado nas imagens abaixo:
Devido a simplicidade, o plug-in Vscode foi usado para rodar a aplicação. Com o simulador da TotalCross é possível ajustar a resolução e a densidade de pixels, podendo assim verificar previamente o resultado antes de rodar no aparelho.
Nas imagens abaixo você pode conferir o resultado já no Raspberry PI 3:
A imagem acima é de um RPI, mas é possível rodar em outras SBCs e computadores em módulos.
Conclusão
Este exemplo demonstra o que seria um “front end” (GUI) para uma aplicação embarcada que acessa o GPIO. É possível aproveitar a lógica aplicada nesse exemplo de maneiras bem variadas, dependendo da necessidade de informação, do design da aplicação e o do quanto de memória pode-se consumir. Vale bastante a pena testar com material UI, pois o aumento de memória não é tão significativo (o que planejo publicar sobre no futuro).






