Die Backend-Schnittstelle für die Finanzverwaltung (Teil 2)

Im ersten Teil habe ich das initiale Modell und die ersten Anforderungen der Anwendung beschrieben. Nun geht es um die Backend-Schnittstelle, die die beschriebenen Anwendungsfälle ermöglicht.

Das Projekt für die Anwendung

Als Basis für – insbesondere das Backend – dient Spring Boot. Mit Hilfe einer unterstützenden Entwicklungsumgebung oder auf der Startseite ist das Projekt schnell konfiguriert.

Ich ergänze das Projekt mit den Startern bzw. Abhängigkeiten

  • Spring Web,
  • Lombok,
  • Spring Boot DevTools,
  • Spring Data JPA und
  • H2

Für die (REST-)Schnittstelle benötige ich den Web-Starter. Mit Lombok spare ich mir den Standardcode (Getter, Setter, …). Die DevTools unterstützen die Entwicklung. Beispielsweise startet der Server nach Änderungen automatisch neu, damit die Änderungen wirksam werden.

Die übrigen Starter sind Grundlage für eine erste konkrete Implementierung der beschriebenen Anwendungsfälle.

Entwicklungshilfe

Für die Entwicklung und den Test füge ich Swagger hinzu. Die pom.xml erhält die Abhängigkeiten

<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger2</artifactId>
  <version>${springfox.swagger2.version}</version>
</dependency>

<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger-ui</artifactId>
  <version>${springfox.swagger2.version}</version>
</dependency>		

In den Property setze ich die aktuelle Version (aktuell 2.9.2) ein.

Es fehlt noch die Konfiguration, dann ist die Test-Oberfläche unter http://localhost:8080/swagger-ui.html verfügbar. 

@Configuration
@EnableSwagger2
public class SwaggerConfig {

	@Bean
    public Docket api() { 
        return new Docket(DocumentationType.SWAGGER_2)  
          .select()                                  
          .apis(RequestHandlerSelectors.any())              
          .paths(PathSelectors.any())                          
          .build();                                           
    }
}

Die Backend-Schnittstelle

Domain Modell

Zunächst lege ich das Domain-Modell in einem eigenen Package an. Die Umsetzung entspricht dem Diagramm aus dem vorhergehenden Beitrag.

@Data
public class Buchung {

	private String verwendung;
	private String empfaenger;
	private List<Umsatz> umsaetze = new ArrayList<>();
	
	public void addUmsatz(Umsatz umsatz) {
		getUmsaetze().add(umsatz);
	}
}
@Data
public class Umsatz {

	private LocalDate valuta;
	private BigDecimal betrag;
	private Konto konto;
	private Buchung buchung;
}

Das Konto enthält die Operation verbuche(.), die einen Umsatz zum Konto aufnimmt. Dabei übernimmt sie den Umsatz in die Liste der zum Konto gehörenden Umsätze und addiert gleichzeitig den Betrag aus dem Umsatz zum Saldo. Da Soll- und Haben-Beträge über das Vorzeichen unterschieden sind, ist keine weitere Logik notwendig.

@Data
public class Konto {

	private String name;
	private BigDecimal saldo;
	private List<Umsatz> umsaetze = new ArrayList<>();

	public void verbuche(Umsatz umsatz) {
		umsaetze.add(umsatz);
		saldo = saldo.add(umsatz.getBetrag());
	}
}

Die REST-Schnittstelle der Anwendung

Die Backend-Schnittstelle ist eine REST-Schnittstelle und entsteht in Spring durch eine Controller-Klasse. Dank der Spring Boot ist die Umsetzung einfach. Wer sich dabei unsicher ist, findet in den Spring Boot Guides schnelle Anleitungen.

Die BuchhaltungController-Klasse setzt dabei die im vorausgegangenen Beitrag definierten drei Anwendungsfälle um:

  • Buchen erstellen
  • Konten abfragen
  • Umsatz anzeigen

Die Umsetzung ist noch nicht final, denn beispielsweise ist die Abfrage der Buchungen je Konto ohne Paging nicht sinnvoll. Die Liste wird nach einiger Zeit einfach zu lang, was zu unnötigen Verzögerungen beim Laden, Übertragen und Darstellen in der Oberfläche führt. Auch mag ich das Konto lieber über eine technische Id abfragen, um Problemen mit Leerzeichen oder Sonderzeichen vorzubeugen.

@RestController
@RequestMapping("api/buchhaltung")
public class BuchhaltungController {

	@GetMapping("konto")
	public List<Konto> findAllKonten() {
	    throw new UnsupportedOperationException();
	}
	
	@GetMapping("konto/{kontoName}/umsatz")
	public List<Buchung> findKontoBuchungenByKontoName(
                @PathVariable("kontoName") String kontoName) {
	    throw new UnsupportedOperationException();
	}

	@PostMapping("buche")
	public ResponseEntity<String> buche(
                @RequestBody Buchung buchung) {
	    throw new UnsupportedOperationException();
	}
}

Eigene Klassen für die Datenübertragung in der Backend-Schnittstelle

Für die Datenübertragung in der Schnittstelle möchte ich eigene Transfer-Klassen einsetzten. Die Services sollen mit Domain-Objekten arbeiten. Deren Klassen möchte ich allerdings frei von Schnittstellen spezifischen Abhängigkeiten halten, und die wären für die Umwandlung in JSON erforderlich. Insbesondere sind die Relationen bidirektional implementiert, was ohne Zutun wegen zu endlosen Rekursionen führen würde. Die API-Klassen haben das folgende Aussehen:

@Data
public class Buchung {
	private String verwendung;
	private String empfaenger;
	private List<Umsatz> umsaetze = new ArrayList<>();
}

@Data
public class Umsatz {
	private LocalDate valuta;
	private BigDecimal betrag;
	private Konto konto;
}

@Data
@JsonInclude(Include.NON_NULL)
public class Konto {
	private String name;
	private BigDecimal saldo;
}

Im Konto sorgt die JsonInclude-Annation dafür, dass nur Felder in das JSON geschrieben werden, die auch tatsächlich Werte enthalten.

Die Übertragung erfolgt in Mapper-Klassen.

@Component
public class KontoMapper {
	public List<Konto> domainKontoListToApiKontoList(
                List<buchhaltung.domain.Konto> domainKonten) {
	    return domainKonten.stream()
		.map( domainKonto ->
                        domainKontoToApiKonto(domainKonto) )
				.collect(Collectors.toList());
	}

	public Konto domainKontoToApiKonto(
            buchhaltung.domain.Konto domainKonto) {
		Konto apiKonto = new Konto();
		apiKonto.setName(domainKonto.getName());
		apiKonto.setSaldo(domainKonto.getSaldo());
		return apiKonto;
	}
}

Der Konto-Mapper steht als Beispiel für das Codierungsmuster. Die Formalität sollte helfen, diese Transformation später ggf. durch Hilfsbibliotheken zu automatisieren, beispielsweise durch MapStruct.

Das folgende Bild zeigt in der Übersicht die Struktur der Schnittstelle.

Backend-Schnittstelle
Rest-Schnittstelle mit Mapper

Der Abschluss

Ergänzt um Service-Klassen mit Methoden-Signaturen entsteht als Schnittstelle der folgende Controller:

@RestController
@RequestMapping("api/buchhaltung")
public class BuchhaltungController {
	private KontoService kontoService;
	private BuchungService buchungService;
	private KontoMapper kontoMapper;
	private BuchungMapper buchungMapper;

	public BuchhaltungController(
			KontoService kontoService, 
			BuchungService buchungService, 
			KontoMapper kontoMapper,
			BuchungMapper buchungMapper) {

		this.kontoService = kontoService;
		this.buchungService = buchungService;
		this.kontoMapper = kontoMapper;
		this.buchungMapper = buchungMapper;
	}

	@GetMapping("konto")
	public List<Konto> findAllKonten() {
		return kontoMapper.domainKontoListToApiKontoList(
				kontoService.findAll());
	}
	
	@GetMapping("konto/{kontoName}/umsatz")
	public List<Buchung> findKontoBuchungenByKontoName(
           @PathVariable("kontoName") String kontoName) {
	    return buchungMapper
             .domainBuchungListToApiBuchungListe(
                buchungService
                  .findBuchungByKontoName(kontoName));
	}

	@PostMapping("buche")
	public ResponseEntity<String> buche(
                @RequestBody Buchung buchung) {
	  try {
            buchungService.buche(
              buchungMapper.apiBuchungToDomainBuchung(buchung));
	  } catch (Exception e) {
	    return ResponseEntity
                .unprocessableEntity()
                .body("Fehler: " + e.getMessage());
	  }
	  return ResponseEntity.accepted().body("Verarbeitet");
	}
}

finally

Die Schnittstelle ist damit beschrieben und es steht die eigentliche Implementierung an. Mein erster Wurf wird auf einer relationalen Datenbank zur Speicherung basieren. Für die Anbindung auf der grünen Wiese, verwende ich die Java Persistente API (JPA). Doch dazu beim nächsten mal mehr.

Happy Coding!

Anwendung entwickeln für zu Hause – eine Finanzverwaltung

Mein Quicken ist in die Jahre gekommen. Mag ich das aber wirklich erneuern, schließlich arbeite ich als Software-Entwickler? Da reift die Überlegung eine Anwendung selbst zu entwickeln.

Die Anforderungen der Anwendung

Bevor ein erster Prototyp entsteht, gilt es erst mal einfache Anforderungen zusammen zu tragen.

Was sollte ein erster Prototyp können? Eine einfache Tabelle mit Datum, Beschreibung und Betrag reicht nicht. Das wäre praktisch ein Kassenbuch. Ich habe dann doch mehr als ein Konto und – Hand aufs Herz – ein Kassenbuch geht mit jeder Tabellenkalkulation.
Die doppelte Buchführung sollte es schon sein. Das ist es in Quicken auch, die Gegenkonten heißen dort halt nur Kategorie. Mehrteilige Buchungen sollten auch möglich sein. Vom Giro-Konto buchen beispielsweise die Stadtwerke ab, der Betrag setzt sich dabei aus den Strom- und den Gas-Kosten zusammen. Wollte ich einen Vergleich dieser Kosten für alternative Anbieter machen, müßte ich das schon trennen können.

Die Modellierung ist nicht neu. Was auf der Arbeit funktioniert, gilt auch für die Software zu Hause: das Rad nicht neu erfinden. Zumindest ist das Datenmodell schon da – zumindest im Prinzip. Martin Fowler hat sich schon vor einiger Zeit Gedanken dazu gemacht: https://martinfowler.com/eaaDev/AccountingNarrative.html

Das Datenmodell

Die erste Wurf für das Modell der Anwendung sieht wie folgt aus. Das ist eine eingedeutschte Variante des Fowler-Vorschlages.

Klassenmodell
Klassenmodell der Anwendung

Die gute doppelte Buchführung: eine Buchung ist immer Soll an Haben oder als Text-Format: “Per Soll-Konto, Betrag an Haben-Konto, Betrag”, wobei es jeweils mehrer Soll- und Haben-Konten geben kann. Die Summe der Soll und Haben-Beträge muss 0 sein. Soll-Beträge sind positiv und Haben-Beträge negativ. Das entspricht dem gewohnten Gefüge, dass Guthaben positiv sind und Schulden negativ.

Die Buchung fasst die (Konto-)Umsätze zusammen. Jeder Umsatz kennt seine Buchung – kann ohne diese auch gar nicht existieren – und er kennt das Konto, welches er anspricht.

Der Saldo des Kontos ergibt sich formal als die Summe der Beträge aller Umsätze des Kontos. Fraglich ist, ob der immer aufs Neue berechnet wird oder der letzte Stand gespeichert ist. Im Unternehmen wäre die Antwort das speichern des Saldos, da die Berechnung auf sehr viele Umsätze zu lange dauert und im Verlauf der Zeit immer länger. Ich lasses es dabei.

Erste Anwendungsfälle der Anwendung

Die folgenden Anwendungsfälle mag ist initial umsetzten:

  • Buchen erstellen
    Das ist praktisch das Aufbauen des Hauptbuches. Initial mag ich ein abgesprochenes Konto automatisch anlegen, wenn es noch nicht existiert. Das spart erst mal einen Anwendungsfall “Konto anlegen”.
  • Konten abfragen
    Ich bekomme eine Liste aller existierenden Konten mit deren aktuellen Salden. Das ist praktisch das Grundbuch.
  • Umsatz anzeigen
    Zuletzt mag ich noch die Umsätze eines Kontos ansehen können. Hier sind die Details sichtbar, um zum Beispiel Ausgaben zu vergleichen oder Positionen zu kontrollieren.

Die technische Basis – ein erster Ansatz

Finanzdaten mag ich nicht unnötig offen haben, also ist der private Rechner meine Zielplattform. Was liegt näher als das für eine kaufmännische Anwendung gewohnte Setup zu wählen.

Auf einem Rechner einen Browser für die Anwendung? Warum nicht, denn was ist mit HTML, CSS und Javascript nicht machbar. Diesen Ansatz verfolgt beispielsweise das Project Electronjs. Hier kommt vollständig Javascript zum Einsatz und es entsteht eine ausführbare Binärdatei, die für unterschiedliche Betriebssysteme erstellt werden kann.

Nun bin ich in – gerade was die Fachlogik angeht – mehr in der Java-Welt zu Hause. Dank der Graal VM ist auch hier mal eine kompilierte Version denkbar und die Entwicklung geht schneller von der Hand.

Aus der täglichen Gewohnheit mag ich mit den Mitteln arbeiten, die ich bereits gut kenne. Das bedeutet: relationale Datenbank, Spring Boot als Backend und Angular für die Oberfläche.

Natürlich bleibt das ganze zu überdenken. Für den Zugang ist ein Browser mit der entsprechenden URL zu starten. Das ist allerdings lösbar.

Für die Datenbank bietet sich eine eingebundene (embedded) Variante an, um zusätzliche Installationen zu vermeiden, z.B. H2.
Da das Datenmodell auf der “grünen Wiese” entsteht, möchte ich als Anbindung an die Datenbank die Java Persistente API (JPA) nutzen, da JPA das Schema direkt erstellen kann und somit Arbeit spart.

Die nächsten Schritte zur Anwendung

Im nächsten Beitrag beschreibe ich zunächst die Schnittstelle des Backends, welches die umzusetzenden Anwendungsfälle darstellt.

Dann schau ich mir die zugehörige Implementierung an. Da steckt etwas Forschung drin. Denn diese Anwendung ist nicht für ein großes Finanzunternehmen gedacht. Im Sinne des KISS-Prinzips gibt es vielleicht Alternativen.

Bis dahin: Happy coding!

Die Uhrzeit im Griff mit Timer und Unterbrechung

Die Anzeige ist aufgebaut, jetzt fehlt noch die Software mit dem richtigen Timing und die Uhr läuft. Damit das funktioniert gilt es nun die im Mikrocontroller enthaltenen Timer nutzbar zu machen.

Warum nicht wie im Testprogramm?

Ja, das Testprogramm hat die Anzeige gesteuert. Allerdings zeitlich nicht sehr präzise. Ein genauerer Blick auf das Blink-Programm macht den Haken jedoch deutlich.

void loop() {
  digitalWrite(13, HIGH);
  delay(1000);
  digitalWrite(13, LOW);
  delay(1000);
}

Das Programm schaltet den Ausgang ein, wartet eine Sekunde, schaltet den Ausgang aus und wartet wieder. Anschließend beginnt der Zyklus von vorne. Die Verzögerungen mögen exakt eine Sekunde dauern, aber wie lange dauert das Schalten? Schon die Zahl der Kommandos kann durch bedingte Ausführungen, beispielsweise einen Übertrag auf die nächste Stelle unterschiedlich sein.  Folglich ist die Laufzeit unterschiedlich. Das Multiplex braucht nur ausreichend schnell sein, so dass die Anzeige nicht flimmert. Für eine genaue Uhr taugt das allerdings nicht.

Die Zeit im Griff durch Unterbrechung

Was meint das? Im Mikrocontroller des Arduino sind drei sogenannte Timer/Counter integriert. Im Grunde handelt es sich um Zähler. Sie zählen Ereignisse wie den Systemtakt und führen eine gegeben Funktion aus, wenn ein definierter Wert oder die Zählerobergrenze erreicht ist. Das Ausführen startet der Timer mit Hilfe einer Unterbrechung – einem Interrupt. Der Interrupt unterbricht das laufende Programm und ruft den Unterbrechungscode auf. Ein Beispiel dafür ist der Reset, der durch einen Taster auf dem Arduino-Board ausgelöst werden kann. Der Mikrocontroller beginnt dann mit der Ausführung seines Programmes von ganz vorne.

Die Timer zählen vollständig unabhängig vom laufenden Programm. Zählen sie nun den Systemtakt,  bilden sie eine gute Basis für eine Uhr. Der ist immerhin quarzgenau bei 16000000 Impulse in der Sekunde.

Was gibt es für Timer?

Das hängt vom zugrundeliegen Controller ab. Auf dem Arduino stehen die beiden 8-Bit-Timer/Counter 0 und 2 sowie der 16-Bit-Timer/Counter 1 zur Verfügung.

Die 8-Bit Zähler können bis 255 Zählen, Timer 1 schafft es immerhin bis 65535. Anschließend beginnen sie wieder bei 0. Wenn sie von vorn beginnen, erfährt der Zähler einen Überlauf. Den kann er durch einen Interrupt markieren. Genauso ist ein Interrupt konfigurierbar, sobald ein bestimmter Wert erreicht ist.

 

Timer 2 könnte auch externe Ereignisse zählen. An den dafür vorgesehenen Eingängen betreibt der Arduino allerdings seinen Quarz. Eine mögliche Taktquelle für die Timer ist wie gesagt der Systemtakt. Die 16 MHz sind recht schnell für den Zahlenumfang der Timer und die Zähler laufen in Bruchteilen einer Sekunde über. Für längere Distanzen kann der Takt vor dem Zähler vorgeeilt werden. Als Vorteile sind die 8/32/64/128/256 und 1024 möglich.

Die Programmierung

Die Timer besitzen diverse Kontrollregister. Die Register sind Adressen im Programmraum. Die Programmierung erfolgt durch setzen bestimmter Steuer-Bits dieser Register. Die Arduino IDE basiert letztlich auf dem C++ für die Atmel-Prozessoren. Die Header definieren Konstanten für die Adressen und die Steuerbits, mit denen die Programmierung einbänglicher ist.

Die benötigten Register sind im Einzelnen

  • OCRxy
    Output Compare Register. Hier können zwei Vergleichswerte hinterlegt werden, bei deren Erreichen ein TIMERx_COMPy_vect-Interrupt ausgelöst wird. x ist die Nummer des Timers (0,1, 2). y steht für die beiden möglichen Werte (A und B)
  • TCCRxy
    Timer/Counter Control Register: Einstellen des Vorteile
  • TIMSKx
    Timer/Counter Interrupt Mark Register: welche Interrupts soll ein Timer aus auslösen

Weiterhin sind noch die Funktionen cli() – Interrupts deaktivieren – und das Gegenstück sei() – Interrupt aktivieren wichtig. Während der Timerprogrammierung sollten die Interrupts deaktiviert sein, damit die Programmierung nicht unterbrochen wird und ein bisher erfolgte Programmierung ein ungewolltes Verhalten hinterlässt.

Der auszuführende Code für einen Interrupt ist in einer Callback-Funktion zu hinterlegen:

ISR(Interruptname) {
   // auszuführender Code
}

Damit die Interrupts sich nicht „selbst überholen“ sollte der Code natürlich möglichst kurz sein.

Eine Übungsprogramm

Um mich mit der Programmierung vertraut zu machen, schadet ein einfaches Beispielsprogramm nicht. Ich mag das Blink-Programm dafür mit Hilfe eines Timers umsetzen. Der dabei entstandene Code findet sich auch auf Github.

Natürlich ist bei diesem Programm ein abweichender Setup-Code erforderlich. Die eigentliche Aktivität erfolgt allerdings in der Interrupt-Behandlung (ISR(.)) infolge dessen ist die Loop-Prozedur leer. Der Arduino wartet. Ist die vorgegebene Zeit des Timers um, schaltet die Interrupt-Behandlung den Ausgang der Onboard-LED um.

Bevor ich kurz auf die Timereinstellungen eingehe, möchte ich noch kurz auf eine Besonderheit bei der Deklaration der Zustandsvariable für den Status des Ausgangs hinweisen:

volatile boolean blink = false;

Der Compiler versucht zu optimieren. Stellt er nun fest, dass eine Variable in einem Codeblock nicht geschrieben wird, könnte er sie infolgedessen durch eine Konstante ersetzen. Soll also eine Variable  in einem Block festgelegt und in einem anderen ausgelesen werden, müssen das dem Compiler mitteilen. Genau dafür ist die Variable blink als volatile deklariert.

Kommen wir nun zu den Einstellungen. Ich möchte die delay(.)-Anweisungen durch die Interrupt-behandlung ersetzen. Diese schaltet die Leuchtdiode ein und vice versa. Der Aufruf sollte entsprechend einmal in der Sekunde erfolgen.

Dafür wähle ich den 16-Bit-Timer. Er erhält den Systemtakt geteilt durch 1014. Damit ist die Eingangsfrequenz 16.000.000 / 1014 = 15.625 Hz. Zählt der Timer nun bis 15624 und startet mit dem nächsten Takt den Interrupt, ist folglich der Aufruf je Sekunde erreicht.

Die Details zu den Steuerregistern

Die Bits der Register sind ausführlich im Datenblatt von Atmel beschrieben. Damit der Timer bei erreichen des Zielwertes zurückgesetzt wird, ist in das TCCR1B Register die WGM-Bits entsprechend zu setzen WGM12 auf 1 die beiden anderen auf 0. Sie befinden sich im TCCR1A-Register.

Der Precaller wird über die CS-Bits gesteuert, die ebenfalls im TCCR1B-Register zu finden sind. Daraus ergeben sich die Zeilen

TCCR1A = 0;
TCCR1B = 0;
// Set CS12 and CS10 bits for 1:1024 prescaler
TCCR1B |= (1 << CS12) | (1 << CS10);
// turn on CTC mode
TCCR1B |= (1 << WGM12);

Das Vergleichsregister A ist mit den oben genannten 15624 zu füllen (es könnte genauso gut B Verwendung finden)

OCR1A = 15624;

Zuletzt muss der Vergleichseinheit A gesagt werden, den Interrupt bei erreichen zu aktivieren

TIMSK1 |= (1 << OCIE1A);

Vor der Programmierung des Timers sind Interrupts zu deaktivieren und anschließend wieder zu aktivieren.

Resumee

Das Blink-Programm auf Timer Basis zeigt einen sehr schönen Weg zur Steuerung der Digitaluhr auf. Die Zeitmessung damit ist quarzgenau. Die Programmierung kann durch diese “Event”-Behandlung auch etwas leichter werden.

Einen Zeitgeber für eine Sekunde ist nun schon gefunden. Der nächste Beitrag betreibt die Anzeige und Zählt dann die Sekunden.

Bisherige Beiträge zum Winterprojekt

 

Winterprojekt: Ansteuern der 7-Segment-Anzeige

Mit der nun gelieferten 7-Segment-Anzeige kann das die Programmierung und die Ansteuerung durch den Arduino erfolgen. Zunächst bin ich auf Draht …

Der Anschluss

Was steckt in der 7-Segment-Anzeige?

Die 7-Segment-Anzeig hat gleich eine Überraschung bereit: sie hat eine gemeinsame Anode.

schaltplan-7-segmentDie einzelne LED des vorausgehenden Blogs wurde durch einen aktiven Ausgang (high, 5 Volt) aktiviert. Für die Segment-Anzeige dagegen heisst es umdenken. Nur die Kathoden der einzelnen Dioden sind zugänglich, folglich kann der Arduino sie nur durch ein Low-Pegel, 0 Volt, zum leuchten bringen. Das gilt es anschließend bei der Programmierung zu berücksichtigen.
Anschlussbelegung 7-Segment-AnzeigeDer Schaltplan der Anzeige enthält die Nummern der Pins für das jeweilige Segment. Zudem bezeichnen die Buchstaben “a” bis “f” die jeweiligen Dioden und den Dezimalpunkt. Anhand der Beschreibung aus dem Datenblatt können nun die Verbindungen zum Arduino hergestellt werden. Zwischen jedem Digitalausgang und dem Anschluß habe ich zudem ein Widerstand geschaltet.

Die Verdrahtung

Prinzipiell könnte auch ein einzelner Widerstand an der gemeinsamen Anode genutzt werden. Hierdurch wird allerdings der Strom für alle Dioden begrenzt. Wenn für eine Ziffer 8 alle Segmente leuchten, teilt er sich auf und ist folglich je Diode niedriger als bei weniger leuchten Segmenten. Die Helligkeit könnte dadurch abhängig von der dargestellten Ziffer schwanken. Ich sag mal könnte, weil die Helligkeit durch den Strom nur schlecht steuerbar ist. Beim Ausprobieren habe ich keinen Unterschied sehen können. Durch Materialstreuung könnte es aber dennoch passieren und sähe dann unschön aus.

Die Ansteuerung der sieben Segmente erfordern ebenfalls sieben Digitalausgänge. Ich habe mich dazu entschieden die Ausgänge 13 – Segment “a” bis 7 – Segment “g” zu nutzen. Schließlich besitzen die Digital-Pins 1 und 2 Sonderfunktionen, die ich nicht unnötig blockieren möchte.

Die Software

Das Rahmenprogramm

In der Initialisierung der Ansteuerung definiere ich in einer Schleife die Digitalen Ausgänge und setze sie in einen definierten Zustand.

void setup() {
  for(int i=13; i>6; i--) {
    pinMode(i, OUTPUT);
    digitalWrite(i, HIGH); // Erst mal ausschalten
  }
}

Für den Test soll die 7-Segment-Anzeige die Ziffern 0 bis 9 anzeigen. Jedoch erfolgt nach jeder Ausgabe eine Verzögerung, sonst könnten den Ziffernwechsel wegen der hohen Geschwindigkeit nicht wahrnehmen.

void loop() {
  for(int i=0; i<10; i++) {
    ziffer(i);
    delay(1000);
  }
}

Nachdem die Schleife die Ziffern durchlaufen hat, lässt die übergeordnete Endlosschleife die Ziffern erneut anzeigen. Der spannendere Teil der Ansteuerung liegt darin, wie die Ziffern der 7-Segement-Anzeige auszugeben sind. Dafür ist Prozedur ziffer(int) zuständig.

Ansteuerung der 7-Segment-Anzeige

Ein erster Ansatz

Der erste Gedanke zur Umsetzung ist eine direkte Aufteilung nach anzuzeigender Ziffer

void ziffer(int z) {
  switch(z) {
  case 0:
   ziffer0();
   break;
  case 1:...

und eine zugehörige Aktivieren der Ausgänge. Für die Ziffer 0 auf der 7-Segement-Anzeige sieht das beispielsweise wie folgt aus.

void ziffer0() {
  digitalWrite(A, LOW); 
  digitalWrite(B, LOW);
  digitalWrite(C, LOW);
  digitalWrite(D, LOW);
  digitalWrite(E, LOW);
  digitalWrite(F, LOW);
  digitalWrite(G, HIGH);
}

Das Positive vorweg: es funktioniert. Das geht aber auch anders und besser. Zum einen ist es einfach toll, wenn sich eine Funktion “knackig” ausdrücken lässt. Ein kompakterer Code ist im Folgenden auch besser zu pflegen. Als Nebeneffekt kann aber Speicherersparnis dabei herauskommen. Zur Erinnerung: der Arduino hat 2K freien Speicher und 32K Programmspeicher. Das ist in dieser Baureihe sogar recht viel, denn die kleinen Brüder des verwendeten Chips besitzen nur 16K oder gar nur 8K Programmspeicher. Wenn ein Programm dort hinein passt, wird eine Serie halt auch preiswerter.

Vermeiden von doppelten Code

Was geht also besser? Für jede Ziffer auf der 7-Segment-Anzeige gibt es eine eigene Prozedur, wie die oben dargestellte. Mit Ausnahme der Pegel sieht jedoch jede dieser Prozedur gleich aus. Das ist eine unnötige Wiederholung, die zudem der Faulheit einer Entwicklers widerspricht. Soll nun an der Grundstruktur etwas geändert werden, ist danach jede der anderen Prozeduren anzupassen. Immerhin zehn Prozeduren nach dem gleichen Schema ändern.

Die Alternative heisst ein Feld definieren. Darin stehen nur die Pegel und eine Schleife weist sie jeweiligen Ausgang zu:

const int ZIFFERN[][] = {
          { LOW, LOW, LOW, LOW, LOW, LOW, HIGH },
          { ... 

        }

...

void ziffer(int z) {;
  for(int i=0; i<7; i++) { 
    digitalWrite(13-i, ZIFFERN[z][i]);
  }
}

Meine ursprüngliche Lösung belegt bei mir 1296 Bytes Programm- und 9 Bytes dynamischen Speicher. Die neue dagegen 1128 Bytes Programm- und 149 Bytes dynamischen Speicher. Die “schönere” Lösung ist kleiner benötigt trotzdem viel Platz für das Feld mit den Pegeln, und zwar vom ohnehin kleinen dynamischen Speicher.

Speicheroptimierung

Die Ansteuerung der 7-Segment-Anzeige kann nun noch über eine kompaktere Speicherung der Pegel optimiert werden. Für die Ausgabe ist eigentlich nur interessant, ob er an oder aus geschaltet wird. Als Datentyp habe ich zuvor int gewählt. Dieser Datentyp benötigt deutlich mehr Platz als nur ein Bit. Die sieben Pegel für die 7-Segement-Anzeige finden aber auch als Bit in einem Byte platz. Die Pegel lassen sich dann beispielsweise wir folgt ablegen:

const byte ZIFFERN[] = {
    B0000001, // 0
    B1001111, // 1
    B0010010, // 2
    B0000110, // 3
    B1001100, // 4
    B0100100, // 5
    B0100000, // 6
    B0001111, // 7
    B0000000, // 8
    B0000100, // 9
  };

Für den westlichen Menschen ist eine Kodierung der Segmente von links nach rechts nachvollziehbarer. Entsprechend steht das erste Bit nach dem B für Segment “a”, das zweite für “b”, etc.

Bitte ein Bit

Danach muss die  Prozedur das richtige Bit für den zugehörigen Ausgang setzen. Meine Lösung:

void ziffer(int z) {;
  for(int i=0; i<7; i++) {
    digitalWrite(13-i, (ZIFFERN[z] & 1 << (6-i))==0?LOW:HIGH);
  }
}

Hier spiele ich mit Bit-Operationen. Das ist allerdings nicht auf einen Blick zu lesen. Mit ZIFFERN[z] bekomme ich das Bytes für die gewünschte Ziffer. Die Laufvariable i zählt durch die sieben möglichen Segmente. Für das Segment “c” hat die Variable i den Wert 2. Das ist das dritte Segment, aber die Schleife beginnt mit 0 und nicht mit 1. Mit 1 << (6-i) erhalte ich ein Byte, das nur an der 6-2=4-ten Stelle eine 1 hat. Bei sieben Bits ist das die dritte Stelle von Links, sie soll in den Konstanten die “c”-Stelle darstellen. Die Vorgabe und das Zielsegment verknüpfe ich mit einem logischen “Und”. Das Ergebnis für die jeweiligen Eingangsdaten ist dann

Ziffern-Bit Masken-Bit Ergebnis
0 0 0
0 1 0
1 0 0
1 1 1

Ist also für Segment “c” eine 1 codiert, so sorgt die entstandene Maske B10000 für ein Byte, das ebenfalls eine 1 enthält. Dessen Zahlenwert ist größer als 0. Sonst ist der Wert des Bytes 0, alle Bits auf 0. Diesen Wert prüft nun der ?-Operator und setzt den jeweiligen entsprechend Ausgang auf LOW oder HIGH.

Tipp

Die Programmierung klappt nicht immer auf anhieb. Beispielsweise ist das letzte Bit der Maske auf der Stelle 0, folglich ist der intuitive Ansatz (7-i) für die Maske falsch. Es gibt einen einfachen Weg, um das zu prüfen: einfach Zwischenwerte auf die Serielle Schnittstelle zum Rechner ausgeben lassen. Dazu ist im Setup die Schnittstelle mit Serial.beginn(9600); zu initialisieren. Ausgaben erfolgen dann mit Serial.println(<wert>); Den seriellen Monitor (die Lupe oben links im Fenster) in der IDE starten, die bei der Initialisierung angegebene Geschwindigkeit (9600 Baud) auswählen und schon können wir dem Programm bei der Arbeit zusehen.

Zum Abschluss

Die für die einzelne 7-Segment-Anzeige gefundene Lösung belegt 1000 Bytes Programm- und 19 Bytes dynamischen Speicher. Offensichtlich lohnt es sich Gedanken über seine Programme zu machen. Wenn die Idee dann auch noch funktioniert … Film der zählenden Anzeige

Bisherige Beiträge

Tutorial – JavaFX Custom Dialog

JavaFX enthält ab Version 8 auch Dialogklassen. Dazu gehören die vorgefertigten Standarddialoge Alert (Warnung), TextInputDialog (Texteingabe) und ChoiceDialog (Auswahl). Über deren Verwendung ist im Internet auch einiges zu finden. Das Erstellen eigener Dialoge wird dagegen kaum beschrieben. Am Beispiel eines Login-Dialogs soll dieser Blog das ändern. Der Beispielcode erstellt die Dialogoberfläche einmal programmatisch und einmal per FXML. Der Beispielcode ist auf Github  zu finden.

Das Basisvorgehen in JavaFX

Für eigene Dialoge bietet sich ein Blick auf die Struktur der vorgefertigten Dialoge an

Klassenstruktur der Dialoge
Klassenstruktur der Java FX Dialoge und der darstellenden DialogPane

Am einfachsten leitet sich der eigene Dialog – wie bei den vorhandenen Dialogen – von der Klasse javafx.scene.control.Dialog<R> ab. <R> ist der Rückgabetyp. Für die Darstellung der Oberfläche inklusive der Buttons ist die DialogPane-Eigenschaft des eigenen Dialogs zu setzen oder zu bestücken. Abschließend benötigt der eigene Dialog noch einen Konverter, um die relvanten Daten aus den Eingabefeldern in ein Rückgabeobjekt zu transferieren.

Der Aufruf einer eigenen Dialogklasse ist dann z.B.

CodedLoginDialog codedDialog = new CodedLoginDialog();
Optional result = codedDialog.showAndWait();
result.ifPresent(login -> System.out.println(login));

Bei Abbruch des Dialogs enthält das Optional keinen Wert, die Ausgabe findet entsprechend nicht statt. Die Javadoc zur Dialog-Klasse zeigt weitere Aufrufvarianten.

Erstellen der Dialogoberfläche

DieDialogPaneeines Dialogs bestimmt das Aussehen.

Dialoglayout
Standardlayout der DialogPane

Das wichtigste dürfte der Content sein. Im Bild ist ihm eine Grid mit Labeln und Eingabefeldern eines Logins zugewiesen.

Diverse optionale Ergänzungen sind möglich:

  • Zusätzlich zum Titel des Dialogs ist noch ein Headertext möglich.
  • Ein Bild/Icon (graphic-Eigenschaft) ist im Bild nicht dargestellt. Dessen Position hängt vom Headertext ab. Ist ein Headertext angegeben, erscheint ein Bild rechts vom Headertext, andernfalls links vom Content.
  • Ein expandierbarer Content sorgt wie im Bild für einen Button, der auf Anforderung den Inhalt des expandable Contents anzeigt.

Programmatische Oberfläche

In der eigenen Dialogklasse ist der Content der DialogPane zu befüllen

this.grid = new GridPane();
...

final DialogPane dialogPane = getDialogPane();
dialogPane.setContent(grid);

...

FXML-Oberfläche

Seit der Version 8 des SceneBuilder bietet dieser die Möglichkeit eine DialogPane grafisch zu erstellen und als FXML-File zu speichern.

Mit Hilfe des FXMLLoader erhält eigenen Dialog die gewünsche DialogPane:

DialogPane dialogPane =
    (DialogPane)FXMLLoader.load(
        getClass().getResource("/fxml/DialogPane.fxml"));

...

setDialogPane(dialogPane);

Buttons ergänzen

Die Buttons eines Dialoges speichert die DialogPane in der Eigenschaft Buttontypes. Es gibt diverse vordefinierte Buttons, die der Buttontype-Collection hinzugefügt werden können. Im Beispiel enthält der Dialog einen Abbruch und einen Ok-Button:

dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);

Obwohl in der DialogPane verborgen, sind die vordefinierten Button im Nachgang noch anpassbar, z.B. der Text des Ok-Buttons

Button okButton = (Button)dialogPane.lookupButton(ButtonType.OK);
    okButton.setText("Login");

Der Konverter

Nach dem Ok soll der Dialog nun auch die Eingaben verpackt in der Ergebnisklasse zurück liefern. Das ist Aufgabe des Konverters, der in eigenen Klassen entsprechend zu definieren ist

setResultConverter((dialogButton) -> {
    ButtonData data =
        dialogButton == null ? null : dialogButton.getButtonData();
    return data == ButtonData.OK_DONE
        ? new Login(usernameField.getText(), passwordField.getText())
        : null;
});

Ist im Beispiel der Dialog mit dem Ok-Button beendet worden, so legt das Lambda einen neues Ergebnisobjekt an und übergibt dem Konstruktur die Daten aus den Eingabefeldern.

Für die Konvertierung benötigt das Lambda referenzen auf die Eingabefelder. Bei der programmatischen Erstellung der Oberfläche sind diese schnell als Attribute der neuen Dialog-Klasse deklariert. Anders verhält es sich, wenn die Oberfläche per FXMLLoader erstellt ist. In diesem Fall muss die Dialogklasse die Referenzen im FXML-Objektbaum suchen. Die gewünschten Elemente können im FXML per Id gekennzeichnet werden

<TextField fx:id="usernameField"...

um anschließend nach dem Laden im Elementbaum der DialogPane gesucht zu werden

final TextField usernameField =
   (TextField)dialogPane.lookup("#usernameField");

Validierung

Die Eingaben eines Dialogs sollten natürlich nur weiter gegeben werden, wenn sie sinnvoll sind. Über Data-Binding könnten nun Hinweise, Markierungen, etc. in den Content eingeblendet werden. Aber auch der Ok-Button sollte nur bei vollständigen Eingaben überhaupt aktiv sein. In Beispiel muss mindestens ein User angegeben sein, bevor ein Login möglich ist.

Zuvor wurde der Text des Ok-Buttons angepasst. Aber auch die übrigen Buttoneigenschaften sind ansprechbar

okButton.setDisable(true);
usernameField.textProperty().addListener(
            (event, oldValue, newValue)
                -> okButton.setDisable(newValue.trim().isEmpty()));

Fazit

Solange das vorgegebene Layout der für die eigenen Zwecke ausreicht, sind eigene Dialoge seit Java 8 recht einfach zu erstellen.