Caliper, der Plan
Caliper ist eine Erweiterung für LVGL, die Oberflächen auf Controller-Boards vermessbar macht. Sie trennt körperliche Maße von räumlichen, hält die Gestaltungswerte an genau einer Stelle, gibt allem, was Text trägt, eine Mindestbreite statt einer festen, und prüft auf jedem Bau, ob das Ergebnis noch stimmt. Dieses Papier ist zugleich die Bauanleitung: es nennt die Typen, die Dateien, die Ordner und die Aufgaben, aus denen die Issues geschnitten werden. Das Bedienteil LEVEL ist der erste Nutzer und bleibt vorerst der einzige.
Inhalt
- Wozu es das gibt
- Was es ist und was LVGL bleibt
- Das Panel und die zwei Arten von Maß
- Die Werte und ihre eine Quelle
- Wie ein Bildschirm entsteht
- Die Komponenten
- Die Prüfungen
- Wie es gebaut und eingebunden wird
- Der Weg dahin
Anhang
1. Wozu es das gibt
Eine Oberfläche für ein Controller-Board wird heute für ein Display entworfen und lebt dann in dessen Zahlen. Beim Bedienteil LEVEL ist das nachweisbar: der Erzeuger seiner Entwürfe setzt 1716 Maße, und jedes einzelne davon steht in Bildpunkten dieses einen Panels. Ein zweites Display hieße, sie alle neu zu entscheiden.
Der naheliegende Ausweg, alles mit einem Faktor zu skalieren, ist falsch, und zwar messbar. Eine Fingerkuppe misst neun Millimeter. Auf dem Panel dieses Projekts sind das 66 Punkte, weil 800 auf 480 über fünf Zoll 7,35 Bildpunkte je Millimeter ergeben. Auf einem Sieben-Zoll-Panel mit 1024 auf 600 ist die Dichte geringer, nämlich 6,68, und dieselbe Fingerkuppe misst dort 60 Punkte. Ein Faktor aus der Breite sagte 84. Das sind vierzig Prozent daneben, auf jeder einzelnen Taste.
Drei weitere Dinge fallen in dieselbe Lücke. Ein Knopf mit fester Breite schneidet ein längeres Wort ab, sobald die Oberfläche eine zweite Sprache trägt. Die Gestaltungswerte stehen an zwei Orten, im Gerät und in den Dokumenten, die es beschreiben, und laufen lautlos auseinander. Und ob eine Fläche noch anzutippen und ein Text noch zu lesen ist, sieht niemand, bevor das Gerät auf dem Tisch steht.
Caliper beantwortet diese vier Fragen einmal, statt sie in jedem Projekt neu zu stellen.
2. Was es ist und was LVGL bleibt
Caliper ersetzt LVGL nicht und tritt auch nicht neben es. Es ist eine Erweiterung, fest an LVGL gebunden, und es registriert sich als dessen Thema. Damit trägt auch ein Bedienelement, das jemand unmittelbar von LVGL nimmt, dieselbe Gestaltung.
Die Bindung ist eine Entscheidung und kein Zufall. Was an einer Oberfläche wirklich Arbeit macht, hängt an LVGL: jedes Bedienelement ist ein LVGL-Objekt, Layout heißt dort Flex und Grid, die Breite eines Wortes kommt aus LVGLs Schriftmetrik, und die Prüfungen lesen den fertigen Objektbaum. Was ohne LVGL bliebe, sind ein paar Umrechnungen und eine Tabelle mit Zahlen. Eine Trennschicht dafür wäre eine Naht ohne Nutzen.
Geschrieben ist Caliper in C++, in der Fassung von 2020, und ruft LVGL als das C an, das es ist. Die Sprache ist hier kein Geschmack, sondern trägt genau den Punkt, um den es geht: die zwei Arten von Maß können verschiedene Typen sein. Ein Millimeter und ein Bildpunkt sind in C beide eine Zahl, und nichts hält jemanden davon ab, das eine für das andere einzusetzen. Als eigene Typen lässt der Übersetzer es nicht zu, und der Fehler, den dieses Projekt gerade auf einem Panel gefunden hat, kann gar nicht erst geschrieben werden.
Der zweite Gewinn ist, dass die Umrechnungen zur Übersetzungszeit stattfinden können. Wo das Panel schon beim Bauen feststeht, und das ist der Normalfall auf einem Gerät, kostet eine Fingerkuppe in Millimetern zur Laufzeit nichts; steht es erst beim Start fest, rechnet dieselbe Zeile dann. Gebaut wird ohne Ausnahmen, ohne Laufzeittypen und ohne Speicheranforderung nach dem Start, also in den Einstellungen, die ESP-IDF für C++ ohnehin voreinstellt.
| Aufgabe | Wer | Wie |
|---|---|---|
| Zeichnen, Teilbild-Auffrischung, Eingabe, Schriften | LVGL | unverändert übernommen |
| Layout aus Stapeln und Rastern | LVGL | Flex und Grid, von Caliper nur richtig eingestellt |
| Stile und Themen | LVGL | Caliper liefert ein Thema, keine zweite Stilwelt |
| Das Panel und seine Dichte | Caliper | eine Beschreibung, aus der jedes körperliche Maß folgt |
| Millimeter und Leseabstand als Einheit | Caliper | eigene Typen, Umrechnung über die Dichte |
| Die Gestaltungswerte | Caliper | eine Quelle, aus der der Header und die Dokumente entstehen |
| Mindestbreiten für alles, was Text trägt | Caliper | gemessen gegen jede Sprache des Baus |
| Die Komponenten einer Bediengeräteoberfläche | Caliper | status_bar, sidebar, List, Row, Card, keyboard und was Tabelle 5 sonst nennt |
| Die Prüfungen | Caliper | auf jedem Bau, gegen den fertigen Objektbaum |
3. Das Panel und die zwei Arten von Maß
Am Anfang steht eine Beschreibung des Panels: seine Breite, seine Höhe und wie viele Bildpunkte auf einen Millimeter gehen. Aus Breite und Höhe folgt alles Räumliche, also wie breit eine Spalte wird und wie viele Zeilen unter den header passen. Aus der Dichte folgt alles Körperliche, und aus dem Leseabstand die Schrift.
| Typ | Was er bedeutet | Woher der Wert kommt |
|---|---|---|
Point | Ein Bildpunkt des Panels, ganzzahlig | Ergebnis jeder Umrechnung, und das Einzige, was LVGL sieht |
Millimeter | Ein körperliches Maß auf der Glasscheibe | vom Entwurf gesetzt, etwa neun Millimeter für eine Fingerkuppe |
Reading | Ein Leseabstand in Millimetern | vom Entwurf gesetzt, etwa 400 für einen Fließtext |
Panel | Das Gerät, für das gerechnet wird | je Gerät einmal, zur Übersetzungszeit oder beim Start |
Der Übersetzer nimmt einen Millimeter nicht an, wo ein Point hingehört, und umgekehrt. Wer umrechnen will, muss das Panel fragen, und genau dabei fällt auf, dass man es überhaupt braucht.
namespace cal {
struct Point { int32_t value; };
struct Millimeter { float value; };
struct Reading { float millimetres; };
constexpr Millimeter operator""_mm(long double size);
constexpr Point operator""_pt(unsigned long long size);
struct Panel {
Point width;
Point height;
float points_per_mm;
constexpr Point operator()(Millimeter size) const;
constexpr Point TypeSize(Reading distance) const;
};
inline constexpr Panel kJc8048w500{ .width = 800_pt, .height = 480_pt,
.points_per_mm = 7.35f };
static_assert(kJc8048w500(token::kFingertip) == 66_pt);
}
Die letzte Zeile ist kein Beispiel, sondern die Prüfung selbst. Eine Fingerkuppe auf diesem Panel misst sechsundsechzig Punkte, und sobald das nicht mehr stimmt, baut die Bibliothek nicht. Daneben stehen dieselben Zusicherungen für den Daumen, für die vier Grade der Schrift, für die drei Abstände und für die Ecken.
| Panel | Punkte | Zoll | Punkte je mm | 9 mm in Punkten |
|---|---|---|---|---|
| JC8048W500, das dieses Projekt vermisst | 800 × 480 | 5,0 | 7,35 | 66 |
| ein Sieben-Zoll-Panel | 1024 × 600 | 7,0 | 6,68 | 60 |
| dasselbe, mit einem Faktor aus der Breite gerechnet | 1024 × 600 | 7,0 | entfällt | 84 |
Die Schrift folgt derselben Logik, nur über den Winkel. Eine Versalhöhe erscheint dem Auge unter einem Winkel, und lesbar ist sie, solange dieser Winkel groß genug bleibt. Aus dem Leseabstand und dem Winkel folgt die Versalhöhe in Millimetern, daraus über die Dichte ihre Höhe in Punkten und über das Verhältnis der Schrift die Schriftgröße. Der Winkel ist ein Wert der Gestaltung und steht in der Wertedatei. Er ist an der Skala des Bedienteils nachgerechnet und liegt bei 6,1 Milliradiant, was Anhang B zeigt.
Die untere Grenze steht fest
Caliper rechnet ab 800 auf 480 und nicht darunter. Der Grund ist die Tastatur: zehn Tasten zu je einer Fingerkuppe brauchen mit ihren Rinnen 732 Punkte, und zwischen den Rändern stehen 752 zur Verfügung. Auf einem schmaleren Panel hilft kein Layout, sondern nur eine andere Tastatur, und das ist eine Entscheidung über das Produkt.
4. Die Werte und ihre eine Quelle
Abstände, Radien, die Typenskala, die Palette und die Mindestgrößen sind die Gestaltung. Sie stehen in einer Datei, die nur Werte enthält und keinen Code, und aus ihr entstehen der Header für das Gerät und das Modul, mit dem die Dokumente gezeichnet werden. Beide erzeugten Dateien werden nie von Hand geändert, also können sie sich nicht widersprechen.
Das Format ist TOML, weil Python es seit Fassung 3.11 ohne Zusatzpaket liest und ein Mensch es ohne Anleitung schreibt. Abstände stehen darin in Millimetern und nicht in Punkten: ein Rand gilt dem Auge und dem Finger, und auf einem dichteren Panel soll er gleich aussehen und nicht gleich viele Bildpunkte breit sein.
[spacing]
edge = "2.18 mm" # Bildschirmrand zu allem darin
group = "3.27 mm" # zwei Gruppen nebeneinander
inset = "2.72 mm" # Kante einer Fläche zu ihrem Inhalt
line_gap = "1.63 mm" # zwei Zeilen, die zusammengehören
[touch]
fingertip = "9.0 mm"
thumb = "12.0 mm"
minimum = "6.53 mm" # der Boden, gegen den die Prüfung misst
[type]
cap_angle_mrad = 6.1 # unter diesem Winkel steht jede Versalhöhe
cap_ratio = 0.7 # Versalhöhe je Punkt Schriftgröße
title = "935 mm" # Leseabstand, aus dem der Grad folgt
heading = "560 mm"
body = "405 mm"
small = "345 mm"
[radius]
tile = "2.18 mm" # die eine Ecke des Entwurfs
[colour]
bg = "#111111"
surface = "#1b1b1b"
accent = "#009ce9"
Die vier Abstände sind aus den Graden zurückgerechnet, die der Entwurf heute trägt, und nicht auf runde Zahlen gesetzt. Der Grund steht in Anhang B: die Skala war als Sammlung gewachsen, und die genannten Abstände ergeben unter einem einzigen Winkel nicht dieselben vier Grade. Zurückgerechnet tun sie es, auf den Punkt genau, und damit ist die Skala eine Regel, ohne dass sich ein Bildschirm ändert. Was gerundet aussieht, war vorher die Ungenauigkeit.
Daraus entsteht ein Header, in dem jeder Wert constexpr ist und seinen Typ trägt. Ein Abstand ist ein Millimeter und keine nackte Zahl, also lässt er sich nirgends einsetzen, wo Punkte erwartet werden, ohne durch das Panel zu gehen.
// Erzeugt aus tokens/caliper.toml. Nicht von Hand ändern.
namespace cal::token {
inline constexpr Millimeter kEdge{2.18f};
inline constexpr Millimeter kInset{2.72f};
inline constexpr Millimeter kFingertip{9.0f};
inline constexpr Reading kBody{405.0f};
inline constexpr float kCapAngleMrad = 6.1f;
}
Der Name im Header folgt aus der Gruppe, in der der Wert steht: was unter spacing steht, heißt k und der Name, was unter layout steht, kBand und der Name. Das ist mechanisch und wird nicht entschieden, damit ein neuer Wert nicht darüber nachdenken lässt, wie er heißen soll.
Eine Regel steht dabei nicht als Zahl, sondern als Rechnung: eine Ecke innerhalb einer anderen ist die äußere weniger dem Abstand zwischen beiden. Wer sie als zwei Zahlen schreibt, bekommt beim nächsten Wechsel eine eingeschnürte Ecke. Caliper rechnet sie, und die Prüfung meldet jede, die anders herausgekommen ist.
| Gruppe | Was darin steht | Was im Gerät daraus wird |
|---|---|---|
| spacing | Rand, Gruppenabstand, Innenabstand, Zeilenabstand | Millimeter, über das Panel in Punkte |
| touch | Fingerkuppe und Daumen | Millimeter, zugleich die Untergrenze der Prüfung |
| type | Sehwinkel, Versalverhältnis, je Grad ein Leseabstand | Schriftgrößen, aus Winkel und Abstand gerechnet |
| radius | die eine Ecke des Entwurfs | jede weitere Ecke folgt aus ihr und dem Innenabstand |
| colour | die Palette | unverändert, denn eine Farbe hat kein Maß |
5. Wie ein Bildschirm entsteht
Ein Bildschirm sagt, was auf ihm steht und in welcher Ordnung. Wo etwas steht, entscheiden die Komponenten. Das ist der ganze Unterschied zu einer abgezählten Anordnung, und er ist der Grund, warum dasselbe Ergebnis auf einem anderen Panel steht, ohne dass jemand nachrechnet.
Die Fläche zerfällt in status_bar, header, sidebar und den Inhalt dazwischen. Jedes davon nimmt sich seine Höhe aus dem, was es trägt, und der Inhalt bekommt, was übrig bleibt. Einen footer gibt es nicht: was unten steht, ist die Wiedergabesteuerung, und Platz dafür hält der Inhalt nur auf einem Bildschirm, der eine trägt. Darunter arbeitet LVGLs Flex-Layout; Caliper stellt es nur richtig ein, statt eine zweite Layoutmaschine daneben zu setzen.
cal::Screen screen{cal::kJc8048w500};
screen.sidebar({ .icons = {&house, &sofa, ¬es, &gear}, .active = 1 });
screen.status_bar({ .clock = "21:14", .network = &signal });
screen.Header("Räume", "4 Räume");
cal::Object rooms = cal::List(screen.Content(), screen.panel());
cal::Row(rooms, screen.panel(), "Wohnzimmer", "42", &sofa);
cal::Row(rooms, screen.panel(), "Esszimmer", "38", &sofa);
screen.player_controller({ .title = "Blue in Green", .playing = true });
cal::InspectAndLog(screen);
Was ein Bildschirm nicht enthält, sind Zahlen. Weder eine Zeilenhöhe noch ein Rand noch eine Position steht dort, weil alle drei aus den Werten und dem Panel folgen. Wo eine Anordnung wirklich eine eigene Zahl braucht, ist das ein Zeichen, dass eine Komponente fehlt.
Zusammengesetzte Komponenten tragen ihre Teile als eigene Typen darin. In den Dokumenten trennt ein Punkt die beiden, also heißt der Balken eines Schiebers dort slider.track; in C++ ist es cal::Slider::Track. Die Zuordnung ist mechanisch, damit ein Lauf die Liste im Dokument gegen den Code halten kann.
6. Die Komponenten
Die Komponenten stammen nicht aus einer Ideensammlung, sondern aus einer Oberfläche, die es gibt: das Bedienteil führt siebenundvierzig davon, jede mit einer Zeichnung in Originalgröße und allen Maßen daneben, in PAP-018-C. Caliper übernimmt davon, was jede Bediengeräteoberfläche braucht. Was dem Produkt gehört, bleibt beim Produkt.
Tabelle 5 nennt links den Namen im Entwurf und rechts den, unter dem er in C++ steht. Die beiden sehen verschieden aus, weil der Entwurf in Python geschrieben ist und die Bibliothek dem Google-Stil folgt, und sie meinen dasselbe. Wo ein Name in der Bibliothek anders lautet als im Entwurf, steht er daneben.
| Wofür | Bei Caliper | Bei LEVEL |
|---|---|---|
| die Ränder des Bildschirms | Screen::status_bar, Screen::Header, Screen::sidebar, Screen::player_controller | was in ihnen steht, also die vier Bereiche und was gerade spielt |
| untereinander | List, Row, MarkCurrent, ChooseOne, LeadsAway, Chosen | room_card |
| etwas sagen | Screen::card, CentredBlock, ContentUnavailableView, Heading | nichts |
| etwas tun | Button, IconButton, MediaButtons, Screen::keyboard, TextField | nichts |
| etwas zeigen | PlayingInfo, Spacer | now_playing mit Spalte, album_grid mit Kachel |
| Bildzeichen | nichts; ein Zeichen ist ein Bild, das LVGL zeichnet | wordmark, meter_glyph, die Zeichen selbst |
| Grundformen | Squircle, SquircleImage, SquircleMask | engraved |
Noch nicht bei Caliper, aber im Entwurf vorhanden, sind der Schieber mit Balken und Griff, das Fähnchen über einer Taste, der Fortschrittsbalken und der Schalter einer Zeile. Sie stehen in Stufe 5, die als Einzige noch offen ist.
Die Namen tragen nichts aus dem Produkt. Eine Liste heißt list und nicht nach dem, was in ihr steht, denn das entscheidet der Bildschirm und nicht das Framework. Sie stapelt, was sie bekommt: list.row ist ihre schlichte Zeile mit Namen links, Wert rechts und einer Haarlinie darunter, row die anzutippende mit Grund, Zeichen und einem Zeichen am Ende. Beide stehen in derselben Liste.
Die Liste rollt. Sie nimmt so viele Zeilen, wie sie bekommt, zeigt so viele, wie zwischen header und footer Platz haben, und der Rest kommt beim Schieben nach. Das ist die teuerste Bewegung, die dieses Panel kennt, und der Kasten weiter unten nennt den Preis. Bezahlt wird er trotzdem, weil eine Liste rollt.
Zwei Regeln gelten für alle. Was Text trägt, hat eine Mindestbreite und keine feste, damit ein längeres Wort den Knopf breiter macht, statt darin abgeschnitten zu werden; gemessen wird gegen jede Sprache, die der Bau kennt, denn ein Wort, das nur in der zweiten zu lang ist, fällt sonst niemandem auf. Und jede Komponente rechnet aus dem Rahmen, den sie bekommt.
Was der Durchsatz den Komponenten vorschreibt
Auf dem Board dieses Projekts rechnet der Chip rund 4,3 Millionen Bildpunkte je Sekunde, gemessen mit zwei unabhängig gebauten Bibliotheken und in beiden gleich. Ein Vollbild sind 384.000 Bildpunkte, also bleiben elf bis dreizehn davon je Sekunde. Ein Ausschnitt dagegen läuft mit der vollen Bildrate des Panels. Daraus folgt für die Komponenten zweierlei: ein Wechsel der ganzen Fläche geschieht sofort und ohne Blende, und animiert wird nur, was einen Ausschnitt betrifft. Eine rollende Liste kostet dieselbe Rate, und sie rollt trotzdem. PAP-021 trägt die Messung.
Eine Komponente steht in der Liste, die das Bedienteil nicht führt: Spacer, die leere Fläche zwischen zwei Dingen. Mit einer Größe ist sie genau so groß, ohne eine wächst sie und schiebt, was links und rechts von ihr steht, an die Enden. Das ist dieselbe Komponente in zwei Zuständen und nicht zwei, und es ist dieselbe Sache, die SwiftUI Spacer nennt.
Ihre Richtung kommt vom Behälter und nicht von ihr selbst. In einer Zeile wächst sie in die Breite, in einer Spalte in die Höhe, und dafür braucht sie weder einen Parameter noch eine zweite Komponente. Senkrecht ist sie genauso brauchbar wie waagerecht: was oben steht und was unten, ist erste Zeile, Spacer, letzte Zeile.
Sie wird gebraucht, sobald eine Anordnung nicht gleichmäßig ist. Die beiden Tasten eines Schrittes stehen an den Enden ihres Bandes, also sind sie Taste, Spacer, Taste, und niemand rechnet dafür eine Position aus der Breite des Bildschirms. Wo eine Reihe wirklich gleichmäßig verteilt ist, tut es die Rinne des Bandes und Flex mit seiner eigenen Verteilung; ein Spacer je Lücke wäre dort ein Objekt zu viel, und auf einem Controller kostet jedes eines.
Zum Zentrieren taugt sie dagegen nicht, und das ist keine Lücke. Ein einzelnes Objekt in der Mitte einer Fläche ist eine Ausrichtung, also eine Eigenschaft des Objekts und kein Objekt daneben. Vier Spacer dafür bräuchten zwei geschachtelte Behälter, weil Flex in einer Richtung arbeitet, und kosteten sechs Objekte für das, was eine Ausrichtung ohne eines tut. Steht ein ganzer Stapel mittig, also Zeichen, Überschrift, Zeile und eine Bedienung darunter, macht das CentredBlock: er zählt Höhen und Lücken zusammen und setzt den Stapel als Ganzes. Von Hand gesetzt sitzt so ein Stapel fast immer zu tief, weil das Auge die Zeilen zählt und die Lücken vergisst.
Eine Komponente trägt ein Bild, und das ist die einzige, die etwas holt statt nur zu rechnen. artwork bekommt eine Adresse und eine Größe und liefert eine Fläche, die entweder das Bild zeigt oder den Platzhalter. Was dahinter liegt, gehört ihr nicht: LVGL entpackt, und wer die Daten heranschafft, entscheidet das Produkt, weil eine Datei auf einer Speicherkarte und ein Abruf über das Netz verschiedene Dinge sind. Was Caliper festlegt, ist die Größe und dass es einen Platzhalter gibt, denn ein Bild, das noch nicht da ist, ist der Normalfall und kein Fehler. Der Zwischenspeicher gehört LVGL und ist einzustellen, weil er ab Werk auf null steht und ein Gitter dann leer bleibt, ohne dass eine Meldung darauf hinweist.
Ein Zustand bekommt keine eigene Komponente. Der löschende Knopf ist der Knopf in der Warnfarbe, die Leiste beim Einrichten ist dieselbe Leiste in einem anderen Zustand, und die Kachel ohne Bild ist dieselbe Kachel ohne Bild. Beim Bedienteil sind auf diese Weise sieben Namen wieder verschwunden, und jeder von ihnen war ein zweites Bauteil, das dasselbe tat.
7. Die Prüfungen
Dies ist der Teil, den kein Oberflächenwerkzeug mitbringt, und der Grund, warum Caliper mehr ist als ein Thema. Nach jedem Aufbau eines Bildschirms läuft eine Reihe von Prüfungen über den fertigen Objektbaum. Sie sind Teil des Baus und kein Testwerkzeug daneben, und ein Fehlschlag bricht ihn ab.
| Prüfung | Die Regel | Was sie verhindert |
|---|---|---|
| Trefferfläche | Jede Bedienfläche misst in beiden Richtungen mindestens eine Fingerkuppe | eine Taste, die der Finger nicht trifft und die auf dem Rechner groß genug aussieht |
| Rand | Nichts steht näher an einer Kante des Panels als der Rand | Inhalt, der am Gehäuse klebt |
| Passung | Kein Text ist breiter als die Fläche, die ihn trägt, in keiner Sprache | ein Wort, das nur in der zweiten Sprache abgeschnitten wird |
| Ecke | Eine Ecke in einer Ecke ist die äußere weniger dem Abstand | die eingeschnürte Ecke, die man sieht und nicht benennen kann |
| Raster | Jede Kante liegt auf dem Raster des Entwurfs | zwei Flächen nebeneinander, um einen halben Punkt versetzt |
| Leiste | Nichts im Inhalt reicht unter den header oder unter die Wiedergabesteuerung | eine Zeile, die von einer Leiste verdeckt wird |
Zwei der sechs treten zurück, sobald etwas in einem rollenden Bereich steht: die Leiste und der untere Rand. Eine Zeile unterhalb der Kante ist dort der Sinn der Sache und kein Fehler, und sie klebt auch an keinem Gehäuse, weil sie gar nicht auf dem Glas steht. Die Seiten hält der Rand weiterhin, und die anderen vier Prüfungen laufen unverändert.
Ein Befund nennt die Komponente, die Regel und beide Zahlen, damit niemand nachmessen muss:
caliper: touch target: "button" is 48, needs 66 (352,404 120x48)
Hält ein Bildschirm alle sechs, meldet der Lauf screen holds und sonst nichts. Das ist die Zeile, an der sich ablesen lässt, dass eine Anordnung stimmt, und sie steht im Protokoll des Geräts wie im Fenster auf dem Schreibtisch.
Dieselben Prüfungen laufen ohne Gerät, denn LVGL baut auch für den Rechner. Ein Bildschirm lässt sich im Test aufbauen und vermessen, ohne dass ein Board angeschlossen ist. Das ist zugleich der Weg, auf dem sich später zeigen lässt, dass eine Oberfläche auf einem zweiten Panel hält: sie wird dafür gebaut, und die Prüfungen sagen, ob sie passt.
8. Wie es gebaut und eingebunden wird
Caliper ist eine ESP-IDF-Komponente und wird über ihre Repository-Adresse gezogen. Derselbe Baum baut ohne ESP-IDF für den Rechner, wo die Tests laufen.
| Ordner | Was darin liegt |
|---|---|
include/caliper | Die öffentlichen Header: Panel, Einheiten, Thema, Komponenten, Prüfungen |
src | Was nicht im Header stehen muss, also alles, was LVGL anfasst |
tokens | caliper.toml, die eine Quelle der Werte |
tools | Der Erzeuger, der daraus Header und Python-Modul schreibt |
test | Die Tests für den Rechner, ohne Board |
examples | Ein Bildschirm, der sich allein bauen und ansehen lässt |
Der Erzeuger läuft vor dem Übersetzen und nicht von Hand. Wer die Wertedatei ändert und nur baut, bekommt den neuen Header, und wer den Header ändert, bekommt ihn beim nächsten Bau überschrieben. Das ist der ganze Mechanismus, der das Auseinanderlaufen verhindert.
9. Der Weg dahin
Gebaut wird in fünf Stufen, und jede ist für sich brauchbar. Jede Zeile der Tabelle ist als eigenes Issue geschnitten; die Nummern sind die im Repository. Vier der fünf stehen, und mit ihnen zeichnet das Bedienteil seine Bildschirme. Offen ist die fünfte, in der die Komponenten nachkommen, die der Entwurf noch verlangt.
| Stufe | Issue | Stand | Die Aufgaben darin | Fertig, wenn |
|---|---|---|---|---|
| 1 | #1 | steht | Die Wertedatei anlegen; den Erzeuger schreiben, der daraus den Header und das Python-Modul schreibt; die Ecke als Rechnung statt als Zahl; beide Ausgaben mit dem Vermerk versehen, dass sie erzeugt sind | Der Erzeuger der Paper des Bedienteils liest die Werte von dort und zeichnet unverändert weiter |
| 2 | #2 | steht | Die Typen Point, Millimeter und Reading; die Beschreibung des Panels; die Umrechnung über die Dichte; die Schriftgröße aus dem Sehwinkel; die Mindestbreite eines Bedienelements | Tests rechnen Fingerkuppe und Schriftgröße für drei Panels aus, und der static_assert für das eigene Panel steht |
| 3 | #3 | steht | Der Durchgang über den Objektbaum; die sechs Prüfungen mit ihren Meldungen; der Bau für den Rechner mit LVGLs Desktop-Anbindung; je Prüfung ein Test, der sie absichtlich rot laufen lässt | Ein zu kleines Bedienelement lässt den Bau scheitern und die Meldung nennt beide Zahlen |
| 4 | #4 | steht | Das Thema aus den erzeugten Werten; status_bar, header und footer mit dem Inhalt dazwischen; header, row, card und button; die Mindestbreite an allem, was Text trägt | Ein Bildschirm des Bedienteils steht auf dem Board und die Prüfungen sind still |
| 5 | #5 | offen | Auflage, Schieber mit seinen Teilen, Tastatur mit ihren Ebenen und dem Fähnchen, das Zeichen in seinen Spielarten, Fortschritt, Bild und der Satz darunter | Alle Bildschirme des Bedienteils stehen |
Was danach möglich wird, gehört noch nicht zum Plan und steht hier als das, worauf er zuläuft: LVGL läuft auch auf dem Rechner, also könnten die Zeichnungen in den Dokumenten aus derselben Oberfläche kommen, die auf dem Gerät läuft. Dann können Dokument und Gerät nicht mehr auseinanderlaufen. Was das kostet, ist die Schärfe der heutigen Vektorzeichnungen mit ihren Maßlinien, und ob sich das lohnt, entscheidet sich, wenn die Stufen oben stehen.
Anhang
Die Umgebung, in der gemessen wurde, die Rechnung hinter der Typenskala und die Fundstellen. Für den Plan oben braucht man das hier nicht.
A. Die Messumgebung
Alle körperlichen Zahlen dieses Dokuments gelten für ein Gerät und einen Tag. Was auf einem anderen Panel gilt, rechnet Caliper aus dessen Beschreibung, und was hier steht, ist der Fall, an dem es geprüft wurde.
| Was | Stand | Herkunft |
|---|---|---|
| Board | JC8048W500, ESP32-S3, Panel 800 × 480 auf fünf Zoll, an /dev/cu.usbserial-11110 | gemessen |
| Werkzeugkette | ESP-IDF v5.3.2 unter ~/esp/esp-idf, xtensa-esp32s3-elf-gcc 13.2.0, das C++20 trägt | gemessen |
| Dichte | 7,35 Bildpunkte je Millimeter | abgeleitet |
| Firmware | das Bedienteil selbst, auf Caliper gebaut: Startbild, Einrichtung bis ins Netz, Home, Räume und die Einstellungen. Dieselben Bildschirme laufen in einem Fenster auf dem Rechner | gemessen |
| LVGL | 9.5.0 mit esp_lvgl_port 2.9.0, auf dem Board gelaufen und vermessen | gemessen |
| Durchsatz | rund 4,3 Millionen Bildpunkte je Sekunde, also elf bis dreizehn Vollbilder; mit Slint und mit LVGL derselbe Wert | gemessen |
| Vergleichspanel | 1024 × 600 auf sieben Zoll, nur gerechnet; kein solches Panel lag vor | abgeleitet |
Blind war die Messung an einer Stelle, und das ist die wichtigste: es gibt kein zweites Panel auf dem Tisch. Die Zahlen für sieben Zoll stammen aus der Diagonale und nicht aus einer Messung, und ob eine Oberfläche dort wirklich hält, sagt erst der Bau dafür.
B. Die Typenskala nachgerechnet
Die Typenskala des Bedienteils nennt zu jedem Grad einen Leseabstand. Rechnet man daraus den Sehwinkel der Versalhöhe zurück, sollten alle Grade denselben Winkel ergeben, wenn die Skala eine Regel ist und keine Sammlung von Werten. Drei von vier tun das.
| Grad | Punkte | Versalhöhe mm | Abstand mm | Winkel mrad |
|---|---|---|---|---|
| TITLE | 60 | 5,75 | 950 | 6,05 |
| HEADING | 36 | 3,45 | 650 | 5,31 |
| BODY | 26 | 2,49 | 400 | 6,23 |
| SMALL | 22 | 2,11 | 350 | 6,03 |
TITLE, BODY und SMALL liegen bei 6,05, 6,23 und 6,03 Milliradiant, also innerhalb von drei Prozent beieinander. HEADING fällt mit 5,31 heraus. Entweder ist die Überschrift kleiner, als ihr genannter Abstand verlangt, oder ihr Abstand ist ein anderer: bei sechsunddreißig Punkten trägt sie 565 Millimeter, und für die genannten 650 bräuchte sie 41 Punkte.
Für Caliper ist das kein Nebenbefund, sondern die Entscheidung, wie die Skala entsteht. Der Winkel steht in der Wertedatei, die Abstände stehen daneben, und die Grade folgen daraus. Damit ist die Skala eine Regel, und ein Ausreißer fällt beim nächsten Mal auf, statt sich einzunisten. Welcher der beiden Werte bei HEADING der richtige ist, entscheidet sich am Gerät und nicht auf dem Papier.
C. Geprüfte Belege
| Aussage | Fundstelle |
|---|---|
| Das Panel misst 800 auf 480 bei 7,35 Punkten je Millimeter | build_screens.py:31 bis :33 |
| Eine Fingerkuppe sind 66 Punkte und meint neun Millimeter | build_screens.py:125 |
| Die Typenskala ist über Leseabstände begründet | build_screens.py:107 und :120 |
| Die vier Abstände des Entwurfs in Punkten | build_screens.py:39 bis :41 und :84 |
| Das Spaltenraster geht auf 800 auf | build_screens.py:53 bis :55 |
| Der Erzeuger setzt 1716 Maße, alle in Bildpunkten dieses Panels | Lauf von build_screens.py, 8. September 2026 |
| Siebenundvierzig Komponenten mit Zeichnung und Maßen | Papers/level/components.html, PAP-018-C |
| Sieben Namen sind entfallen, weil sie Zustände waren | derselbe Ort, Abschnitt 10 |
| Zehn Tasten brauchen 732 von 752 Punkten | gerechnet aus KEY_W, KEY_GAP und EDGE |
| Die Winkel der Typenskala | gerechnet aus Grad, CAP_RATIO und PX_PER_MM, 6. September 2026 |
| Die Werkzeugkette ist vollständig installiert | ~/esp/esp-idf und ~/.espressif/tools gelistet, 6. September 2026 |
| Vier der fünf Stufen stehen, die fünfte ist offen | Issues #1 bis #5 im Repository, 8. September 2026 |
| Python liest TOML seit Fassung 3.11 ohne Zusatzpaket | tomllib in der Standardbibliothek; hier läuft 3.14.7 |
| Rund 4,3 Millionen Bildpunkte je Sekunde, elf bis dreizehn Vollbilder | Messung auf dem Board am 6. September 2026, PAP-021 |
| Slint und LVGL erreichen bei Flächen und Text denselben Wert | 12,5 gegen 12,1 Bilder je Sekunde im selben festen Ablauf, derselbe Ort |
LVGL 9.5.0 und esp_lvgl_port 2.9.0 laufen auf diesem Board | dependencies.lock des Versuchsaufbaus, geflasht und vermessen |
| LVGLs Bildspeicher steht ab Werk auf null | CONFIG_LV_CACHE_DEF_SIZE, Vorgabe 0; das Gitter blieb ohne Meldung leer |