LinkedIn YouTube Facebook
Szukaj

Wstecz
Artykuły

[4] JAVA i STM32 – ekspresowy kurs programowania z MicroEJ – zegar z budzikiem i obsługą touch-panela

Uwaga! Wszystkie opublikowane odcinki kursu są dostępne pod adresem.

W poprzedniej części cyklu artykułów o programowaniu STM32 w języku JAVA, pokazano sposób stworzenia w języku JAVA prostego GUI do obsługi wewnętrznych przetworników mikrokontrolera: ADC i DAC. Interfejs graficzny został zbudowany za pomocą biblioteki Widgets. W kolejnym przykładzie przedstawione zostaną zaawansowane możliwości konfiguracji wyglądu aplikacji. Tym razem pokażemy jak zrobić zegarek cyfrowy z alarmem, bazujący na module Tiny RTC z układem DS1307Z, dołączanym do zestawu STM32F429I-DISCO za pośrednictwem magistrali I2C. Cała aplikacja dla STM32 została napisana w JAVIE.

 

Interfejs graficzny

Główną częścią przykładu jest interfejs graficzny oparty, tak jak poprzednio, o bibliotekę Widgets. Aplikacja zostanie zbudowana dla tej samej platformy co ostatnio, więc można od razy przystąpić do tworzenia nowego projektu: File?New?Java Project. Na liście dostępnych bibliotek należy zaznaczyć następujące moduły:

  • EDC
  • EJ.MOTION
  • MICROUI
  • MWT
  • SNI

Po utworzeniu projektu można od razu dodać bibliotekę Widgets pobraną ze strony projektu: https://github.com/MicroEJ/ExampleJava-Widget. Bibliotekę należy dodać przez właściwości projektu opcję Java Build Path?Libraries?Add External JARs. Dodatkowo do biblioteki można dodać znajdujące się w tym samym katalogu archiwum z jej dokumentacją. Można to zrobić klikając prawym przyciskiem myszy na bibliotekę w drzewie projektu (Referenced Libraries?widgets.jar) i wybierając opcję Properties. Następnie w menu Javadoc Location należy zaznaczyć opcję Javadoc in archive i podać ścieżkę do pliku widgets-javadoc.jar (rysunek 1). Dzięki temu można będzie korzystać z dokumentacji biblioteki podczas pisania kodu.

Rys. 1. Dodawanie dokumentacji biblioteki Widgets

Teraz można już przystąpić do utworzenia klasy Main i pozostałych klas niezbędnych do przygotowania GUI. Kod klasy Main pokazany został na listingu 1. Cały interfejs został utworzony w głównej metodzie – main.

List. 1. Kod klasy Main

public class Main {
  
  private static TitleLabel clockLabel;
  private static IntegerWheel hourWheel;
  private static IntegerWheel minuteWheel;
  private static IntegerWheel secondWheel;

  public static void main(String[] args) {
    
    MicroUI.errorLog(true);    
    MWT.RenderingContext.add(new MyTheme());
    Desktop desktop = new Desktop();
    Panel panel = new Panel();
    GridComposite gc = new GridComposite(MWT.VERTICAL, 3);
    
    clockLabel = new TitleLabel("00:00:00");
    clockLabel.setFontSize(LookExtension.GET_X_BIG_FONT_INDEX);
    gc.add(clockLabel);
    
    MultipleSpinner ms = new MultipleSpinner();
    hourWheel = new IntegerWheel();
    EndlessIntegerValue v1 = new EndlessIntegerValue(0, 23, 1, 0, "");
    hourWheel.setModel(v1);
    ms.add(hourWheel);
    
    minuteWheel = new IntegerWheel(false, 3);
    EndlessIntegerValue v2 = new EndlessIntegerValue(0, 59, 1, 0, ""); 
    minuteWheel.setModel(v2);
    ms.add(minuteWheel);
    
    secondWheel = new IntegerWheel(false, 3);
    EndlessIntegerValue v3 = new EndlessIntegerValue(0, 59, 1, 0, ""); 
    secondWheel.setModel(v3);
    ms.add(secondWheel);
    gc.add(ms);
    
    BorderComposite bc =new BorderComposite(MWT.HORIZONTAL);
    GridComposite bgc = new GridComposite(MWT.VERTICAL, 2);
    
    Button timeButton = new Button("Set Time");
    bgc.add(timeButton);
    
    Button alarmButton = new Button("Set Alarm");
    bgc.add(alarmButton);
    
    bc.addAt(bgc, MWT.CENTER);
    gc.add(bc);
    
    panel.setWidget(gc);
    panel.show(desktop, true);
    desktop.show();    
  }
}

Pierwszą instrukcją jest włączenie wyświetlania informacji o błędach. Informacje te wyświetlane będą w konsoli – zarówno podczas symulacji w środowisku MicroEJ, jak i w trakcie wykonywania programu na mikrokontrolerze dzięki omówionej poprzednio funkcjonalności Debug View w środowisku ARM/Keil MDK. Następnie tworzony jest motyw graficzny. Tym razem jednak, w miejsce motywów bibliotecznych, niezbędne będzie przygotowanie własnego motywu, opisanego w dalszej części artykułu.

Dalsza część metody main tworzy widgety tworzące interfejs użytkownika. Część z nich, jak na przykład Panel, GridComposite czy Button była już użyta i omówiona w poprzednim przykładzie. Nowością jest obiekt MultipleSpinner. Jest to widget służący do wybierania jednej z wielu wartości liczbowych lub tekstowych, znajdujących na obracającym się kole. Dodane są do niego trzy obiekty typu IntegerWheel przechowujące odpowiednio godziny, minuty i sekundy. Wartości na każdym kole są zdefiniowane jako EndlessIntegerValue. Wszystkie te obiekty tworzą, z punktu widzenia użytkownika, jeden widget służący do ustawiania aktualnej godziny lub alarmu.

Użytkownik będzie miał również do dyspozycji aktualną godzinę – obiekt typu TitleLabel, oraz dwa przyciski, którymi będzie mógł zmienić wyświetlaną godzinę lub alarm.

List. 2. Kod klasy MyLook

public class MyLook implements Look {

  private static final int FOREGROUND_COLOR_CONTENT = 0x000000;
  private static final int FOREGROUND_COLOR_DEFAULT = 0x000000;
  private static final int FOREGROUND_COLOR_FOCUSED = 0x000000;

  private static final DisplayFont SMALL_FONT = DisplayFont.getFont(DisplayFont.LATIN, 15, DisplayFont.STYLE_PLAIN);
  private static final DisplayFont MEDIUM_FONT = DisplayFont.getFont(DisplayFont.LATIN, 30, DisplayFont.STYLE_BOLD);
  private static final DisplayFont BIG_FONT = DisplayFont.getFont(DisplayFont.LATIN, 40, DisplayFont.STYLE_PLAIN);
  private static final DisplayFont X_BIG_FONT = DisplayFont.getFont(DisplayFont.LATIN, 50, DisplayFont.STYLE_PLAIN);  
  private static final DisplayFont[] FONTS = {SMALL_FONT, MEDIUM_FONT, BIG_FONT, X_BIG_FONT};
  
  @Override
  public int getProperty(int resource) {
    switch (resource) {
    case Look.GET_FOREGROUND_COLOR_CONTENT:
      return FOREGROUND_COLOR_CONTENT;
    case Look.GET_FOREGROUND_COLOR_DEFAULT:
      return FOREGROUND_COLOR_DEFAULT;
    case Look.GET_FOREGROUND_COLOR_FOCUSED:
      return FOREGROUND_COLOR_FOCUSED;
    default:
      System.out.println("Need resource: "+resource);
    }
    return 0;
  }

  @Override
  public DisplayFont[] getFonts() {
    return FONTS;
  }
}

 

Kolejnym obiektem, który należy dodać do projektu, jest przedstawiony na listingu 2 MyTheme, będący argumentem metody MWT.RenderingContext.add(new MyTheme). Obiekt ten dziedziczy po bibliotecznej klasie Theme i ma dwie główne funkcje. Pierwszą z nich jest rejestracja wszystkich niezbędnych w aplikacji rendererów, czyli obiektów odpowiedzialnych za rysowanie widgetów. Na każdy użyty w aplikacji typ widgetu musi przypadać jeden zarejestrowany renderer. Drugą z funkcji jest przekazywanie obiektu typu Look, który posiada informacje m.in. o kolorach i czcionkach użytych w aplikacji. Z tych informacji korzystają renderery aby móc odpowiednio narysować widgety. Dzięki takiemu mechanizmowi można łatwo wymienić wygląd aplikacji, zmieniając jedynie obiekt motywu typu Look.

List. 3. Kod klasy MyTheme

public class MyTheme extends Theme {

  @Override
  public String getName() {
    return null;
  }

  @Override
  protected void populate() {
    Drawer drawer = new PlainDrawer();    
    add(new MultipleSpinnerRenderer());
    add(new TitleLabelRenderer());
    add(new DesktopRenderer());
    add(new WidgetsWheelRenderer());
    add(new ButtonRenderer(drawer));
  }

  @Override
  public Look getDefaultLook() {
    return new MyLook();
  }

  @Override
  public boolean isStandard() {
    return false;
  }
}


Jak już zostało wspomniane, obiekty motywów, dziedziczące po klasie bazowej Look, dostarczają rendererom danych na temat wyglądu aplikacji. Fragment obiektu motywu – MyLook, wykorzystanego w przykładzie, pokazano na listingu 3. Zawiera on listę wszystkich używanych przez renderery wartości w postaci stałych oraz funkcje zapewniające do nich dostęp. Pierwsza z nich – getProperty, zwraca wartość w zależności od przekazanego kodu. Listę wszystkich kodów można znaleźć w dokumentacji klas Look oraz LookExtension. Druga z metod klasy MyLook zwraca listę dostępnych w aplikacji czcionek. Więcej informacji na temat tworzenia i obsługi czcionek w MicroEJ znajduję się w dalszej części tekstu.

Do uruchomienia symulacji potrzebne są jeszcze dwie klasy, których obiekty są tworzone w metodzie populate() klasy MyTheme. Są to DesktopRenderer oraz WidgetsWheelRenderer. Są to renderery klas Desktop oraz Wheel. Dzięki tworzeniu nowych rendererów w miejsce istniejącyh klas bibliotecznych można także modyfikować wygląd widgetów, jak np. kolor tła widgetu Desktop.

Rys. 2. Interfejs użytkownika projektu RTC

Teraz można już uruchomić symulację, tworząc nową konfigurację (Run As?Run Configurations) dla platformy Full, pamiętając o zaznaczeniu opcji Execute on SimJPF. Efekt jest widoczny na rysunku 2.

Niestety aplikacja nie wygląda jeszcze tak jak powinna. Jest to spowodowane brakiem wymaganych czcionek, zdefiniowanych w klasie MyLook. W kolejnym rozdziale przedstawione zostaną metody tworzenia oraz dołączania czcionek do projektu.

Czcionki

Do poprawnego wyświetlenia wszystkich elementów interfejsu użytkownika potrzebne są jeszcze czcionki. Domyślnie, platforma Full zawiera jeden rodzaj czcionki, do poprawnego wyświetlenia widgetu MultipleWheelSpinner należy dodać własną czcionkę z opcją antyaliasingu. Do tego celu można wykorzystać generator czcionek znajdujący się w środowisku MicroEJ. W tym celu należy kliknąć prawym przyciskiem myszy na drzewie projektu i wybrać New?MicroEJ Font. W otwarty oknie trzeba podać nazwę katalogu i dowolną nazwę czcionki, a po naciśnięciu przycisku Next, podać jej rozmiar. W przykładzie używane są dwie czcionki o rozmiarach 30 i 50 pikseli. Nowo dodana czcionka nie ma jeszcze zdefiniowanych żadnych znaków. Można je rysować własnoręcznie w dołączonym edytorze, lub zaimportować istniejącą czcionkę. W przykładzie przedstawiono drugą metodę, importując dwie czcionki znajdujące się w archiwum biblioteki Widgets (ExampleJava-Widget-master\com.is2t.demo.widgets\src\resources\fonts).

Rys. 3. Importowanie czcionki z pliku

Aby zaimportować istniejącą czcionkę należy kliknąć przycisk Import w oknie czcionki i zaznaczyć opcję EJF file. Następnie należy podać ścieżkę do pliku *.ejf i wybrać zakres znaków do zaimportowania (rysunek 3). Dodatkowo, po zaimportowaniu znaków, część z nich można usunąć, aby nie zabierały miejsca w pamięci.

Po utworzeniu plików z czcionkami należy je jeszcze dodać do projektu. W tym celu należy utworzyć plik tekstowy z rozszerzeniem *.list z listą dodanych czcionek. W przykładzie zawiera on tylko dwa wiersze:

fonts/big_font.ejf::4

fonts/small_font.ejf::4

Plik ten informuje środowisko gdzie szukać plików z czcionkami, dlatego też ścieżki należy zmienić, w zależności od ich lokalizacji w projekcie.

Ostatnim krokiem jest podanie ścieżki do pliku z listą czcionek w konfiguracji projektu. Aby to zrobić należy otworzyć okno Run Confgurations, a w zakładce Configuration podać odpowiednią ścieżkę (rysunek 4).

Rys. 4. Dodawanie listy czcionek do projektu

Z uwagi na to, że w przykładzie nie będą używane czcionki domyślne platformy, można je usunąć ze względu na zużycie pamięci. Plik z czcionkami znajduje się w katalogu *-configuration/microui/resources/fonts. Po usunięciu pliku, platformę należy skompilować ponownie.

Teraz można ponownie uruchomić symulację i sprawdzić czy nowe czcionki zostały poprawnie dodane do projektu.

Odmierzanie czasu

Do poprawnej obsługi aktualnej godziny przygotowana została klasa RTC pełniąca dwie role. Pierwszą z nich jest komunikacja ze sprzętowym modułem RTC (opisanym w kolejnym rozdziale), a drugą generowanie napisu przedstawiającego aktualną godzinę. Kod klasy RTC przedstawiono na listingu 4.

List. 4. Kod klasy RTC

public class RTC {
  
  private int time;
  private int alarmTime = 0xFFFFFF;
  
  public RTC() {
  }
  
  void readTime() {
  }
  
  public void setTime(int sec, int min, int hr) {
    this.time = (sec % 10) | ((sec/10) << 4) | ((min % 10) << 8) | ((min/10) << 12) | ((hr % 10) << 16) | ((hr/10) << 20); } public String createTimeString() { int sec = ((this.time & 0xF0) >> 4) * 10 + (this.time & 0x0F); 
    int min = ((this.time & 0xF000) >> 12) * 10 + ((this.time & 0x0F00) >> 8);
    int hour = ((this.time & 0xF00000) >> 20) * 10 + ((this.time & 0x0F0000) >> 16);
    
    StringBuilder timeBuilder = new StringBuilder();
    if(hour<10)
      timeBuilder.append(0);
    timeBuilder.append(hour);
    timeBuilder.append(":");
    if(min<10)
      timeBuilder.append(0);
    timeBuilder.append(min);
    timeBuilder.append(":");
    if(sec<10)
      timeBuilder.append(0);
    timeBuilder.append(sec);
    return timeBuilder.toString();
  }
  
  void setAlarmTime(int sec, int min, int hr) {
    this.alarmTime = (sec % 10) | ((sec/10) << 4) | ((min % 10) << 8) | ((min/10) << 12) | ((hr % 10) << 16) | ((hr/10) << 20);
  }
  
  boolean checkAlarm() {
    return this.time == this.alarmTime;
  }
}