LinkedIn YouTube Facebook
Szukaj

Newsletter

Proszę czekać.

Dziękujemy za zgłoszenie!

Wstecz
Artykuły

[5] JAVA i STM32 – ekspresowy kurs programowania z MicroEJ – stacja pogodowa

Klasa ChartWidget – przedstawiona na listingu 4 – dziedziczy po klasie abstrakcyjnej Widget. Klasa ta zawiera m.in. metody zwracające rozmiar oraz pozycje widgetu, jednak przez to, że jest klasą abstrakcyjną, nie może być instancjonowana. Warto także zwrócić uwagę na to, że klasa ta nie zawiera abstrakcyjnych metod, więc klasy dziedziczące mogą implementować wyłącznie metody potrzebne im do zarządzania danymi. Tak jest w przypadku klasy ChartWidget znajdującej się na listingu 4. Zawiera ona pięć pól: listę punktów, maksymalną i minimalną wartość widoczną na wykresie, maksymalna liczbę punktów oraz kolor wykresu. Oprócz metod zwracających wartości tych pól, lub referencję w przypadku listy punktów, zawiera także metodę addPoint, która dodaje punkt do listy, kontroluje jej długość oraz aktualizuje maksymalną i minimalną wartość.

Klasa ChartRenderer wykorzystuje dane znajdujące się w obiekcie ChartWidget do narysowania wykresu. W przykładzie, nie przechowuje ona danych – posiada wyłącznie metody i stałe używane przy rysowaniu. Kod klasy ChartRenderer, z pominięciem pustych metod getPreferredContentWidth i getPreferredContentHeight, został przedstawiony na listingu 5.

List. 5. Kod klasy ChartRenderer

public class ChartRenderer extends WidgetRenderer {

	private final int X_AXIS_LEFT_MARGIN = 15;
	private final int X_AXIS_RIGHT_MARGIN = 15;
	private final int Y_AXIS_TOP_MARGIN = 10;
	private final int Y_AXIS_BOTTOM_MARGIN = 10;

	@Override
	public Class<?> getManagedType() {
		return ChartWidget.class;
	}

	@Override
	public void render(GraphicsContext g, Renderable renderable) {
		ChartWidget f = (ChartWidget) renderable;
		
		g.setColor(0xFFFFFF);
		g.fillRect(0, 0, f.getWidth(), f.getHeight());
		
		g.setColor(0x000000);
		g.drawRect(this.X_AXIS_LEFT_MARGIN-1, this.Y_AXIS_TOP_MARGIN-1, f.getWidth()-this.X_AXIS_LEFT_MARGIN-this.X_AXIS_RIGHT_MARGIN+1, f.getHeight()-this.Y_AXIS_TOP_MARGIN-this.Y_AXIS_BOTTOM_MARGIN+1);
		
		DisplayFont font = getLook().getFonts()[getLook().getProperty(LookExtension.GET_SMALL_FONT_INDEX)];
		int labelsCount = (f.getHeight()-this.Y_AXIS_TOP_MARGIN-this.Y_AXIS_BOTTOM_MARGIN)/(2*font.getHeight());
		
		g.setFont(font);
		
		for(int i=0; i<labelsCount+1; i++) {
			int labelValue = Math.round(f.getMaxY() - i*(f.getMaxY()-f.getMinY())/labelsCount);
			String label = new Float(labelValue).toString();
			g.drawChars(label.toCharArray(), 0, label.length(), 0, this.Y_AXIS_TOP_MARGIN + 2*i*font.getHeight(), GraphicsContext.LEFT);
		}
		
		float scalingFactor = (f.getHeight()-this.Y_AXIS_TOP_MARGIN - this.Y_AXIS_BOTTOM_MARGIN)/(f.getMaxY()-f.getMinY());
		g.setColor(f.getColor());
		for(int i=0; i<f.getPoints().size()-1; i++) {
			int x1 = i+this.X_AXIS_LEFT_MARGIN;
			int y1 = f.getHeight() - this.Y_AXIS_BOTTOM_MARGIN - (int)((f.getPoints().get(i).floatValue() - f.getMinY())*scalingFactor); 
			int x2 = i+1+this.X_AXIS_LEFT_MARGIN;
			int y2 = f.getHeight() - this.Y_AXIS_BOTTOM_MARGIN - (int)((f.getPoints().get(i+1).floatValue() - f.getMinY())*scalingFactor);		
			g.drawLine(x1, y1, x2, y2);
		}		
	}
}
 

Pierwsza z metod – getManagedType – zwraca klasę za którą jest odpowiedzialny renderer. W tym wypadku jest to oczywiście ChartWidget. W drugiej z metod render rysuje widget na ekranie. Argumentami tej metody są obiekty typu GraphicsContext oraz Renderable. Pierwszy z nich umożliwia rysowanie na ekranie prostych kształtów, takich jak punkty, linie, prostokąty, a także wyświetlanie tekstu. Drugi z argumentów, typu Renderable, jest referencją do obiektu rysowanego widgetu zawierającego wszystkie niezbędne dane. Argument ten musi zostać zrzutowany na odpowiedni typ, co umożliwi wywoływanie jego metod i dostęp do danych. Zostało to wykonane w pierwszej instrukcji funkcji render. Następnie czyszczone jest całe dostępne pole przez narysowanie białego prostokąta o rozmiarze widgetu. W dalszej kolejności rysowane są osie wykresu oraz ich opisy. Odstępy między krawędziami widgetu, a osiami zostały zdefiniowane jako stałe pola klasy ChartRenderer. Na samym końcu, przy pomocy linii, rysowane są wszystkie punkty wykresu, po ich wcześniejszym przeskalowaniu względem maksymalnej i minimalnej wartości. Dzięki temu wykres zostaje wyświetlony na całej powierzchni widgetu, przez co taje się bardziej czytelny.

Po utworzeniu nowego widgetu, można dodać go do interfejsu graficznego. Wszystkie cztery obiekty typu ChartWidget zostały dodane jako prywatne obiekty statyczne klasy Main, ponieważ będą potrzebne w jej klasach wewnętrznych (np. w słuchaczach przycisków). Podobnie jak w poprzednim przykładzie, nie jest to rozwiązanie najlepsze, ale najprostsze. Lepszą alternatywą byłoby przekazanie wszystkich wymaganych obiektów jako argumenty konstruktora lub innych metod.

List. 6. Wykres dodany do klasy Main

public class Main {
	
	private static ChartWidget temperatureChart;
	private static ChartWidget pressureChart;
	private static ChartWidget humidityChart;
	private static ChartWidget lightChart;
	
	public static void main(String[] args) {
		
		temperatureButton = new Button("T: ");
		temperatureButton.setListener(new Listener() {
			
			@Override
			public void performAction(int value, Object object) {
				bc.addAt(temperatureChart, MWT.CENTER);
			}
			
			@Override
			public void performAction(int value) {
			}
			
			@Override
			public void performAction() {
			}
		});
		valuesGcV1.add(temperatureButton);
							
	temperatureChart = new ChartWidget(200, 0xFF0000);
		pressureChart = new ChartWidget(200, 0x0000FF);		
		humidityChart = new ChartWidget(200, 0x00FF00);
		lightChart = new ChartWidget(200, 0xFFFF00);		
	}
}
 

Podczas tworzenia obiektów ChartWidget, do konstruktora przekazywane są dwie wartości: liczba widocznych punktów wykresu oraz jego kolor. Dzięki temu będzie można rozróżnić poszczególne widgety. Należy także obsłużyć zdarzenia od przycisków, tak aby po ich naciśnięciu zmieniał się wyświetlany wykres. Można to zrobić dodając do pozycji CENTER, obiektu BorderComposite odpowiedni widget w każdym ze słuchaczy przycisków. Kod obsługujący jeden wykres, dodany w funkcji main (z pominięciem pokazanego wcześniej) znajduje się na listingu 6. Do poprawnego wyświetlenia brakuje jeszcze tylko odpowiedniego renderera w obiekcie motywu aplikacji. Należy to zrobić przez dodanie linii add(new ChartRenderer()); na końcu metody populate obiektu MyTheme. Teraz można już uruchomić symulację aby obejrzeć kompletny interfejs graficzny (rysunek 1).

Rys. 1. Widok okna symulacyjnego środowiska MicroEJ – interfejs użytkownika po dodaniu wykresu

Obsługa czujników (Java)

Zanim czujniki zostaną podłączone do magistrali I2C należy przygotować kod odpowiedzialny za ich konfigurację i odczyt danych, zarówno na poziomie aplikacji Javy, jak i platformy. Przy okazji zostaną przedstawione mechanizmy dziedziczenia oraz interfejsów w Javie.

List. 7. Kod klasy I2CSensor

public class I2CSensor {
	protected I2CBus i2cBus;
	
	public I2CSensor(I2CBus i2cBus) {
		this.i2cBus = i2cBus;
	}

}
 

Dziedziczenie w językach obiektowych polega na utworzeniu jednej klasy bazowej oraz szeregu klas pochodnych. Klasy potomne mogą dziedziczyć tylko po jednej klasie bazowej przejmując jej metody i pola, jeżeli nie są zadeklarowane jako prywatne. Dodatkowo mogą one rozszerzać funkcjonalność klasy bazowej. Może się to odbywać przez dodawanie nowych metod i pól, ale również przez przesłanianie metod klasy bazowej, czyli definiowanie nowych funkcjonalności dla istniejących metod w klasie bazowej. W przykładzie została utworzona jedna klasa bazowa I2CSensor przedstawiona na listingu 7. Klasa ta zawiera jedynie konstruktor oraz pole przechowujące obiekt typu I2CBus (kod na listingu 8), zawierający metody zapisu i odczytu urządzeń na magistrali I2C oraz deklarację metod natywnych, implementowanych w C (opisanych dalej).

List. 8. Kod klasy I2CBus

public class I2CBus {
	static native int I2CRead(int slaveAddr, int regAddr, int bytes);
	static native void I2CWrite(int slaveAddr, int regAddr, int value, int bytes);
	
	public int readRegister(int slaveAddr, int regAddr, int bytes) {
		return I2CBus.I2CRead(slaveAddr, regAddr, bytes);
	}
	
	public void writeRegister(int slaveAddr, int regAddr, int value, int bytes) {
		I2CBus.I2CWrite(slaveAddr, regAddr, value, bytes);
	}
}
 

Idea interfejsów w Javie jest podobna do dziedziczenia jednak istnieją pewne różnice. Przede wszystkim interfejsy implementowane przez klasy mogą zawierać jedynie deklaracje metod (bez ich implementacji) i nie mogą posiadać zmiennych. Jednak w przeciwieństwie do dziedziczenia, każda klasa może implementować wiele interfejsów, implementując ich metody. W ramach przykładu utworzony został jeden interfejs o nazwie Sensor (listing 9). Deklaruje on metodę getValue, która posłuży do odczytu danych z sensorów. Interfejs ten jest implementowany przez wszystkie klasy czujników, które jednocześnie dziedziczą po klasie bazowej I2CSensor. Rozwiązanie to może wydawać się zbyt skomplikowane wobec tak prostej aplikacji, jednak jego celem jest zobrazowanie mechanizmów języka Java mogących znaleźć zastosowanie w bardziej złożonych projektach.

List. 9. Kod interfejsu Sensor

public interface Sensor {
	public float getValue();
}
 

Ostatnimi klasami dodanymi do projektu są cztery klasy odpowiadające podłączanym czujnikom:

  • TemperatureSensor
  • PressureSensor
  • HumiditySensor
  • LightSensor

Każda z klas zawiera konstruktor oraz metodę getValue. W konstruktorze następuje konfiguracja czujnika oraz inicjalizacja zmiennych, jeżeli to konieczne, natomiast metoda getValue wykonuje wszystkie niezbędne operacje aby odczytać dane i obliczyć wartości rzeczywiste temperatury, ciśnienia, wilgotności oraz natężenia światła.

List. 10. Kod klasy TemperatureSensor

public class TemperatureSensor extends I2CSensor implements Sensor {

	public TemperatureSensor(I2CBus i2cBus) {
		super(i2cBus);
	}

	@Override
	public float getValue() {
		int i2cValue = this.i2cBus.readRegister(0x48, 0, 1);
		float temperature = i2cValue&0x7F;
		if((i2cValue&0x80)>0)
			temperature *= -1;
		return temperature;
	}
}
 

Dla przykładu, na listingu 10 pokazany został kod klasy TemperatureSensor. Klasy odpowiadające pozostałym czujnikom można znaleźć w źródłach projektu. Obliczenia dla poszczególnych czujników znajdują się w ich kartach katalogowych.

Na koniec należy jeszcze do klasy Main dodać pola odpowiadające wszystkim czterem sensorom i magistrali I2C, a w metodzie main utworzyć odpowiednie obiekty. Na samym końcu, po utworzeniu interfejsu graficznego, tworzony jest także obiekt klasy Timer, który w równych odstępach czasu odczytuje dane z czujników i dodaje je do wykresów oraz opisów przycisków (listing 11).

List. 11. Timer wykonujący pomiar co 10 minut

		Timer tim = new Timer();
		tim.schedule(new TimerTask() {
			
			@Override
			public void run() {
				float temperature = temperatureSensor.getValue();
				float pressure = pressureSensor.getValue();
				float humidity = humiditySensor.getValue();
				float light = lightSensor.getValue();
				
				temperatureChart.addPoint(temperature);
				pressureChart.addPoint(pressure);
				humidityChart.addPoint(humidity);
				lightChart.addPoint(light);
				
				temperatureButton.setText("T: "+(int)temperature);
				pressureButton.setText("P: "+ (int)pressure);
				humidityButton.setText("H: "+ (int)humidity);
				lightButton.setText("L: "+(int)light);
			}
		}, 0, 600000);
	} 

Obsługa czujników (w języku C)

Po zbudowaniu projektu dla platformy trzeba jeszcze dodać brakujące funkcje C. Z uwagi na to, że magistrala I2C jest już używana na płytce STM32F429I-DISCO, jej konfiguracja jest wykonywana przy starcie systemu. Wystarczy więc zaimplementować funkcje odczytu i zapisu wywoływane z poziomu aplikacji. Obie funkcje potrzebują 7-bitowego adresu urządzenia na magistrali I2C oraz adresu rejestru do zapisu lub odczytu. Dane przekazywane do funkcji zapisu lub zwracane przez funkcję odczytu są w postaci jednej liczby 32-bitowej. Z tego powodu liczba bajtów przekazywana jako ostatni argument ograniczona jest do maksymalnie czterech.

Podłączenie czujników

Wszystkie czujniki znajdują się na magistrali I2C3 zajmującej piny PA8 (SCL) oraz PC9 (SDA). Nie stanowi to żadnego problemu, ponieważ każde z urządzeń ma unikalny 7-bitowy adres.

W przykładzie wykorzystano gotowe płytki z czujnikami:

Podłączając czujniki należy zwrócić uwagę na wartości rezystorów podciągających obie linie (SDA i SCL) do zasilania. Na płytce STM32F429I-DISCO rezystory są już podłączone, więc dołączenie równolegle kolejnych może uniemożliwić operacje na magistrali. Z tego powodu z płytki KamodTEM przed podłączeniem należy wylutować rezystory R1 i R2, a na płytce Luminosity Sensor Breakout trzeba usunąć zwarcie na polu PU. Pozostałe płytki zawierają translatory napięcia, zatem rezystory po stronie czujnika nie przeszkadzają w poprawnej pracy magistrali. Pozostaje więc już tylko zaprogramowanie mikrokontrolera i rozpoczęcie pomiarów.