Compare commits

...
Author SHA1 Message Date
raw-designsandClaude Opus 5 a3034f7c42 feat(P4): UART zum S3 beim Waveshare-Board auf GPIO17/GPIO18 legen
Der 40-polige Header des ESP32-P4-Pico hat eine andere Belegung als die
Expansion-IO der JC-Panels. Für WS_PANEL_7H gilt jetzt TX = GPIO17,
RX = GPIO18; die JC-Panels bleiben bei GPIO33/GPIO31.

README und Changelog entsprechend ergänzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018QoLDQeUh1dCQz7yYZVb4Y
2026-09-01 03:04:28 +02:00
raw-designsandClaude Opus 5 93bb0deba1 feat(P4): Waveshare ESP32-P4-Pico mit 7inch DSI LCD (H) unterstützen (1.6.0)
Dieselbe P4-Firmware läuft jetzt zusätzlich auf einem Waveshare ESP32-P4-Pico
mit dem Waveshare 7inch DSI LCD (H) (1280x720). Auswahl über JC_PANEL_TYPE in
config.h (neu: WS_PANEL_7H, ab jetzt Vorgabe); JC_PANEL_43 und JC_PANEL_70
bleiben unverändert.

- board_bringup.c: DSI-Bus, DBI-Kommandokanal und DPI-Panel werden ohne
  Hersteller-Panel-Treiber direkt angelegt (PLL_F20M, 2 Lanes zu 1250 Mbit/s,
  80 MHz DPI, Austastlücken 64/64/64, RGB888). Panel-Freigabe und Helligkeit
  über die Bridge auf I2C 0x45; Licht erst nach dem Start der DSI-Ausgabe.
- lv_conf.h leitet LV_COLOR_DEPTH aus JC_PANEL_TYPE ab (24 Bit für das
  Waveshare-Panel, 16 Bit für die JC-Panels) und liest dafür config.h.
  display_hal.cpp sichert das mit static_assert ab.
- Touch GT9271 (I2C 0x14) über den GT911-Treiber; Achsentausch und Begrenzung
  auf die Bildfläche im process_coordinates-Hook.
- pins_config.h: Auflösung, DSI-Zeitbasis und Touch-Umrechnung als WS7_*-Werte.
- README und Changelog ergänzt.

Die UART-Pins zum S3 stehen für das Waveshare-Board noch auf den JC-Werten
(GPIO33/31) und müssen nach dem Header-Aufdruck gesetzt werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DxAvqVnrY6YSoGFrNGeRTi
2026-09-01 02:56:23 +02:00
thomas 27a3ee840e Merge pull request 'feat(P4): 7-Zoll-Display JC1060P470C-I-W unterstützen (1.5.0)' (#26) from feat/7zoll-display-JC1060P470C into main 2026-08-22 02:36:52 +02:00
raw-designsandClaude Opus 5 92c1446824 feat(P4): 7-Zoll-Display JC1060P470C-I-W unterstützen (1.5.0)
Dieselbe Display-Firmware bedient jetzt wahlweise das bisherige 4,3-Zoll-Panel
JC4880P443C-I-W (ST7701, 480x800) oder das neue 7-Zoll-Panel JC1060P470C-I-W
(JD9165, 1024x600). Umgeschaltet wird über JC_PANEL_TYPE in config.h
(JC_PANEL_43 / JC_PANEL_70) oder per Compiler-Flag -DJC_PANEL_TYPE=70. Vorgabe
bleibt das 4,3-Zoll-Panel; dessen Verhalten ändert sich nicht.

- src/lcd/esp_lcd_jd9165.c/.h aus dem Hersteller-Paket übernommen;
  board_bringup.c legt je nach Panel DSI-Bus, DPI-Zeitbasis und Herstellertreiber an.
- pins_config.h liefert Auflösung, LCD-Reset (4,3": GPIO5, 7,0": GPIO27) und
  Ausrichtung panelabhängig. Das 7-Zoll-Panel ist nativ Querformat, deshalb
  entfallen dort die 270-Grad-Rotation und die PPA-Beschleunigung.
- lvgl_port_v9.h bezieht LVGL_PORT_H/V_RES aus pins_config.h statt fest 480/800.
- Der GT911 des 7-Zöllers meldet Rohkoordinaten im Raster 800x480 statt 1024x600.
  Die Umrechnung hängt am process_coordinates-Hook von esp_lcd_touch, damit die
  Herstellerdatei esp_lcd_touch_gt911.c unverändert bleibt.
- ui.cpp rechnet nicht mehr mit fest verdrahteten 480 Pixeln Höhe, sondern mit
  DISP_VER_RES. Layout und Schriften bleiben sonst unverändert.
- Hersteller-Paket des 7-Zöllers unter JC_Display_Firmware_7zoll/ abgelegt;
  Werkzeuge, Archive und mitgelieferte Fremdbibliotheken per .gitignore
  ausgeschlossen (587 MB im Original, 21 MB im Repo).

Kompiliertest ESP32-P4, beide Varianten:
  4,3": 1.173.768 B Flash (37 %), 322.880 B RAM (98 %)
  7,0": 1.138.752 B Flash (36 %), 322.600 B RAM (98 %)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 02:35:40 +02:00
thomas 544c6ae3b8 Merge pull request 'fix: Auswahl beim Aufwecken über Touch entfiel manchmal (S3 5.6.3 / P4 1.4.2)' (#25) from fix/wake-abfrage-touch-pfad into main 2026-08-19 01:03:24 +02:00
raw-designsandClaude Opus 5 743c38e994 fix: Auswahl beim Aufwecken über Touch entfiel manchmal (S3 5.6.3 / P4 1.4.2)
Über den Schalter kam die Abfrage "Espresso oder Cold Extraction" zuverlässig,
über Antippen am Display nur manchmal. Ursache waren zwei unterschiedliche
Bedingungen für dieselbe Sache:

- askMode (zeigt die Auswahl im Dialog) verlangte zusätzlich cxArmable
- choiceRelevant (sendet "wakeEspresso" und schließt damit das Auswahlfenster
  der S3) verlangte es nicht

In der Lücke dazwischen zeigte der Dialog nur "Aufwecken", unterdrückte aber
trotzdem die Rückfrage der S3 - die Frage war auf beiden Seiten weg. Der
Schalterweg hatte diese Lücke nie, weil er allein an der S3 hängt.

P4: beide Stellen benutzen jetzt wake_choice_offered(). Bot der Dialog die
Auswahl nicht an, sendet er "deactivateStandby"; dann öffnet die S3 ihr
eigenes Auswahlfenster und die Frage kommt trotzdem.

S3: Die Fensterentscheidung ist als Bedingungskette geschrieben und hält
Quelle und Ergebnis des letzten Aufweckens fest. /Cold-Extraction zeigt das
an - die bisherige Live-Liste zeigt nur den jetzigen Stand, der beim
Nachsehen längst wieder ein anderer sein kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 01:02:30 +02:00
thomas 535f640bbd Merge pull request 'fix: Abfrage beim Aufwecken geht in beide Richtungen (S3 5.6.2 / P4 1.4.1)' (#24) from fix/cold-extraction-abfrage-beide-richtungen into main 2026-08-12 07:20:11 +02:00
raw-designsandClaude Opus 5 f8d51a57b7 fix: Abfrage beim Aufwecken geht in beide Richtungen (S3 5.6.2 / P4 1.4.1)
Die Abfrage erschien nicht, sobald der Cold-Extraction-Modus noch aktiv war — und
da er seit 5.3.1 persistent ist, war er das nach dem ersten kalten Bezug dauerhaft.
Damit erklärt sich, warum weder der Schalter- noch der Touch-Weg etwas zeigte.

Die Bedingung "Modus ist aus" war falsch gedacht ("sonst gibt es nichts zu fragen").
Die Auswahl muss in beide Richtungen gehen, also auch von kalt zurück auf Espresso.
Sonst hängt man nach dem ersten kalten Bezug im kalten Modus fest, ohne beim
Aufwecken je wieder gefragt zu werden.

- Bedingung aus beiden Öffnungspfaden entfernt (Standby-Übergang und Kaltstart).
- Das Fenster bleibt bei aktivem Modus offen; bisher schloss es sofort wieder.
- "wakeEspresso" beendet jetzt einen laufenden Modus, statt ihn nur nicht
  einzuschalten — sonst wäre die Auswahl eine Einbahnstraße.
- Diagnose auf /Cold-Extraction listet den Modus nicht mehr als Voraussetzung,
  sondern zeigt ihn als Ist-Zustand.
- P4: Der Cold-Knopf zeigt den Ist-Zustand ("aktiv lassen" und hervorgehoben, wenn
  der Modus läuft).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 07:19:50 +02:00
thomas 8432bd3f15 Merge pull request 'fix(S3): Auswahl beim Aufwecken auch nach Kaltstart + Diagnose der Bedingungen (5.6.1)' (#23) from fix/cold-extraction-auswahl-diagnose into main 2026-08-12 02:33:54 +02:00
raw-designsandClaude Opus 5 49bdc686c4 fix(S3): Auswahl beim Aufwecken auch nach Kaltstart + Diagnose der Bedingungen (5.6.1)
Die Abfrage hängt an fünf Bedingungen gleichzeitig (freigeschaltet, Abfrage
eingeschaltet, Display verbunden, Modus gerade aus, kein Wartungsmodus/Reinigung/
Tuning). Fehlt eine, passiert schlicht nichts, und von außen war nicht erkennbar
welche. Die Seite /Cold-Extraction zeigt sie jetzt live mit Haken bzw. Kreuz an,
dazu ob das Auswahlfenster gerade offen ist.

Zusätzlich eine echte Lücke geschlossen: Die Abfrage kam nur beim Standby-Übergang.
Wer die Maschine am Netzschalter einschaltet, durchläuft nie einen solchen Übergang
— dieser Weg war nicht abgedeckt. Jetzt öffnet das Fenster einmal pro Laufzeit auch
nach einem Kaltstart, sobald sich das Display gemeldet hat. Startet die Maschine in
den Standby hinein, bleibt es beim Standby-Übergang.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 02:33:35 +02:00
thomas 8dcebaa22f Merge pull request 'feat: Espresso/Cold-Auswahl bei jedem Aufwecken, auch per Schalter (S3 5.6.0 / P4 1.4.0)' (#22) from feat/cold-extraction-auswahl-nach-aufwecken into main 2026-08-12 01:47:09 +02:00
raw-designsandClaude Opus 5 7c1e485722 feat: Espresso/Cold-Auswahl bei jedem Aufwecken, auch per Schalter (S3 5.6.0 / P4 1.4.0)
Die Abfrage war eine reine Vorab-Abfrage des Displays und erschien nur beim
Antippen des Standby-Bildschirms. Beim Aufwecken über den Schalter ist der Standby
aber schon beendet, bevor das Display überhaupt fragen könnte — dieser Weg war nie
abgedeckt.

Die Steuerung hält jetzt nach dem Aufwecken ein Auswahlfenster offen (20 s): Der
Wasserkreis heizt in dieser Zeit nicht, das Display blendet die Auswahl von sich aus
ein und zeigt einen Countdown. Ohne Entscheidung wird danach normal geheizt, es geht
also nichts verloren, wenn niemand hinsieht.

Das Fenster öffnet nur mit angeschlossenem Display (sonst könnte niemand wählen),
bei freigeschalteter Funktion und aktiver Einstellung "Abfrage beim Aufwecken", und
nur wenn der Modus nicht ohnehin schon läuft. Es schließt bei Entscheidung,
Bezug/Spülen oder erneutem Standby.

Neue Aktion wakeEspresso (beendet Standby falls aktiv und schließt das Fenster) —
das Display sendet sie statt deactivateStandby, sobald die Auswahl relevant ist,
sonst würde direkt nach dem Aufwecken nochmal gefragt. Neue State-Felder
cxWakeChoice und cxWakeChoiceSec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:46:47 +02:00
thomas 7d14f94a72 Merge pull request 'feat: Abfrage Espresso oder Cold Extraction beim Aufwecken (S3 5.5.0 / P4 1.3.0)' (#21) from feat/cold-extraction-abfrage-beim-aufwecken into main 2026-08-12 01:02:58 +02:00
raw-designsandClaude Opus 5 11307d255b feat: Abfrage Espresso oder Cold Extraction beim Aufwecken (S3 5.5.0 / P4 1.3.0)
Der Aufweck-Dialog am Touch-Display wird zur Auswahl: "Womit aufwecken?" mit
"Espresso" (heizt normal auf) und "Cold Extraction (Wasser bleibt kalt)". Damit
lässt sich bei jedem Aufwecken neu entscheiden, ohne vorher die Web-UI zu bedienen.
Bei der kalten Variante wird der Modus scharf geschaltet, bevor der Standby endet —
der Wasserkreis heizt also gar nicht erst an.

Abschaltbar über die neue Einstellung "Abfrage beim Aufwecken" (EEPROM 982), zu
finden auf /Cold-Extraction und auf der Cold-Extraction-Seite des Displays. Ist sie
aus, erscheint wieder der bisherige Dialog. Ohne angeschlossenes Display oder bei
nicht freigeschalteter Funktion hat sie keine Wirkung. Neues Feld cxAskOnWake im
State und in saveBrew.

Dazu: Die Meldungstexte der kalten Extraktion sind jetzt mit echten Umlauten
geschrieben ("nicht möglich" statt "nicht moeglich"). Für das OLED übersetzt die
neue Hilfsfunktion oledUmlauts() sie beim Rendern nach CP437 — dort hätte UTF-8
sonst zwei Fehlzeichen je Umlaut ergeben, weshalb die Texte ursprünglich
transliteriert waren. Web-UI und Touch-Display stellen UTF-8 direkt dar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:02:37 +02:00
thomas f1a8d8c175 Merge pull request 'fix: Zielgewicht bei Cold Extraction ueberall korrekt anzeigen (S3 5.4.1 / P4 1.2.1)' (#20) from fix/cold-extraction-zielgewicht-anzeige into main 2026-08-12 00:52:07 +02:00
raw-designsandClaude Opus 5 334896b9cf fix: Zielgewicht bei Cold Extraction überall korrekt anzeigen (S3 5.4.1 / P4 1.2.1)
Während einer kalten Extraktion stand als Zielgewicht überall noch der
Brew-by-Weight-Wert des normalen Espresso-Bezugs. OLED, Web-Dashboard und
Touch-Display griffen alle direkt auf brewByWeightTargetGrams zu — die kalte
Extraktion ersetzt Brew-by-Weight aber und stoppt nach coldExtractionTargetGrams.

Neue Hilfsfunktion activeTargetWeightGrams() liefert das Ziel des aktuellen bzw.
nächsten Bezugs. Das UART-Feld targetWeight führt jetzt diesen Wert; bbwTarget
bleibt unverändert die Roh-Einstellung, damit die Brew-Control-Seite weiter das
Richtige anzeigt.

Zusätzlich hing die Gewichtsanzeige selbst an "Brew-by-Weight aktiv" — das ist bei
kalter Extraktion typischerweise aus, obwohl ein Ziel existiert. OLED, Waage-Kachel
und Live-Bezugsschirm zeigen Gewicht und Fortschritt jetzt auch beim kalten Bezug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:51:48 +02:00
thomas e8b1832fc1 Merge pull request 'feat: eigene Cold-Extraction-Seite in Web-UI und Display (S3 5.4.0 / P4 1.2.0)' (#19) from feat/cold-extraction-eigene-seite into main 2026-08-12 00:09:49 +02:00
raw-designsandClaude Opus 5 2d8f70ae37 feat: eigene Cold-Extraction-Seite in Web-UI und Display (S3 5.4.0 / P4 1.2.0)
Cold Extraction bekommt in beiden Oberflächen einen eigenen Menüpunkt statt eines
angehängten Blocks auf der Brew-Control-Seite. Die Parameter sind damit erstmals
auch am Touch-Display änderbar.

S3: neue Seite /Cold-Extraction, gegliedert in Grundeinstellung, Vorbenetzung,
Hauptbezug, Bezugsende und Pumpenschutz, jeder Abschnitt mit kurzer Begründung.
/Brew-Control verweist nur noch darauf. Das UART-Kommando saveBrew akzeptiert jetzt
die cx-Felder, und das State-JSON liefert die restlichen Parameter mit, damit das
Display seine Felder vorbelegen kann.

Wichtig: Der Speicher-Handler von /Brew-Control wertet die cx-Felder nicht mehr aus.
Er tat es bisher, und nach dem Verschieben der Checkbox hätte ein Speichern dort die
Freischaltung abgeschaltet, weil das Feld im Formular fehlt.

P4: neue Seite "Cold Extraction" mit allen Parametern; der Modus-Schalter ist von der
Temperatur-Seite dorthin umgezogen, damit es nur eine Stelle gibt. Die Brew-Control-
Seite ist neu gegliedert: jede Funktion in einer eigenen Karte mit Titel und einer
Zeile, die erklärt was sie tut, statt einer durchlaufenden Liste unter
Zwischenüberschriften. Neues Hilfsmakro group_card() für beide Seiten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:09:23 +02:00
thomas 537f1d4c20 Merge pull request 'fix(S3): Cold Extraction benetzt sanft statt mit voller Leistung (5.3.6)' (#18) from fix/cold-extraction-sanfte-vorbenetzung into main 2026-08-11 23:41:43 +02:00
raw-designsandClaude Opus 5 2cf375a2b4 fix(S3): Cold Extraction benetzt jetzt sanft statt mit voller Leistung (5.3.6)
Messung an der Maschine: Nach 7 s Vorbenetzung kommen heiß 1-2 sirupartige Tropfen,
kalt bei voller Pumpenleistung rund 10 g dünne Brühe ohne Crema — etwa zehnfacher
Durchfluss trotz dreifacher Viskosität.

Die Ursache ist nicht die Viskosität, sondern das Kaffeebett: Kaltes Mehl quillt
nicht, ist schlecht benetzbar, es wandert kein Feinanteil und es lösen sich kaum
Feststoffe, die das Bett verdichten. Volle Leistung auf den trockenen kalten Puck
legt deshalb sofort Kanäle an, die sich — anders als bei heißem Bezug — nie wieder
schließen; der restliche Bezug läuft daran vorbei statt zu extrahieren.

Vorbenetzung und Hauptbezug pulsen jetzt beide, nur mit unterschiedlicher Leistung.
Neue Einstellung "Pumpenleistung Vorbenetzung" (Standard 10 %), Standard im
Hauptbezug von 30 % auf 20 % gesenkt. Bestehende Installationen behalten ihre Werte;
das neue Feld greift beim Update automatisch auf den Default zurück.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:41:23 +02:00
thomas e33de56edc Merge pull request 'feat: Cold Extraction im Standby scharf schaltbar (S3 5.3.5 / P4 1.1.3)' (#17) from feat/cold-extraction-im-standby into main 2026-08-11 03:01:10 +02:00
raw-designsandClaude Opus 5 9b91543435 feat: Cold Extraction im Standby scharf schaltbar (S3 5.3.5 / P4 1.1.3)
"Standby aktiv" und "Kessel zu warm" galten bisher auch fürs Aktivieren. Das war
falsch herum gedacht: Der Modus schaltet die Heizung AB, man kam aber erst an den
Schalter, nachdem die Maschine aufgeweckt war und schon zu heizen begonnen hatte —
genau der Ablauf, den die Funktion vermeiden soll.

Scharfschalten und Beziehen sind jetzt getrennt:
- Scharfschalten (neues State-Feld cxArmable): blockiert nur durch fehlende
  Freischaltung, Wartungsmodus, Reinigungsassistent, PID-Tuning oder einen
  laufenden Bezug/Spülvorgang.
- Bezug (unverändert cxAllowed): zusätzlich kein Standby, Kessel unter der
  Freigabeschwelle, gültiger Sensorwert.

Ist der Modus scharf, der Bezug aber gesperrt, sagen Web-UI und Display das jetzt
ausdrücklich. Bei Aktivierung mit warmem Kessel geht die Heizung sofort aus, die
Rückmeldung nennt die Sperre bis zum Unterschreiten der Schwelle.

P4: Der Aufweck-Dialog bekommt "Aufwecken mit Cold Extraction" — sendet
startColdExtraction vor deactivateStandby, damit der Wasserkreis gar nicht erst
anheizt. Der Schalter hängt jetzt an cxArmable; bei älterer S3-Firmware fällt das
Feld auf cxEnabled zurück und bleibt bedienbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 02:59:22 +02:00
thomas 6dd3f1eaf2 Merge pull request 'feat: Cold Extraction warnt bei fehlender Waage (S3 5.3.4 / P4 1.1.2)' (#16) from feat/cold-extraction-waagen-hinweis into main 2026-08-11 02:07:35 +02:00
raw-designsandClaude Opus 5 b6155acafe feat: Cold Extraction warnt bei fehlender Waage (S3 5.3.4 / P4 1.1.2)
Ohne aktive und verbundene Waage greifen weder das Zielgewicht noch die
Stillstands-Erkennung — der Bezug endet dann erst nach der maximalen Bezugsdauer,
die damit faktisch zur Dosierung wird. Bisher passierte diese Degradierung
stillschweigend.

Die Funktion bleibt ohne Waage bewusst nutzbar (Zeitsteuerung als Rückfallebene),
damit ein kurzzeitig abgemeldeter HX711 die kalte Extraktion nicht komplett
blockiert. Angezeigt wird der Zustand jetzt an vier Stellen: dauerhafter
Warnhinweis auf /Brew-Control (Zielgewicht-Feld zusätzlich als "nur mit Waage
wirksam" beschriftet), Hinweiszeile unter dem Dashboard-Schalter, Statustext
"Cold Extraction bereit (ohne Waage: Zeitsteuerung)" und eine einmalige Meldung
beim Aktivieren.

P4: Statuszeile zeigt "ohne Waage, Ende nach Zeit" in Warnfarbe statt des blauen
Normalhinweises; Erklärzeile am Schalter und Parameter-Anzeige auf der Brew-Seite
ergänzen den Hinweis und nennen bei aktivem Modus ohne Waage die Zeit statt des
Zielgewichts. Neuer cxNotice-Code 4 (additiv, Protokoll bleibt v2).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 02:05:50 +02:00
thomas 4cc9ceea7c Merge pull request 'fix(S3): Cold Extraction brach waehrend der Vorbenetzung ab (5.3.3)' (#15) from fix/cold-extraction-vorbenetzung into main 2026-08-11 01:55:31 +02:00
75 changed files with 35410 additions and 247 deletions
+10
View File
@@ -0,0 +1,10 @@
# -------------------------------------------------------------------------------------
# Hersteller-Paket des 7-Zoll-Displays (JC1060P470C-I-W)
# Der Ordner ist im Original ~587 MB gross. Ins Repo kommen nur Schaltplaene, Specs,
# Datenblaetter, Handbuch und die Arduino-Demos; ausgeschlossen bleiben Werkzeuge,
# Archive und die mitgelieferten Fremdbibliotheken - die sind anderweitig beziehbar.
# -------------------------------------------------------------------------------------
JC_Display_Firmware_7zoll/8-Burn operation/flash_download_tool_3.9.7/
JC_Display_Firmware_7zoll/1-Demo/Demo_Arduino/*/lvgl/
JC_Display_Firmware_7zoll/1-Demo/Demo_IDF/
JC_Display_Firmware_7zoll/7-Character&Picture_Molding_Tool/
+137
View File
@@ -1,3 +1,140 @@
Version 5.6.3:
- Bug-Fix: Beim Aufwecken über das Touch-Display blieb die Auswahl „Espresso oder Cold Extraction"
manchmal aus, während sie über den Schalter kam. Ursache waren zwei unterschiedliche Bedingungen für
ein und dieselbe Sache: Das Display prüfte zum ANZEIGEN der Auswahl zusätzlich, ob der Modus gerade
scharfschaltbar ist (cxArmable) - zum UNTERDRÜCKEN der Rückfrage der S3 („wakeEspresso") aber nicht.
Fiel diese Bedingung weg, zeigte der Dialog nur noch „Aufwecken", schloss aber trotzdem das
Auswahlfenster der S3: Die Frage war auf beiden Seiten weg. Der Schalterweg hatte diese Lücke nie,
weil er allein an der S3 hängt. Behoben in der Display-Firmware 1.4.2.
- Diagnose: /Cold-Extraction zeigt jetzt zusätzlich zum Live-Stand der Voraussetzungen auch das
LETZTE Aufwecken - über welchen Weg (Schalter/Timer/Web oder Display, dort mit getroffener Wahl)
und mit welchem Ergebnis (Fenster geöffnet, am Display bereits entschieden, Abfrage aus, kein
Display verbunden, Wartung/Reinigung/Tuning aktiv, nicht freigeschaltet). Hintergrund: Die
Live-Liste zeigt nur den jetzigen Stand - blieb die Frage aus, hatte sich die Lage bis zum
Nachsehen längst wieder geändert, und der Grund war nicht mehr zu ermitteln.
- Die Entscheidung über das Auswahlfenster ist als Bedingungskette geschrieben, damit jeder Zweig
seinen Grund hinterlässt (vorher eine einzige verknüpfte Bedingung ohne Rückmeldung).
Version 5.6.2:
- Bug-Fix: Die Abfrage beim Aufwecken erschien nicht, sobald der Cold-Extraction-Modus noch aktiv war -
und da er persistent ist, war er das nach dem ersten kalten Bezug dauerhaft. Die Bedingung „Modus ist
aus" war falsch gedacht („sonst gibt es nichts zu fragen"): Die Auswahl muss in BEIDE Richtungen gehen,
also auch von kalt zurück auf Espresso. Ohne das hing man nach dem ersten kalten Bezug im kalten Modus
fest, ohne beim Aufwecken je wieder gefragt zu werden.
- „Espresso" beendet jetzt einen laufenden Cold-Extraction-Modus, statt ihn nur nicht einzuschalten.
Sonst wäre die Auswahl eine Einbahnstraße gewesen.
- Das Auswahlfenster bleibt bei aktivem Modus offen (bisher wurde es sofort geschlossen); die Frage
lautet dann „weiter kalt oder zurück auf Espresso?".
- Die Diagnose auf /Cold-Extraction listet den Modus nicht mehr als Voraussetzung, sondern zeigt ihn
als Ist-Zustand an.
Version 5.6.1:
- Diagnose: Die Seite /Cold-Extraction zeigt jetzt live, welche Voraussetzungen der Abfrage beim
Aufwecken erfüllt sind (freigeschaltet, Abfrage eingeschaltet, Touch-Display verbunden, Modus
gerade aus, kein Wartungsmodus/Reinigung/Tuning) und ob das Auswahlfenster gerade offen ist.
Hintergrund: Die Abfrage hängt an fünf Bedingungen gleichzeitig - fehlt eine, passiert einfach
nichts, und es war von außen nicht erkennbar welche.
- Die Abfrage kommt jetzt auch nach einem Kaltstart, nicht nur nach dem Standby. Wer die Maschine
am Netzschalter einschaltet, durchläuft nie einen Standby-Übergang; dieser Weg war deshalb bisher
nicht abgedeckt. Startet die Maschine in den Standby hinein, bleibt es beim Standby-Übergang.
Version 5.6.0:
- Die Abfrage „Espresso oder Cold Extraction" kommt jetzt bei JEDEM Weg aus dem Standby, also auch beim
Aufwecken über den Schalter. Bisher war sie eine reine Vorab-Abfrage des Touch-Displays und erschien nur
beim Antippen des Standby-Bildschirms - beim Schalter ist der Standby aber schon beendet, bevor das
Display überhaupt fragen könnte. Dieser Weg war damit nie abgedeckt.
- Dafür hält die Steuerung nach dem Aufwecken ein Auswahlfenster offen (20 s): Der Wasserkreis heizt in
dieser Zeit nicht, das Display zeigt die Auswahl mit Countdown. Ohne Entscheidung wird anschließend
normal geheizt - es geht also nichts verloren, wenn niemand hinsieht.
- Das Fenster öffnet nur mit angeschlossenem Touch-Display (ohne Display könnte niemand wählen), bei
freigeschalteter Funktion und aktivierter Einstellung „Abfrage beim Aufwecken", und nur wenn der
Cold-Extraction-Modus nicht ohnehin schon aktiv ist. Es schließt sofort bei einer Entscheidung, bei
Bezug/Spülen oder erneutem Standby.
- Neue Aktion „wakeEspresso": beendet den Standby (falls noch aktiv) und schließt das Auswahlfenster.
Das Display sendet sie statt „deactivateStandby", sobald die Auswahl relevant ist - sonst würde nach
dem Aufwecken über den Dialog gleich nochmal gefragt.
- Neue State-Felder „cxWakeChoice" (Fenster offen) und „cxWakeChoiceSec" (Restsekunden).
Version 5.5.0:
- Neu: Beim Aufwecken aus dem Standby fragt das Touch-Display, ob normal geheizt (Espresso) oder kalt
extrahiert werden soll. Damit lässt sich das bei jedem Aufwecken neu entscheiden, ohne vorher die
Web-UI zu bedienen. Bei „Cold Extraction" wird der Modus scharf geschaltet, BEVOR der Standby endet -
der Wasserkreis heizt also gar nicht erst an.
- Abschaltbar über die neue Einstellung „Abfrage beim Aufwecken" auf /Cold-Extraction bzw. auf der
Cold-Extraction-Seite des Touch-Displays. Ohne angeschlossenes Display oder bei nicht freigeschalteter
Funktion hat sie keine Wirkung; ist sie aus, bleibt der bisherige Dialog („Aufwecken" / „Abbrechen").
- Neues UART-Feld „cxAskOnWake" im State und als Feld in saveBrew.
- Die Meldungstexte der kalten Extraktion sind jetzt mit echten Umlauten geschrieben („nicht möglich"
statt „nicht moeglich"). Für das OLED werden sie beim Rendern nach CP437 übersetzt (neue Hilfsfunktion
oledUmlauts) - dort hätte UTF-8 sonst zwei Fehlzeichen je Umlaut ergeben. Web-UI und Touch-Display
stellen UTF-8 ohnehin direkt dar.
Version 5.4.1:
- Bug-Fix: Während einer kalten Extraktion stand als Zielgewicht überall noch der Brew-by-Weight-Wert
des normalen Espresso-Bezugs. Betroffen waren OLED, Web-Dashboard und Touch-Display, weil alle drei
direkt brewByWeightTargetGrams anzeigten - die kalte Extraktion ersetzt Brew-by-Weight aber und stoppt
nach coldExtractionTargetGrams. Neue Hilfsfunktion activeTargetWeightGrams() liefert jetzt das Ziel
des aktuellen bzw. nächsten Bezugs; das UART-Feld "targetWeight" führt diesen Wert (bbwTarget bleibt
unverändert die Roh-Einstellung).
- Auf dem OLED wird während eines kalten Bezugs jetzt überhaupt Gewicht angezeigt: Die Anzeige hing an
„Brew-by-Weight aktiv", das bei kalter Extraktion typischerweise aus ist - trotz vorhandenem Ziel.
Version 5.4.0:
- Cold Extraction hat eine eigene Seite: /Cold-Extraction, eigener Eintrag im Menü. Auf /Brew-Control
steht nur noch ein Verweis darauf - die Seite war mit dem angehängten Block unübersichtlich geworden.
- Die neue Seite ist in Abschnitte gegliedert (Grundeinstellung, Vorbenetzung, Hauptbezug, Bezugsende,
Pumpenschutz), jeder mit einer kurzen Erklärung, warum der Wert so eingestellt gehört.
- Wichtig für bestehende Installationen: Ein Speichern auf /Brew-Control fasst die Cold-Extraction-
Einstellungen nicht mehr an. Vorher wertete dieser Handler die cx-Felder mit aus; nach dem Verschieben
hätte ein Speichern dort die Freischaltung abgeschaltet, weil die Checkbox im Formular fehlt.
- Das UART-Kommando "saveBrew" akzeptiert jetzt zusätzlich die Cold-Extraction-Felder (cxEnabled,
cxMaxTemp, cxPreInf, cxPreInfDuty, cxPulseMs, cxDuty, cxTarget, cxMaxSecs, cxPumpMaxRun, cxPumpRest) -
damit lässt sich die Funktion auch am Touch-Display vollständig einstellen.
- Das State-JSON liefert die restlichen Parameter mit (cxPreInfDuty, cxDuty, cxPulseMs, cxPumpMaxRun,
cxPumpRest), damit das Display seine Eingabefelder vorbelegen kann.
Version 5.3.6:
- Cold Extraction: Die Vorbenetzung läuft nicht mehr mit voller Pumpenleistung, sondern gepulst mit
eigener, niedriger Leistung (neue Einstellung „Pumpenleistung Vorbenetzung", Standard 10 %).
Messung an der Maschine: Nach 7 s Vorbenetzung kommen heiß 1-2 Tropfen, kalt bei voller Leistung
rund 10 g dünne Brühe ohne Crema - etwa zehnfacher Durchfluss trotz dreifacher Viskosität.
Ursache ist nicht die Viskosität, sondern das Kaffeebett: Kaltes Mehl quillt nicht, ist schlecht
benetzbar, es wandert kein Feinanteil und es lösen sich kaum Feststoffe, die das Bett verdichten.
Volle Leistung auf den trockenen kalten Puck legt deshalb sofort Kanäle an, die sich - anders als
bei heißem Bezug - nie wieder schließen; der restliche Bezug läuft daran vorbei statt zu extrahieren.
- Standard der Pumpenleistung im Hauptbezug von 30 % auf 20 % gesenkt (gleicher Grund: das kalte Bett
ist erheblich durchlässiger als ein heißes). Bestehende Installationen behalten ihren Wert.
- Der Erklärtext auf /Brew-Control benennt den Zusammenhang jetzt und weist darauf hin, dass die
Vorbenetzung deutlich schwächer eingestellt sein sollte als der Hauptbezug.
Version 5.3.5:
- Cold Extraction lässt sich jetzt auch im Standby und bei noch warmem Kessel scharf schalten.
Bisher galt „Standby aktiv" und „Kessel zu warm" auch fürs Aktivieren - das war falsch herum
gedacht: Der Modus schaltet die Heizung AB, man kam aber erst an den Schalter, nachdem die
Maschine aufgeweckt war und bereits zu heizen begonnen hatte. Genau der Ablauf, den die
Funktion vermeiden soll.
- Scharfschalten und Beziehen sind jetzt zwei getrennte Bedingungen:
- Scharfschalten (neues State-Feld „cxArmable"): blockiert nur durch fehlende Freischaltung,
Wartungsmodus, Reinigungsassistent, PID-Tuning oder einen laufenden Bezug/Spülvorgang.
- Bezug (unverändert „cxAllowed"): zusätzlich kein Standby, Kessel unter der Freigabeschwelle,
gültiger Sensorwert.
Ist der Modus scharf, der Bezug aber noch gesperrt, sagen Web-UI und Display das jetzt
ausdrücklich („Modus aktiv, Bezug noch gesperrt: …").
- Wird bei warmem Kessel aktiviert, geht die Wasserheizung sofort aus und der Kessel kühlt ab;
die Rückmeldung weist darauf hin, dass Bezüge bis zum Unterschreiten der Schwelle gesperrt sind.
Version 5.3.4:
- Cold Extraction weist jetzt deutlich darauf hin, wenn keine Waage aktiv/verbunden ist. Ohne Waage
greifen weder das Zielgewicht noch die Stillstands-Erkennung - der Bezug endet dann erst nach der
maximalen Bezugsdauer, die damit faktisch zur Dosierung wird. Bisher passierte das stillschweigend.
- Die Funktion bleibt ohne Waage bewusst nutzbar (Zeitsteuerung als Rückfallebene): Ein kurzzeitig
abgemeldeter HX711 soll die kalte Extraktion nicht komplett blockieren.
- Angezeigt wird das an vier Stellen: dauerhafter Warnhinweis auf /Brew-Control (dort ist das
Zielgewicht-Feld zusätzlich als „nur mit Waage wirksam" beschriftet), Hinweiszeile unter dem
Dashboard-Schalter, Statustext „Cold Extraction bereit (ohne Waage: Zeitsteuerung)" sowie eine
einmalige Meldung beim Aktivieren.
- Neuer Code im UART-Feld cxNotice: 4 = ohne Waage aktiviert (additiv, Protokoll bleibt v2).
Version 5.3.3:
- Bug-Fix (Cold Extraction, hätte die Funktion mit Waage komplett unbrauchbar gemacht): Die
Stillstands-Erkennung lief bereits während der Vorbenetzung mit. In dieser Phase sättigt der Puck
+36 -8
View File
@@ -322,18 +322,31 @@ Alle gültigen `action`-Strings aus `executeDashboardAction()` (Stand v4.8.1, Ze
| `startColdExtraction` | — | Modus aktivieren (Wasserkreis heizt nicht, Dampf bleibt möglich) |
| `stopColdExtraction` | — | Modus beenden |
| `toggleColdExtraction` | — | Modus umschalten |
| `wakeEspresso` | — | Standby beenden (falls aktiv) und das Auswahlfenster schließen, also „normal heizen" wählen (ab S3 5.6.0) |
Alle drei Aktionen werden abgewiesen (`ok:false` + `message`), solange die Bedingungen nicht
erfüllt sind — Hauptfall ist ein zu warmer Wasserkessel (`cxAllowed:false`). Der Modus ist
Für das Bedienelement zählt `cxArmable`, **nicht** `cxAllowed`: Der Modus schaltet die
Wasserheizung ab und muss deshalb auch im Standby und bei noch warmem Kessel scharf zu
stellen sein — sonst heizt die Maschine nach dem Aufwecken an, bevor man an den Schalter
kommt. `cxAllowed` gilt nur für den einzelnen Bezug; ist der Modus scharf und `cxAllowed`
false, sollte die UI „Modus aktiv, Bezug noch gesperrt" zeigen.
Ein Aufwecken direkt in die kalte Extraktion sendet `startColdExtraction` **vor**
`deactivateStandby` — in dieser Reihenfolge, damit der Wasserkreis gar nicht erst anheizt.
Die Aktionen werden abgewiesen (`ok:false` + `message`), solange die Bedingungen nicht
erfüllt sind. Der Modus ist
**persistent** (EEPROM) und überlebt Standby und Neustart; er endet nur durch explizites
Ausschalten, durch Entzug der Freischaltung auf `/Brew-Control` oder durch einen Werksreset.
Das P4-UI sollte `cxActive` deshalb dauerhaft sichtbar spiegeln — sonst wundert sich der
Benutzer, warum die Maschine nicht heizt. Ein laufender Bezug wird nie abgebrochen,
`stopColdExtraction` schlägt dann fehl.
Die Parameter der kalten Extraktion (Schwelle, Vorbenetzung, Pulsperiode, Pumpenleistung,
Zielgewicht, Timeout, Pumpenschutz) sind bewusst **nicht** über `saveBrew` erreichbar, sondern
nur über die Web-Seite `/Brew-Control`. Das Display schaltet den Modus, konfiguriert ihn nicht.
Die Parameter der kalten Extraktion sind ab S3 5.4.0 über `saveBrew` einstellbar (zusätzlich
zur Web-Seite `/Cold-Extraction`). Zulässige Felder: `cxEnabled` (bool), `cxMaxTemp`,
`cxPreInf`, `cxPreInfDuty`, `cxPulseMs`, `cxDuty`, `cxTarget`, `cxMaxSecs`, `cxPumpMaxRun`,
`cxPumpRest`, `cxAskOnWake` (bool). Es werden nur die tatsächlich gesendeten Felder übernommen; Grenzwerte prüft
die S3. Die aktuellen Werte stehen alle im `state`, damit das Display seine Eingabefelder
vorbelegen kann.
**Persistente Toggles**
@@ -383,11 +396,20 @@ Aus `getTouchUartStatus()`. Für sprach-unabhängige UI-Logik/Icons auf dem P4 v
| `cxEnabled` | bool | Funktion in der Web-UI freigeschaltet (sonst Bedienelement ausblenden) |
| `cxActive` | bool | Modus scharf: Wasserkreis heizt nicht |
| `cxShot` | bool | Der laufende Bezug ist eine kalte Extraktion |
| `cxAllowed` | bool | Freigabe **jetzt** (Wasserkessel kalt genug, kein Standby/Wartung/Tuning) |
| `cxAllowed` | bool | **Bezug** jetzt möglich (Wasserkessel kalt genug, kein Standby/Wartung/Tuning) |
| `cxArmable` | bool | **Modus umschaltbar** — auch im Standby und bei warmem Kessel (ab S3 5.3.5) |
| `cxResting` | bool | Zwangspause des Pumpenschutzes läuft (Bezug läuft weiter) |
| `cxMaxTemp` | float | Freigabeschwelle in °C (für eigene Hinweistexte am Display) |
| `cxTarget` | float | Zielgewicht in g |
| `cxPreInf` | float | Vorbenetzung in s (für die Phasenleiste des kalten Bezugs) |
| `cxPreInf` | float | Vorbenetzung in s (auch für die Phasenleiste des kalten Bezugs) |
| `cxPreInfDuty` | float | Pumpenleistung Vorbenetzung in % (ab S3 5.4.0) |
| `cxDuty` | float | Pumpenleistung Hauptbezug in % (ab S3 5.4.0) |
| `cxPulseMs` | uint16 | Pulsperiode in ms (ab S3 5.4.0) |
| `cxPumpMaxRun` | float | Pumpenschutz: max. kumulierte Laufzeit in s (ab S3 5.4.0) |
| `cxPumpRest` | float | Pumpenschutz: Zwangspause in s (ab S3 5.4.0) |
| `cxAskOnWake` | bool | Beim Aufwecken aus dem Standby nach Espresso/Cold Extraction fragen (ab S3 5.5.0) |
| `cxWakeChoice` | bool | Auswahlfenster nach dem Aufwecken offen — Wasserkreis heizt solange nicht (ab S3 5.6.0) |
| `cxWakeChoiceSec` | long | Restsekunden der Bedenkzeit |
| `cxMaxSecs` | float | Sicherheits-Timeout des Bezugs in s |
| `cxBlock` | uint8 | Sperrgrund (siehe unten), `0` = frei |
| `cxNotice` | uint8 | transiente Meldung, `0` = keine (läuft nach ~6 s selbst ab) |
@@ -396,7 +418,13 @@ Aus `getTouchUartStatus()`. Für sprach-unabhängige UI-Logik/Icons auf dem P4 v
`4` Reinigungsassistent, `5` PID-Tuning, `6` Sensorfehler, `7` Wasserkessel zu warm.
`cxNotice`: `0` keine, `1` Start abgewiesen (Grund steht in `cxBlock`), `2` Bezug wegen
fehlenden Zulaufs beendet, `3` Umschalten während eines Bezugs abgelehnt.
fehlenden Zulaufs beendet, `3` Umschalten während eines Bezugs abgelehnt, `4` ohne Waage
aktiviert (ab S3 5.3.4).
**Ohne Waage** (`scaleEnabled`/`scaleConnected` false) läuft die kalte Extraktion rein
zeitgesteuert: `cxTarget` ist wirkungslos und die Stillstands-Erkennung ist inaktiv, es
bleibt `cxMaxSecs` als Ende. Das ist Absicht (ein kurz abgemeldeter HX711 soll die Funktion
nicht blockieren) und **kein** Sperrgrund — das Display sollte es aber dauerhaft anzeigen.
Bewusst **Codes statt Klartext**: Die State-Zeile muss unter dem Zeilenlimit des P4 bleiben
(`PROTO_RX_LINE_MAX`; sie liegt real bereits bei ~2,1 KB), und die Texte gehören ins Display —
+633 -110
View File
File diff suppressed because it is too large Load Diff
+132
View File
@@ -1,3 +1,135 @@
Version 1.6.0:
- Neu: Dieselbe Firmware läuft jetzt auch auf einem Waveshare ESP32-P4-Pico mit dem
Waveshare 7inch DSI LCD (H) (1280x720). Umgeschaltet wird über „JC_PANEL_TYPE" in
config.h; WS_PANEL_7H ist ab dieser Version die Vorgabe, JC_PANEL_43 und JC_PANEL_70
bleiben unverändert verfügbar.
- Das Waveshare-Panel hängt hinter einer Bridge, die ausschließlich RGB888 annimmt. LVGL
rechnet für dieses Panel deshalb mit 24 statt 16 Bit Farbtiefe. lv_conf.h liest dazu
JC_PANEL_TYPE aus config.h, damit Sketch und LVGL-Bibliothek garantiert dieselbe
Farbtiefe benutzen; display_hal.cpp bricht den Build ab, falls sie auseinanderlaufen.
Der Panelwechsel muss aus diesem Grund in config.h erfolgen und nicht per Compiler-Flag.
- Bringup ohne Hersteller-Panel-Treiber: DSI-Bus, Kommandokanal und DPI-Panel werden direkt
angelegt. Entscheidend ist die Legacy-Referenztaktquelle PLL_F20M - mit der Standardquelle
bleibt der Bildschirm auf ESP32-P4 Rev. 1.3 / ECO2 schwarz. Die Zeitbasis (2 Lanes zu
1250 Mbit/s, 80 MHz DPI-Takt, Austastlücken 64/64/64, rund 60 Hz) steht als WS7_*-Werte
in pins_config.h und ist dort anpassbar, falls das Bild reißt.
- Panel-Freigabe und Helligkeit laufen beim Waveshare nicht über einen PWM-Pin, sondern über
Register der Bridge auf I2C-Adresse 0x45. Die Hintergrundbeleuchtung wird bewusst erst
eingeschaltet, wenn die DSI-Ausgabe steht - sonst blitzt beim Start Bildrauschen auf.
Standby-Abdunklung und „Uhrzeit im Standby" funktionieren wie bei den JC-Panels.
- Touch: Der GT9271 des Waveshare-Panels benutzt dieselbe Registerbelegung wie der GT911,
liegt aber auf I2C-Adresse 0x14 und meldet im Hochformat. X und Y werden deshalb getauscht
und auf die Bildfläche begrenzt; Ausreißer des Controllers landen nicht mehr als Klick am
Bildrand. Sollte die Bedienung spiegelverkehrt reagieren, lässt sich das mit
WS7_TOUCH_MIRROR_RAW_X / _Y in pins_config.h umdrehen.
- Der Framebuffer wächst auf 3 x 2,8 MB (rund 8,3 MB PSRAM). Auf dem P4-Pico mit 32 MB
PSRAM ist das unkritisch.
- Die UART-Pins zur Hauptplatine sind für das Waveshare-Board getrennt eingestellt, weil
dessen 40-poliger Header eine andere Belegung hat als die JC-Panels: TX = GPIO17,
RX = GPIO18 (JC-Panels unverändert TX = GPIO33, RX = GPIO31).
Version 1.5.0:
- Neu: Dieselbe Firmware unterstützt jetzt auch das 7-Zoll-Display Guition JC1060P470C-I-W
(JD9165, 1024x600) zusätzlich zum bisherigen 4,3-Zöller JC4880P443C-I-W (ST7701, 480x800).
Umgeschaltet wird über „JC_PANEL_TYPE" in config.h (JC_PANEL_43 / JC_PANEL_70) oder per
Compiler-Flag -DJC_PANEL_TYPE=70. Vorgabe bleibt das 4,3-Zoll-Panel, dessen Verhalten sich
nicht ändert.
- Panel-Treiber esp_lcd_jd9165.c/.h aus dem Hersteller-Paket übernommen; board_bringup.c legt
je nach Panel den passenden DSI-Bus, die DPI-Zeitbasis und den Herstellertreiber an.
- pins_config.h liefert Auflösung, LCD-Reset-Pin (4,3": GPIO5, 7,0": GPIO27) und Ausrichtung
panelabhängig. Das 7-Zoll-Panel ist nativ Querformat, deshalb entfällt dort die 270-Grad-
Rotation samt PPA-Beschleunigung.
- Der GT911 des 7-Zöllers meldet Rohkoordinaten im Raster 800x480 statt 1024x600. Die
Umrechnung hängt am process_coordinates-Hook von esp_lcd_touch, damit die Herstellerdatei
esp_lcd_touch_gt911.c unverändert bleibt. Ohne sie wäre nur der linke obere Bildteil
bedienbar.
- Die Oberfläche rechnet nicht mehr mit fest verdrahteten 480 Pixeln Höhe, sondern mit
DISP_VER_RES. Layout und Schriften bleiben sonst unverändert: Das Dashboard arbeitet fast
durchgehend mit Prozentmaßen, und die feineren Pixelabstände des 7-Zöllers (170 statt 213 dpi)
lassen dieselben Schriftgrößen physisch größer erscheinen.
- Die UART-Verkabelung ist bei beiden Panels gleich (P4 GPIO33 = TX, GPIO31 = RX). Beim
7-Zöller liegen diese Pins allerdings auf der FPC-Buchse FPC4 (1,0 mm, 16-polig) statt auf
einer Stiftleiste - dort wird ein Adapterkabel gebraucht.
Version 1.4.2:
- Bug-Fix: Beim Aufwecken über Antippen erschien die Auswahl „Espresso oder Cold Extraction"
manchmal nicht. Der Dialog entschied nach einer anderen Bedingung, ob er die Auswahl ANZEIGT
(verlangte zusätzlich cxArmable), als er beim Absenden benutzte, um die Rückfrage der S3 zu
UNTERDRÜCKEN. In der Lücke dazwischen zeigte er nur „Aufwecken", sendete aber „wakeEspresso" -
und damit war die Frage auch auf der S3 erledigt. Beide Stellen benutzen jetzt dieselbe Funktion
wake_choice_offered().
- Hat der Dialog die Auswahl nicht angeboten, sendet er wieder „deactivateStandby". Dann öffnet die
S3 ihr eigenes Auswahlfenster, und die Frage kommt trotzdem - derselbe Weg wie beim Aufwecken über
den Schalter. Die Auswahl kann damit nicht mehr ersatzlos entfallen.
Version 1.4.1:
- Passend zu S3 5.6.2: Die Auswahl beim Aufwecken erscheint auch dann, wenn der Cold-Extraction-Modus
bereits aktiv ist - sie geht in beide Richtungen. „Espresso" schaltet den Modus dann ab.
- Der Cold-Extraction-Knopf zeigt den Ist-Zustand: Bei aktivem Modus ist er hervorgehoben und heißt
„Cold Extraction (aktiv lassen)", sonst „Cold Extraction (Wasser bleibt kalt)".
Version 1.4.0:
- Die Auswahl „Espresso oder Cold Extraction" erscheint jetzt auch beim Aufwecken über den Schalter
(braucht S3-Firmware ab 5.6.0). Meldet die S3 ein offenes Auswahlfenster (cxWakeChoice), blendet das
Display den Dialog von sich aus ein - ohne Antippen - und zeigt die verbleibende Bedenkzeit als
Countdown. „Abbrechen" entfällt in diesem Fall, weil es nichts abzubrechen gibt: Läuft die Zeit ab,
heizt die Maschine normal auf.
- Der Espresso-Knopf sendet „wakeEspresso" statt „deactivateStandby", sobald die Auswahl relevant ist.
Damit wird die Entscheidung auf der S3 vermerkt und nicht direkt danach erneut gefragt.
- Der Cold-Extraction-Knopf beendet den Standby nur noch, wenn er überhaupt aktiv ist - im Auswahlfenster
nach dem Aufwecken schaltet er nur den Modus.
Version 1.3.0:
- Der Aufweck-Dialog wird zur Auswahl: „Womit aufwecken?" mit den Knöpfen „Espresso" (heizt normal auf)
und „Cold Extraction (Wasser bleibt kalt)". Braucht S3-Firmware ab 5.5.0.
Bei „Cold Extraction" wird erst der Modus scharf geschaltet und dann der Standby beendet, damit der
Wasserkreis nicht zwischendurch anheizt.
- Steuerbar über die neue Einstellung „Beim Aufwecken fragen" (Cold-Extraction-Seite, Abschnitt
Grundeinstellung; identisch zur Web-UI). Ist sie aus, erscheint wieder der bisherige Dialog mit
„Aufwecken" und „Abbrechen" - der Zwischenschritt entfällt dann komplett.
Version 1.2.1:
- Bug-Fix: Während einer kalten Extraktion zeigten Waage-Kachel und Live-Bezugsschirm das Zielgewicht
des normalen Espresso-Bezugs (bbwTarget) statt des Ziels der kalten Extraktion (cxTarget). Außerdem
erschienen Gewichtsanzeige und Fortschrittsbalken nur, wenn Brew-by-Weight aktiv war - bei kalter
Extraktion ist das typischerweise aus, obwohl ein Ziel existiert. Beide Anzeigen nutzen jetzt
cxTarget, sobald cxShot gesetzt ist.
Version 1.2.0:
- Neue Seite „Cold Extraction" mit eigenem Menüeintrag (braucht S3-Firmware ab 5.4.0). Dort stehen jetzt
alle Parameter der kalten Extraktion und lassen sich direkt am Display ändern: Freigabetemperatur,
Vorbenetzung (Dauer + Pumpenleistung), Hauptbezug (Pulsperiode + Pumpenleistung), Zielgewicht,
maximale Bezugsdauer sowie der Pumpenschutz. Gespeichert wird über das erweiterte saveBrew-Kommando.
- Der Modus-Schalter ist von der Temperatur-Seite auf die neue Seite umgezogen (eine Stelle statt zwei).
Der Knopf „Aufwecken mit Cold Extraction" im Standby-Dialog bleibt unverändert.
- Brew-Control-Seite neu gegliedert: Statt einer durchlaufenden Liste unter Zwischenüberschriften liegt
jede Funktion (Pre-Infusion, Brew-by-Time, Brew-by-Weight, Dampf-Timer) jetzt in einer eigenen Karte
mit Titel und einer Zeile, die erklärt, was sie tut. Die neue Cold-Extraction-Seite nutzt dieselbe
Gliederung.
- Eingabefelder werden nur beim Öffnen der Seite aus dem Zustand gefüllt, damit laufende State-Pushes
eine begonnene Eingabe nicht überschreiben (gleiches Verhalten wie auf der Brew-Control-Seite).
Version 1.1.3:
- Aufweck-Dialog hat einen dritten Knopf: „Aufwecken mit Cold Extraction" (braucht S3 ab 5.3.5).
Er stellt den Modus scharf und beendet erst danach den Standby - in dieser Reihenfolge, damit
der Wasserkreis gar nicht erst anheizt. Ohne diesen Weg musste man normal aufwecken (Maschine
heizt an) und den Modus danach umschalten. Der Knopf erscheint nur, wenn die Funktion
freigeschaltet und umschaltbar ist und der Modus nicht ohnehin schon läuft.
- Der Schalter „Cold Extraction" auf der Temperatur-Seite hängt jetzt am neuen State-Feld
cxArmable statt an cxAllowed: umschaltbar auch im Standby und bei warmem Kessel, gesperrt nur
während eines laufenden kalten Bezugs. Bei älterer S3-Firmware fällt das Feld auf cxEnabled
zurück, der Schalter bleibt also bedienbar.
- Erklärzeile im Standby: „Nach dem Aufwecken bleibt der Wasserkreis aus."
Version 1.1.2:
- Cold Extraction: Fehlt eine nutzbare Waage, weist das Display jetzt dauerhaft darauf hin.
Ohne Waage greifen weder Zielgewicht noch Stillstands-Erkennung, der Bezug endet erst nach der
maximalen Bezugsdauer (braucht S3-Firmware ab 5.3.4).
- Statuszeile: „Cold Extraction aktiv - ohne Waage, Ende nach Zeit" (gelb) statt des blauen
Normalhinweises; beim Aktivieren zusätzlich kurz die Meldung zum neuen cxNotice-Code 4.
- Erklärzeile unter dem Schalter und die Parameter-Anzeige auf der Brew-Seite ergänzen den
Hinweis; bei aktivem Modus ohne Waage nennt die Zeile die Zeit statt des Zielgewichts.
Version 1.1.1:
- Compiler-Warnungen des P4-Builds aufgeräumt (rein technisch, keine Funktionsänderung):
- „LV_FS_DEFAULT_DRIVE_LETTER is deprecated": Die Option heißt ab LVGL 9.3
+5 -1
View File
@@ -268,7 +268,11 @@ static void enterUpdateMode() {
void setup() {
DBG_SERIAL.begin(DBG_BAUD);
delay(50);
DBG_SERIAL.println(F("\nJC4880P443C-I-W Display-Firmware startet..."));
#if JC_PANEL_TYPE == JC_PANEL_70
DBG_SERIAL.println(F("\nJC1060P470C-I-W (7,0 Zoll) Display-Firmware startet..."));
#else
DBG_SERIAL.println(F("\nJC4880P443C-I-W (4,3 Zoll) Display-Firmware startet..."));
#endif
// --- Boot-Loop-Schutz: Fruehstart-Zaehler in NVS erhoehen; Schwelle -> Update-Modus ---
{
+106 -26
View File
@@ -1,33 +1,95 @@
# JC4880P443C-I-W — Touch-Display-Firmware (ESP32-P4)
# Touch-Display-Firmware (ESP32-P4)
Client-Firmware für das Guition-Smart-Display **JC4880P443C-I-W**, die als
UART-Touch-Bedienung für die Dual-PID-Siebträgersteuerung dient.
Client-Firmware für die Touch-Displays, die als UART-Bedienung für die
Dual-PID-Siebträgersteuerung dienen:
- Guition **JC4880P443C-I-W** (4,3", ST7701, 480×800)
- Guition **JC1060P470C-I-W** (7,0", JD9165, 1024×600)
- **Waveshare ESP32-P4-Pico** + **Waveshare 7inch DSI LCD (H)** (1280×720)
Alle drei teilen sich denselben Sketch; das Zielpanel wird über **`JC_PANEL_TYPE`** in
[`config.h`](config.h) gewählt.
Die **Hauptplatine (ESP32-S3, `Dual_PID_FastHeatUp.ino`)** bleibt die Steuerzentrale
inkl. WLAN/Webinterface. Dieses Display ist ein **reiner UART-Client**: es empfängt den
Zustand und sendet Bedienkommandos. Protokoll: siehe
[`../Doku/JC-Display_UART-Protokoll.md`](../Doku/JC-Display_UART-Protokoll.md).
## Hardware (laut Vendor-Paket im Ordner `JC4880P443C_I_W/`)
## Panel-Auswahl
| Komponente | Bauteil / Pin |
|---|---|
| Haupt-MCU (UI) | ESP32-P4 |
| WLAN/BT (hier ungenutzt) | ESP32-C6 |
| Display | ST7701, 480×800 IPS, **MIPI-DSI** |
| Touch | GT911, I²C **SDA=GPIO7 / SCL=GPIO8** |
| Backlight | **GPIO23** (LEDC-PWM) · LCD-Reset **GPIO5** |
| Freie Expansion-IO (JP1) | GPIO 52/51/50/49/35/34/33/32/31/30/29/28, ES_I2C, +3V3/+5V/GND |
In [`config.h`](config.h):
```c
#define JC_PANEL_TYPE WS_PANEL_7H // Waveshare P4-Pico + 7inch DSI LCD (H) (Vorgabe)
#define JC_PANEL_TYPE JC_PANEL_43 // 4,3" JC4880P443C-I-W
#define JC_PANEL_TYPE JC_PANEL_70 // 7,0" JC1060P470C-I-W
```
Der Schalter steuert Panel-Treiber, Auflösung, Rotation, Reset-/Backlight-Ansteuerung,
die Touch-Umrechnung **und die Farbtiefe von LVGL**; UI und Protokoll bleiben identisch.
> **Nur hier umschalten, nicht per Compiler-Flag.** [`lv_conf.h`](lv_conf.h) liest
> `JC_PANEL_TYPE` aus `config.h`, weil das Waveshare-Panel RGB888 verlangt und LVGL
> deshalb mit 24 statt 16 Bit übersetzt werden muss. Ein `-D`-Flag der IDE erreicht die
> LVGL-Bibliothek nicht — Sketch und Bibliothek liefen dann mit verschiedenen Farbtiefen.
> `display_hal.cpp` bricht den Build mit einer `static_assert` ab, falls das doch passiert.
## Hardware
Vendor-Pakete: 4,3" im Ordner `JC4880P443C_I_W/`, 7,0" im Repo-Ordner
`../JC_Display_Firmware_7zoll/`.
| Komponente | 4,3" JC4880P443C-I-W | 7,0" JC1060P470C-I-W | Waveshare P4-Pico + 7" DSI (H) |
|---|---|---|---|
| Haupt-MCU (UI) | ESP32-P4 | ESP32-P4 | ESP32-P4 (32 MB PSRAM, 32 MB Flash) |
| WLAN/BT (hier ungenutzt) | ESP32-C6 | ESP32-C6 | — (kein Funkmodul) |
| Display | ST7701, 480×800 IPS, MIPI-DSI | JD9165, 1024×600 IPS, MIPI-DSI | 1280×720 IPS, MIPI-DSI 2 Lanes über Waveshare-Bridge |
| Farbformat | RGB565 | RGB565 | **RGB888** (Bridge nimmt nichts anderes) |
| LVGL-Ausrichtung | Rotation **270°** (PPA) → 800×480 | **keine** Rotation, nativ 1024×600 | **keine** Rotation, nativ 1280×720 |
| Touch | GT911, I²C SDA=GPIO7 / SCL=GPIO8 | GT911, I²C SDA=GPIO7 / SCL=GPIO8 | **GT9271** (I²C-Adresse **0x14**), SDA=GPIO7 / SCL=GPIO8 |
| Touch-Rohkoordinaten | 480×800 (= Panelraster) | **800×480** → wird auf 1024×600 skaliert | **720×1280** (Hochformat) → X/Y werden getauscht |
| LCD-Reset | GPIO5 | **GPIO27** | kein Reset-Pin (macht die Bridge) |
| Backlight | GPIO23 (LEDC-PWM) | GPIO23 (LEDC-PWM) | **Bridge-Register** auf I²C **0x45** (`AD`/`AB`/`AA`) |
| Framebuffer (3×) | 3 × 750 KB ≈ 2,3 MB PSRAM | 3 × 1,2 MB ≈ 3,6 MB PSRAM | 3 × 2,8 MB ≈ **8,3 MB** PSRAM |
| Freie Expansion-IO | Stiftleiste **JP1**: GPIO 52/51/50/49/35/34/33/32/31/30/29/28 | FPC 1,0 mm 16-pol. **FPC4**: GPIO 34/33/32/31/30/29/28 · **FPC3**: 48/47/46/45/5/4/3/2 | 40-poliger Header im Raspberry-Pi-Pico-Format; belegt sind I²C (7/8), TF-Karte (9/10/11/12/13/53) und Debug-UART0 (37/38) |
| UART zur Hauptplatine | TX GPIO33 / RX GPIO31 | TX GPIO33 / RX GPIO31 | **TX GPIO17 / RX GPIO18** |
### Waveshare 7" (H) — Besonderheiten des Bringups
Das Panel hängt hinter einer Waveshare-Bridge und braucht **keinen** Hersteller-Panel-Treiber;
`board_bringup.c` legt DSI-Bus, DBI-Kommandokanal und DPI-Panel direkt an:
- Referenztakt der D-PHY = **`PLL_F20M`** (Legacy). Mit der Standardquelle bleibt das Bild
auf ESP32-P4 Rev. 1.3 / ECO2 schwarz.
- 2 Lanes à 1250 Mbit/s, DPI-Takt 80 MHz, Austastlücken 64/64/64 (≈ 60 Hz).
Alle Werte stehen in [`pins_config.h`](pins_config.h) (`WS7_*`) und lassen sich dort ändern,
falls das Bild reißt oder flimmert — 1280×720 in RGB888 belastet den PSRAM spürbar.
- Reihenfolge: Bridge-Grundinitialisierung (`C0`/`C2`/`AC`) → DSI → DCS `MADCTL`/`Sleep Out`/
`Display On` → DPI-Start → **erst dann** Hintergrundbeleuchtung. Wird das Licht früher
eingeschaltet, blitzt beim Start Bildrauschen auf.
> Beim 7-Zöller ist die Expansion-IO **kein Stiftleisten-Header**, sondern eine
> FPC-Buchse (1,0 mm Raster, 16-polig) — für die UART-Verdrahtung wird ein
> FPC-Breakout/Adapterkabel gebraucht. Schaltplan: `../JC_Display_Firmware_7zoll/5-Schematic/4_CONN.png`.
## Verkabelung zur Hauptplatine (UART, gekreuzt)
Pins aus dem Expand-IO-Header **JP1** (Schaltplan `JC4880P443C_I_W/5-Schematic/4_USB&IO.png`):
Für **beide JC-Panels** dieselben P4-Pins (4,3": Header JP1, Schaltplan
`JC4880P443C_I_W/5-Schematic/4_USB&IO.png` · 7,0": FPC-Buchse FPC4):
```
S3 GPIO43 (TX) ----> P4 GPIO31 (RX) (DISPLAY_UART_RX_PIN)
S3 GPIO44 (RX) <---- P4 GPIO33 (TX) (DISPLAY_UART_TX_PIN)
S3 GND ------ P4 GND
```
**Waveshare ESP32-P4-Pico** (40-poliger Header):
```
S3 GPIO43 (TX) ----> P4 GPIO18 (RX) (DISPLAY_UART_RX_PIN)
S3 GPIO44 (RX) <---- P4 GPIO17 (TX) (DISPLAY_UART_TX_PIN)
S3 GND ------ P4 GND
```
> Die rechte JP1-Spalte (`C6_U01TXD`/`C6_I10RXD`/…) gehört zum **ESP32-C6** — nicht
> verwenden. 3,3-V-Logik beidseitig → keine Pegelwandlung nötig.
@@ -47,9 +109,10 @@ Pins aus dem Expand-IO-Header **JP1** (Schaltplan `JC4880P443C_I_W/5-Schematic/4
- **Arduino-ESP32** ≥ 3.1 (mit ESP32-P4-Support), Board: *ESP32P4 Dev Module*,
**PSRAM aktiviert**.
- **LVGL** 9.x + `lv_conf.h`. Tipp: die `lv_conf.h` aus dem Vendor-Beispiel
(`JC4880P443C_I_W/1-Demo/arduino_examples/lvgl_v9_sw_rotation/lv_conf.h`) übernehmen
und `LV_FONT_MONTSERRAT_40` aktivieren (große Temperaturanzeige).
- **LVGL** 9.x. Die `lv_conf.h` **im Sketch-Ordner** ist die maßgebliche; sie wird über den
Include-Pfad des Sketches gefunden und leitet `LV_COLOR_DEPTH` aus `JC_PANEL_TYPE` ab.
Liegt zusätzlich eine alte `lv_conf.h` neben dem `lvgl`-Ordner der Bibliotheken, wird sie
nicht benutzt — sie darf dort aber auch nicht stören.
- **ArduinoJson** 7.x.
## Zwei Betriebsmodi (`JC_USE_REAL_PANEL` in `display_hal.h`)
@@ -68,13 +131,23 @@ Sketch-Ordner kopieren**:
pins_config.h
lvgl_port_v9.h
lvgl_port_v9.c
src/lcd/ (komplett: esp_lcd_st7701*.c/.h, st7701_lcd.*)
src/lcd/ (esp_lcd_st7701*.c/.h + st7701_lcd.* sowie esp_lcd_jd9165.c/.h)
src/touch/ (komplett: esp_lcd_touch*.c/.h, gt911_touch.*)
```
Der Init-Ablauf (MIPI-DSI-PHY-LDO, ST7701, GT911, LEDC-Backlight, `lvgl_port_init`) ist
1:1 aus dem Vendor-Beispiel `lvgl_sw_rotation.c` übernommen — nur der Demo-Aufruf
(`lv_demo_widgets()`) wurde durch `ui_init()` ersetzt.
Diese Dateien liegen bereits im Sketch. `esp_lcd_jd9165.*` stammt aus dem
7-Zoll-Vendor-Paket
(`../JC_Display_Firmware_7zoll/1-Demo/Demo_Arduino/1_2_Lvgl_V9/.../src/lcd/`); je nach
`JC_PANEL_TYPE` wird nur einer der beiden Panel-Treiber eingebunden.
Der Init-Ablauf (MIPI-DSI-PHY-LDO, ST7701 bzw. JD9165, GT911, LEDC-Backlight,
`lvgl_port_init`) ist 1:1 aus dem Vendor-Beispiel `lvgl_sw_rotation.c` übernommen — nur
der Demo-Aufruf (`lv_demo_widgets()`) wurde durch `ui_init()` ersetzt.
Die Touch-Skalierung des 7-Zöllers hängt in `board_bringup.c` am
`process_coordinates`-Hook von `esp_lcd_touch`, damit die Vendor-Datei
`esp_lcd_touch_gt911.c` unverändert bleibt (im Vendor-Demo ist die Umrechnung direkt in
den Treiber gepatcht).
**Wichtig:** Im realen Modus läuft LVGL in einem **eigenen FreeRTOS-Task**. UI-Zugriffe aus
der Arduino-`loop()` (z. B. `ui_update` bei jedem state-Push) sind daher in
@@ -82,10 +155,16 @@ der Arduino-`loop()` (z. B. `ui_update` bei jedem state-Push) sind daher in
bereits umgesetzt.
### Ausrichtung
Das Vendor-Beispiel nutzt **90° (Querformat 800×480)**. Das hiesige Dashboard ist für
**Hochformat 480×800** ausgelegt → in `pins_config.h`
`EXAMPLE_LVGL_PORT_ROTATION_DEGREE_` auf **0** setzen (oder das UI-Layout in `ui.cpp` auf
Querformat anpassen).
Das Dashboard ist **Querformat**. Beim 4,3-Zöller (physisch 480×800) wird das per
LVGL-Rotation **270°** mit PPA-Beschleunigung erreicht → 800×480. Der 7-Zöller ist nativ
1024×600, dort steht die Rotation auf **0** und PPA ist abgeschaltet. Beides setzt
`pins_config.h` automatisch anhand von `JC_PANEL_TYPE`.
Das Layout arbeitet fast durchgehend mit `LV_PCT`/`LV_SIZE_CONTENT` und skaliert
deshalb mit. Feste Pixelmaße (Rundinstrumente, Thermometer, Buttons) bleiben gleich groß
— auf dem 7-Zöller entsteht dadurch mehr Luft. Die Schriftgrößen bleiben ebenfalls
unverändert: 4,3" ≈ 213 dpi, 7,0" ≈ 170 dpi, gleiche Pixelhöhe wirkt dort physisch
größer.
## Status
@@ -96,8 +175,9 @@ Querformat anpassen).
(Farbe nach `statusKey`), Verbindungs- & WLAN-Anzeige, Bezugs-Timer, Bedien-Buttons.
- Senden für `setPid`, `startAutotune`/`stopAutotune`, `getProfileDetails` vorhanden.
**Board-Bringup:** auf die konkreten JC-Pins/Treiber gesetzt (ST7701/GT911, Backlight,
I²C) — sobald die BSP-Dateien kopiert und `JC_USE_REAL_PANEL=1` gesetzt ist, lauffähig.
**Board-Bringup:** auf die konkreten JC-Pins/Treiber gesetzt (ST7701 bzw. JD9165, GT911,
Backlight, I²C) — sobald die BSP-Dateien kopiert und `JC_USE_REAL_PANEL=1` gesetzt ist,
lauffähig.
**Noch offen (UI-Ausbau):** weitere Screens — Einstellungen (Brew/Service/Sensor),
Profil-Auswahl/-Editor, PID/AutoTune-Bedienung.
+231 -8
View File
@@ -1,5 +1,8 @@
// =====================================================================================
// board_bringup.c - ESP32-P4 / ST7701 / GT911 Hardware-Bringup (JC4880P443C-I-W)
// board_bringup.c - ESP32-P4 Hardware-Bringup
// JC_PANEL_43 = JC4880P443C-I-W (ST7701, 480x800, GT911)
// JC_PANEL_70 = JC1060P470C-I-W (JD9165, 1024x600, GT911)
// WS_PANEL_7H = Waveshare ESP32-P4-Pico + 7inch DSI LCD (H) (1280x720, GT9271)
// =====================================================================================
// BEWUSST eine .c-Datei (wird als C kompiliert): die esp_lcd-Init-Makros nutzen
// Compound-Literals und Out-of-Order-Designated-Initializer, die in C++ (Arduino .cpp)
@@ -28,24 +31,96 @@
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_mipi_dsi.h"
#include "src/touch/esp_lcd_touch_gt911.h"
#if JC_PANEL_TYPE == WS_PANEL_7H
// Das Waveshare-Panel braucht keinen Hersteller-Panel-Treiber: es wird direkt als
// DPI-Panel angelegt und mit den drei Standard-DCS-Befehlen geweckt.
#include "hal/mipi_dsi_types.h"
#elif JC_PANEL_TYPE == JC_PANEL_70
#include "src/lcd/esp_lcd_jd9165.h"
#else
#include "src/lcd/esp_lcd_st7701.h"
#endif
#include "pins_config.h"
#include "lvgl_port_v9.h"
#define BSP_MIPI_DSI_PHY_PWR_LDO_CHAN (3)
#define BSP_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV (2500)
#define BSP_LCD_H_RES (480)
#define BSP_LCD_V_RES (800)
#define BSP_LCD_H_RES (LCD_H_RES) // native Panel-Aufloesung
#define BSP_LCD_V_RES (LCD_V_RES)
#define BSP_I2C_NUM (I2C_NUM_1)
#define BSP_I2C_SDA (GPIO_NUM_7)
#define BSP_I2C_SCL (GPIO_NUM_8)
#define BSP_LCD_TOUCH_RST (GPIO_NUM_NC)
#define BSP_LCD_TOUCH_INT (GPIO_NUM_NC)
#define BSP_LCD_RST (GPIO_NUM_5)
#define BSP_LCD_BACKLIGHT GPIO_NUM_23
#define BSP_LCD_RST ((gpio_num_t)LCD_RST)
#define BSP_LCD_BACKLIGHT ((gpio_num_t)LCD_LED)
#define LCD_LEDC_CH LEDC_CHANNEL_0
// Bytes je Pixel im Framebuffer. Das Waveshare-Panel laeuft in RGB888 (LVGL ebenfalls,
// siehe lv_conf.h), die JC-Panels in RGB565.
#if JC_PANEL_TYPE == WS_PANEL_7H
#define BSP_LCD_FB_BYTES_PER_PX (3)
#else
#define BSP_LCD_FB_BYTES_PER_PX (2)
#endif
static i2c_master_bus_handle_t s_i2c_handle = NULL;
// =====================================================================================
#if JC_PANEL_TYPE == WS_PANEL_7H
// =====================================================================================
// Waveshare 7inch DSI LCD (H): Panel-Freigabe und Helligkeit haengen an einer Bridge
// auf I2C-Adresse 0x45 - es gibt weder einen Reset- noch einen Backlight-GPIO.
// Registerfolge wie im Linux-Treiber:
// Grundinitialisierung: C0=01, C2=01, AC=01 (vor dem DSI-Bringup)
// Freigabe/Helligkeit: AD=01, AB=255-Helligkeit, AA=01 (ERST wenn DSI laeuft,
// sonst bleibt das Bild dunkel bzw. flackert beim Start)
// =====================================================================================
static i2c_master_dev_handle_t s_bridge_handle = NULL;
static void ws7_bridge_write(uint8_t reg, uint8_t value)
{
if (s_bridge_handle == NULL) return;
uint8_t buf[2] = { reg, value };
i2c_master_transmit(s_bridge_handle, buf, sizeof(buf), 100);
}
// Bridge am I2C-Bus anmelden und Grundinitialisierung schreiben. Muss VOR dem
// DSI-Bringup laufen; die Hintergrundbeleuchtung bleibt dabei noch aus.
static void ws7_bridge_pre_init(void)
{
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = WS7_BRIDGE_I2C_ADDR,
.scl_speed_hz = 100000,
};
if (i2c_master_bus_add_device(s_i2c_handle, &dev_cfg, &s_bridge_handle) != ESP_OK) {
s_bridge_handle = NULL;
return;
}
ws7_bridge_write(0xC0, 0x01);
ws7_bridge_write(0xC2, 0x01);
ws7_bridge_write(0xAC, 0x01);
}
void jc_backlight_init(void)
{
// Nichts zu tun - die Bridge uebernimmt das, siehe ws7_bridge_pre_init().
}
void jc_backlight_set(int percent)
{
if (percent > 100) percent = 100;
if (percent < 0) percent = 0;
uint8_t brightness = (uint8_t)((255 * percent) / 100);
ws7_bridge_write(0xAD, 0x01); // Panel-/Backlight-Freigabe
ws7_bridge_write(0xAB, (uint8_t)(255 - brightness));
ws7_bridge_write(0xAA, 0x01); // Helligkeit uebernehmen
}
// =====================================================================================
#else // JC-Panels: Hintergrundbeleuchtung als LEDC-PWM an einem GPIO
// =====================================================================================
void jc_backlight_init(void)
{
const ledc_channel_config_t ch = {
@@ -76,6 +151,7 @@ void jc_backlight_set(int percent)
ledc_set_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH, duty);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH);
}
#endif // JC_PANEL_TYPE == WS_PANEL_7H
IRAM_ATTR static bool on_vsync(esp_lcd_panel_handle_t panel,
esp_lcd_dpi_panel_event_data_t *edata, void *ctx)
@@ -83,6 +159,54 @@ IRAM_ATTR static bool on_vsync(esp_lcd_panel_handle_t panel,
return lvgl_port_notify_lcd_vsync();
}
#if JC_PANEL_TYPE == WS_PANEL_7H
// Der GT9271 des Waveshare-Panels meldet im Hochformat (Roh-X 0..720, Roh-Y 0..1280),
// das Bild laeuft aber im Querformat 1280x720. Achsentausch (und bei Bedarf Spiegelung)
// deshalb ueber den process_coordinates-Hook, der VOR den swap/mirror-Flags von
// esp_lcd_touch greift - deren Flags rechnen mit x_max/y_max des BILDES und wuerden auf
// den Rohwerten falsche Ergebnisse liefern.
static void ws7_touch_map(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y,
uint16_t *strength, uint8_t *point_num, uint8_t max_point_num)
{
(void)tp; (void)strength; (void)max_point_num;
if (point_num == NULL) return;
for (uint8_t i = 0; i < *point_num; i++) {
uint16_t raw_x = x[i];
uint16_t raw_y = y[i];
#if WS7_TOUCH_MIRROR_RAW_X
raw_x = (raw_x > WS7_TOUCH_RAW_X_MAX) ? 0 : (uint16_t)(WS7_TOUCH_RAW_X_MAX - raw_x);
#endif
#if WS7_TOUCH_MIRROR_RAW_Y
raw_y = (raw_y > WS7_TOUCH_RAW_Y_MAX) ? 0 : (uint16_t)(WS7_TOUCH_RAW_Y_MAX - raw_y);
#endif
// Achsentausch + Begrenzung auf die Bildflaeche (der GT9271 liefert vereinzelt
// Ausreisser, die sonst als Klick am Rand ankaemen).
x[i] = (raw_y >= LCD_H_RES) ? (LCD_H_RES - 1) : raw_y;
y[i] = (raw_x >= LCD_V_RES) ? (LCD_V_RES - 1) : raw_x;
}
}
#endif
#if JC_PANEL_TYPE == JC_PANEL_70
// Der GT911 des 7,0"-Panels liefert Rohkoordinaten im Raster 800x480 (nicht 1024x600).
// Ohne Umrechnung waere nur das linke obere Rechteck erreichbar. Das Hersteller-Beispiel
// skaliert dafuer im Touch-Treiber; hier ueber den vorgesehenen process_coordinates-Hook,
// damit die Vendor-Datei esp_lcd_touch_gt911.c unveraendert bleibt.
#define JC_TOUCH_RAW_H_RES (800)
#define JC_TOUCH_RAW_V_RES (480)
static void jc_touch_scale(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y,
uint16_t *strength, uint8_t *point_num, uint8_t max_point_num)
{
(void)tp; (void)strength; (void)max_point_num;
if (point_num == NULL) return;
for (uint8_t i = 0; i < *point_num; i++) {
x[i] = (uint16_t)(((uint32_t)x[i] * LCD_H_RES) / JC_TOUCH_RAW_H_RES);
y[i] = (uint16_t)(((uint32_t)y[i] * LCD_V_RES) / JC_TOUCH_RAW_V_RES);
}
}
#endif
// Bringt Panel + Touch + LVGL-Port hoch (LVGL laeuft danach in eigenem Task).
void jc_board_bringup(void)
{
@@ -96,6 +220,11 @@ void jc_board_bringup(void)
};
i2c_new_master_bus(&i2c_bus_conf, &s_i2c_handle);
#if JC_PANEL_TYPE == WS_PANEL_7H
// Bridge des Waveshare-Panels vorbereiten (noch ohne Hintergrundbeleuchtung).
ws7_bridge_pre_init();
#endif
static esp_ldo_channel_handle_t phy_pwr_chan = NULL;
esp_ldo_channel_config_t ldo_cfg = {
.chan_id = BSP_MIPI_DSI_PHY_PWR_LDO_CHAN,
@@ -104,14 +233,95 @@ void jc_board_bringup(void)
esp_ldo_acquire_channel(&ldo_cfg, &phy_pwr_chan);
esp_lcd_dsi_bus_handle_t mipi_dsi_bus;
esp_lcd_panel_io_handle_t io = NULL;
esp_lcd_panel_handle_t disp_panel = NULL;
#if JC_PANEL_TYPE == WS_PANEL_7H
// ---------------- Waveshare 7inch DSI LCD (H) (1280x720) ----------------
// Kein Hersteller-Panel-Treiber: DSI-Bus, DBI-Kommandokanal und DPI-Panel werden
// direkt angelegt. Entscheidend ist die Referenztaktquelle PLL_F20M - mit der
// Standardquelle bleibt das Bild auf ESP32-P4 Rev. 1.3 / ECO2 schwarz.
esp_lcd_dsi_bus_config_t bus_config = {
.bus_id = 0,
.num_data_lanes = WS7_DSI_LANES,
.phy_clk_src = MIPI_DSI_PHY_PLLREF_CLK_SRC_PLL_F20M,
.lane_bit_rate_mbps = WS7_DSI_LANE_MBPS,
};
esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus);
esp_lcd_dbi_io_config_t dbi_config = {
.virtual_channel = 0,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
};
esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &io);
esp_lcd_dpi_panel_config_t dpi_config = {
.virtual_channel = 0,
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
.dpi_clock_freq_mhz = WS7_DPI_CLOCK_MHZ,
.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB888,
.num_fbs = LVGL_PORT_LCD_BUFFER_NUMS,
.video_timing = {
.h_size = BSP_LCD_H_RES,
.v_size = BSP_LCD_V_RES,
.hsync_back_porch = WS7_HSYNC_BACK,
.hsync_pulse_width = WS7_HSYNC_PULSE,
.hsync_front_porch = WS7_HSYNC_FRONT,
.vsync_back_porch = WS7_VSYNC_BACK,
.vsync_pulse_width = WS7_VSYNC_PULSE,
.vsync_front_porch = WS7_VSYNC_FRONT,
},
.flags = {
.use_dma2d = true,
},
};
esp_lcd_new_panel_dpi(mipi_dsi_bus, &dpi_config, &disp_panel);
// Panel wecken (MADCTL / Sleep Out / Display On). Ein Reset gibt es hier nicht:
// ein reines DPI-Panel kennt keine reset-Funktion, esp_lcd_panel_reset() wuerde
// ins Leere greifen.
{
uint8_t zero = 0x00;
esp_lcd_panel_io_tx_param(io, 0x36, &zero, 1); // MADCTL
esp_lcd_panel_io_tx_param(io, 0x11, &zero, 1); // Sleep Out
vTaskDelay(pdMS_TO_TICKS(120));
esp_lcd_panel_io_tx_param(io, 0x29, &zero, 1); // Display On
vTaskDelay(pdMS_TO_TICKS(20));
}
esp_lcd_panel_init(disp_panel); // DPI-Videoausgabe starten
#elif JC_PANEL_TYPE == JC_PANEL_70
// ---------------- 7,0" JD9165 (1024x600) ----------------
esp_lcd_dsi_bus_config_t bus_config = JD9165_PANEL_BUS_DSI_2CH_CONFIG();
esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus);
esp_lcd_dbi_io_config_t dbi_config = JD9165_PANEL_IO_DBI_CONFIG();
esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &io);
esp_lcd_dpi_panel_config_t dpi_config = JD9165_1024_600_PANEL_60HZ_DPI_CONFIG(LCD_COLOR_PIXEL_FORMAT_RGB565);
dpi_config.num_fbs = LVGL_PORT_LCD_BUFFER_NUMS;
jd9165_vendor_config_t vendor_config = {
.mipi_config = {
.dsi_bus = mipi_dsi_bus,
.dpi_config = &dpi_config,
},
};
esp_lcd_panel_dev_config_t lcd_dev_config = {
.bits_per_pixel = 16,
.rgb_ele_order = ESP_LCD_COLOR_SPACE_RGB,
.reset_gpio_num = BSP_LCD_RST,
.vendor_config = &vendor_config,
};
esp_lcd_new_panel_jd9165(io, &lcd_dev_config, &disp_panel);
#else
// ---------------- 4,3" ST7701 (480x800) ----------------
esp_lcd_dsi_bus_config_t bus_config = ST7701_PANEL_BUS_DSI_2CH_CONFIG();
esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus);
esp_lcd_panel_io_handle_t io = NULL;
esp_lcd_dbi_io_config_t dbi_config = ST7701_PANEL_IO_DBI_CONFIG();
esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &io);
esp_lcd_panel_handle_t disp_panel = NULL;
esp_lcd_dpi_panel_config_t dpi_config = ST7701_480_360_PANEL_60HZ_DPI_CONFIG(LCD_COLOR_PIXEL_FORMAT_RGB565);
dpi_config.num_fbs = LVGL_PORT_LCD_BUFFER_NUMS;
@@ -131,8 +341,11 @@ void jc_board_bringup(void)
.vendor_config = &vendor_config,
};
esp_lcd_new_panel_st7701(io, &lcd_dev_config, &disp_panel);
#endif
#if JC_PANEL_TYPE != WS_PANEL_7H
esp_lcd_panel_reset(disp_panel);
esp_lcd_panel_init(disp_panel);
#endif
// Alle Framebuffer einmal auf Schwarz loeschen. Im Avoid-Tear-Direct-Mode (mehrere
// Buffer) kopiert LVGL pro Frame nur die geaenderten Bereiche in den naechsten Buffer;
@@ -141,7 +354,7 @@ void jc_board_bringup(void)
// nicht auf. Cache-Writeback noetig, da die Buffer im PSRAM liegen und der DMA liest.
{
void *fbs[3] = { NULL, NULL, NULL };
size_t fb_size = (size_t)BSP_LCD_H_RES * BSP_LCD_V_RES * 2; // RGB565 = 2 Byte/Pixel
size_t fb_size = (size_t)BSP_LCD_H_RES * BSP_LCD_V_RES * BSP_LCD_FB_BYTES_PER_PX;
#if LVGL_PORT_LCD_BUFFER_NUMS >= 3
esp_lcd_dpi_panel_get_frame_buffer(disp_panel, 3, &fbs[0], &fbs[1], &fbs[2]);
#elif LVGL_PORT_LCD_BUFFER_NUMS == 2
@@ -170,6 +383,11 @@ void jc_board_bringup(void)
esp_lcd_touch_handle_t tp_handle;
esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG();
tp_io_config.scl_speed_hz = 100000;
#if JC_PANEL_TYPE == WS_PANEL_7H
// GT9271 des Waveshare-Panels: gleiche Registerbelegung wie der GT911, aber
// andere I2C-Adresse.
tp_io_config.dev_addr = WS7_TOUCH_I2C_ADDR;
#endif
esp_lcd_new_panel_io_i2c(s_i2c_handle, &tp_io_config, &tp_io_handle);
const esp_lcd_touch_config_t tp_cfg = {
.x_max = BSP_LCD_H_RES,
@@ -185,6 +403,11 @@ void jc_board_bringup(void)
.mirror_x = 0,
.mirror_y = 0,
},
#if JC_PANEL_TYPE == WS_PANEL_7H
.process_coordinates = ws7_touch_map,
#elif JC_PANEL_TYPE == JC_PANEL_70
.process_coordinates = jc_touch_scale,
#endif
};
esp_lcd_touch_new_i2c_gt911(tp_io_handle, &tp_cfg, &tp_handle);
+87 -16
View File
@@ -1,14 +1,36 @@
// =====================================================================================
// JC4880P443C-I-W - Touch-Display-Firmware fuer die Dual-PID-Siebtraegersteuerung
// config.h - zentrale Konfiguration (Pins, UART, Protokoll)
// JC-Touch-Display-Firmware fuer die Dual-PID-Siebtraegersteuerung
// config.h - zentrale Konfiguration (Panel-Typ, Pins, UART, Protokoll)
// =====================================================================================
//
// Ziel-Hardware: Guition JC4880P443C-I-W
// - MCU: ESP32-P4 (UI / LVGL)
// - WLAN: ESP32-C6 (hier NICHT genutzt - WLAN/Webinterface laufen auf der Hauptplatine)
// - Display: ST7701 (480 x 800 IPS, MIPI-DSI)
// - Touch: GT911 (kapazitiv, I2C)
// - Audio: ES8311 (optional)
// Zwei unterstuetzte Panels (Auswahl ueber JC_PANEL_TYPE weiter unten):
//
// JC_PANEL_43 - Guition JC4880P443C-I-W (4,3")
// - MCU: ESP32-P4 (UI / LVGL)
// - Display: ST7701 (480 x 800 IPS, MIPI-DSI) -> per Rotation 270 = 800 x 480
// - Touch: GT911 (kapazitiv, I2C SDA=7 / SCL=8)
// - LCD-Reset GPIO5, Backlight GPIO23 (LEDC-PWM)
//
// JC_PANEL_70 - Guition JC1060P470C-I-W (7,0")
// - MCU: ESP32-P4 (UI / LVGL)
// - Display: JD9165 (1024 x 600 IPS, MIPI-DSI) -> nativ Querformat, keine Rotation
// - Touch: GT911 (kapazitiv, I2C SDA=7 / SCL=8); meldet Rohkoordinaten in
// 800x480 und wird in board_bringup.c auf 1024x600 hochskaliert
// - LCD-Reset GPIO27, Backlight GPIO23 (LEDC-PWM)
//
// WS_PANEL_7H - Waveshare ESP32-P4-Pico + Waveshare 7inch DSI LCD (H)
// - MCU: ESP32-P4 (Rev. 1.3 / ECO2), 32 MB PSRAM, 32 MB Flash, KEIN ESP32-C6
// - Display: 1280 x 720 IPS, MIPI-DSI (2 Lanes), nativ Querformat, keine Rotation.
// Das Panel haengt hinter einer Waveshare-Bridge und akzeptiert NUR
// RGB888 -> LVGL laeuft bei diesem Panel mit 24 Bit Farbtiefe
// (siehe lv_conf.h). Kein LCD-Reset-Pin.
// - Touch: GT9271 (kapazitiv, I2C SDA=7 / SCL=8, Adresse 0x14); liefert
// Rohkoordinaten im Hochformat -> X/Y werden getauscht
// - Backlight: kein PWM-Pin, sondern Register der Bridge auf I2C-Adresse 0x45
//
// Gemeinsam: 16 MB Flash (Waveshare: 32 MB), PSRAM aktiviert, Audio ES8311 (optional).
// Bei den JC-Panels ist zusaetzlich ein ESP32-C6 bestueckt, der hier NICHT genutzt wird -
// WLAN/Webinterface laufen auf der Hauptplatine.
//
// Diese Firmware ist ein reiner UART-Client. Quelle der Wahrheit ist die Hauptplatine
// (ESP32-S3, Dual_PID_FastHeatUp.ino). Siehe Doku/JC-Display_UART-Protokoll.md.
@@ -25,6 +47,28 @@
#define JC_USE_REAL_PANEL 1
#endif
// -------------------------------------------------------------------------------------
// Panel-Typ (C-sicher hier definiert, da auch board_bringup.c, pins_config.h und
// lv_conf.h ihn lesen)
// JC_PANEL_43 = Guition JC4880P443C-I-W : 4,3", ST7701, 480x800 (per Rotation 800x480)
// JC_PANEL_70 = Guition JC1060P470C-I-W : 7,0", JD9165, 1024x600 (nativ Querformat)
// WS_PANEL_7H = Waveshare ESP32-P4-Pico + Waveshare 7inch DSI LCD (H) : 1280x720
// HIER umschalten. Ein Compiler-Flag reicht NICHT sicher aus, weil lv_conf.h die
// Farbtiefe aus diesem Wert ableitet und auch die LVGL-Bibliothek damit uebersetzt
// werden muss.
// -------------------------------------------------------------------------------------
#define JC_PANEL_43 43
#define JC_PANEL_70 70
#define WS_PANEL_7H 77
#ifndef JC_PANEL_TYPE
#define JC_PANEL_TYPE WS_PANEL_7H
#endif
#if (JC_PANEL_TYPE != JC_PANEL_43) && (JC_PANEL_TYPE != JC_PANEL_70) && (JC_PANEL_TYPE != WS_PANEL_7H)
#error "JC_PANEL_TYPE muss JC_PANEL_43, JC_PANEL_70 oder WS_PANEL_7H sein"
#endif
// -------------------------------------------------------------------------------------
// DIAGNOSE: interner UART-Loopback-Selbsttest
// 1 = TX wird chip-intern auf RX gelegt (kein Draht, keine Pin-Unsicherheit).
@@ -50,22 +94,39 @@
// Gegenseite (S3): Serial1, 230400 8N1, TX=GPIO43 / RX=GPIO44.
// Verkabelung gekreuzt: S3-TX(43) -> P4-RX, S3-RX(44) -> P4-TX, GND <-> GND.
//
// Pins laut Schaltplan "4_USB&IO.png" (Expand-IO-Header JP1). Frei herausgefuehrte
// P4-GPIOs dort: 52/51/50/49/35/34/33/32/31/30/29/28 (+ ES_I2C, +3V3/+5V/GND).
// 4,3" (JC_PANEL_43): Expand-IO-Header JP1, Schaltplan "4_USB&IO.png". Frei
// herausgefuehrte P4-GPIOs: 52/51/50/49/35/34/33/32/31/30/29/28 (+ ES_I2C, +3V3/+5V/GND).
// Die rechte Header-Spalte (C6_U01TXD/C6_I10RXD/...) gehoert zum ESP32-C6 -> NICHT nutzen.
// Gewaehlt: P4-TX = GPIO33, P4-RX = GPIO31.
// JP1 GPIO33 (P4 TX) ----> S3 GPIO44 (RX)
// JP1 GPIO31 (P4 RX) <---- S3 GPIO43 (TX)
// JP1 GND ---- S3 GND
//
// 7,0" (JC_PANEL_70): Expand-IO liegt auf den FPC-Buchsen FPC3/FPC4 (1,0 mm, 16-polig),
// Schaltplan "5-Schematic/4_CONN.png" - kein Stiftleisten-Header, Adapterkabel noetig.
// FPC4 fuehrt GPIO 34/33/32/31/30/29/28 heraus, FPC3 zusaetzlich 48/47/46/45/5/4/3/2.
//
// Beide JC-Panels: gleiche Pinwahl P4-TX = GPIO33, P4-RX = GPIO31.
// GPIO33 (P4 TX) ----> S3 GPIO44 (RX)
// GPIO31 (P4 RX) <---- S3 GPIO43 (TX)
// GND ---- S3 GND
//
// Waveshare ESP32-P4-Pico (WS_PANEL_7H): 40-poliger Header im Raspberry-Pi-Pico-Format.
// Belegt sind dort u.a. I2C (7/8), TF-Karte (9/10/11/12/13/53) und der Debug-UART0
// (37/38); diese Pins duerfen NICHT genommen werden. Genutzt werden GPIO17/GPIO18:
// GPIO17 (P4 TX) ----> S3 GPIO44 (RX)
// GPIO18 (P4 RX) <---- S3 GPIO43 (TX)
// GND ---- S3 GND
#define DISPLAY_UART_PORT Serial1
#define DISPLAY_UART_BAUDRATE 230400
#if JC_PANEL_TYPE == WS_PANEL_7H
#define DISPLAY_UART_TX_PIN 17
#define DISPLAY_UART_RX_PIN 18
#else
#define DISPLAY_UART_TX_PIN 33
#define DISPLAY_UART_RX_PIN 31
#endif
// -------------------------------------------------------------------------------------
// Firmware
// -------------------------------------------------------------------------------------
#define DISPLAY_FW_VERSION "1.1.1" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
#define DISPLAY_FW_VERSION "1.6.0" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
// -------------------------------------------------------------------------------------
// Protokoll
@@ -82,9 +143,19 @@
// -------------------------------------------------------------------------------------
// Display / LVGL
// -------------------------------------------------------------------------------------
// Querformat: Panel ist physisch 480x800, per Rotation 90 (pins_config.h) -> 800x480.
// Querformat. 4,3": Panel ist physisch 480x800, per Rotation 270 (pins_config.h) -> 800x480.
// 7,0": Panel ist nativ 1024x600, keine Rotation noetig.
// 7" WS: Panel ist nativ 1280x720, keine Rotation noetig.
#if JC_PANEL_TYPE == WS_PANEL_7H
#define DISP_HOR_RES 1280
#define DISP_VER_RES 720
#elif JC_PANEL_TYPE == JC_PANEL_70
#define DISP_HOR_RES 1024
#define DISP_VER_RES 600
#else
#define DISP_HOR_RES 800
#define DISP_VER_RES 480
#endif
#define LVGL_TICK_MS 5
// Debug-Ausgaben ueber USB-CDC (nicht ueber den Display-UART!)
+11 -2
View File
@@ -1,5 +1,5 @@
// =====================================================================================
// display_hal.cpp - LVGL-Glue fuer das JC4880P443C-I-W
// display_hal.cpp - LVGL-Glue fuer JC4880P443C-I-W / JC1060P470C-I-W / Waveshare 7" (H)
// =====================================================================================
// Realer Modus: nutzt die Hersteller-Portierung. Der eigentliche esp_lcd-Bringup liegt
// bewusst in board_bringup.c (C). Hier nur die LVGL-/UI-Verdrahtung.
@@ -11,6 +11,15 @@
#include "ui.h"
#include "config.h"
// LVGL muss mit derselben Farbtiefe uebersetzt sein wie das Panel erwartet. Zieht der
// Build versehentlich eine fremde lv_conf.h (z.B. eine alte Datei neben dem lvgl-Ordner),
// faellt das hier auf - sonst erst am schwarzen oder farbverfaelschten Bild.
#if JC_PANEL_TYPE == WS_PANEL_7H
static_assert(LV_COLOR_DEPTH == 24, "Waveshare 7\" (H) braucht RGB888 - falsche lv_conf.h?");
#else
static_assert(LV_COLOR_DEPTH == 16, "JC-Panels brauchen RGB565 - falsche lv_conf.h?");
#endif
static ProtocolClient* g_client = nullptr;
// =====================================================================================
@@ -46,7 +55,7 @@ void hal_backlight(int percent) { jc_backlight_set(percent); }
// STUB-MODUS - minimales LVGL ohne Panel/Touch (zum Testen von Protokoll/Logik).
// =====================================================================================
#define HAL_BUF_LINES 40
#define HAL_BYTES_PER_PX 2
#define HAL_BYTES_PER_PX (LV_COLOR_DEPTH / 8)
static uint8_t s_buf1[DISP_HOR_RES * HAL_BUF_LINES * HAL_BYTES_PER_PX];
static lv_display_t* s_disp = nullptr;
+4 -2
View File
@@ -1,9 +1,11 @@
// =====================================================================================
// display_hal.h - Board-/Panel-/Touch-Anbindung (ST7701 + GT911) + LVGL-Glue
// display_hal.h - Board-/Panel-/Touch-Anbindung (ST7701 / JD9165 / Waveshare-DSI) + LVGL-Glue
// =====================================================================================
// Einziger board-spezifischer Teil. Baut auf der HERSTELLER-LVGL-PORTIERUNG des
// JC4880P443C-I-W auf (1-Demo/arduino_examples/lvgl_v9_sw_rotation):
// - MIPI-DSI ST7701 (480x800), GT911-Touch (I2C SDA=7/SCL=8), Backlight GPIO23
// - MIPI-DSI ST7701 (480x800), JD9165 (1024x600) oder Waveshare 7" (H) (1280x720)
// - Auswahl ueber JC_PANEL_TYPE
// - GT911- bzw. GT9271-Touch (I2C SDA=7/SCL=8); Backlight GPIO23 bzw. Bridge-Register
// - LVGL laeuft in einem EIGENEN FreeRTOS-Task -> UI-Zugriffe von aussen muessen
// mit hal_lock()/hal_unlock() (= lvgl_port_lock/unlock) geschuetzt werden.
//
+13
View File
@@ -26,8 +26,21 @@
COLOR SETTINGS
*====================*/
/* Panel-Typ des Projekts. Liegt im selben Ordner wie diese Datei, ein Include mit
* Anfuehrungszeichen findet also immer genau diese config.h - auch dann, wenn die
* LVGL-Bibliothek uebersetzt wird. Nur so bekommen Sketch UND Bibliothek dieselbe
* Farbtiefe; ein Compiler-Flag allein wuerde die Bibliothek nicht erreichen. */
#include "config.h"
/*Color depth: 1 (I1), 8 (L8), 16 (RGB565), 24 (RGB888), 32 (XRGB8888)*/
/* Waveshare 7inch DSI LCD (H): Die Bridge des Panels nimmt ausschliesslich RGB888
* entgegen, deshalb muss auch LVGL in 24 Bit rechnen. Die JC-Panels laufen wie
* bisher mit RGB565. */
#if JC_PANEL_TYPE == WS_PANEL_7H
#define LV_COLOR_DEPTH 24
#else
#define LV_COLOR_DEPTH 16
#endif
/*=========================
STDLIB WRAPPER SETTINGS
+4 -2
View File
@@ -33,8 +33,10 @@ typedef enum {
* LVGL related parameters, can be adjusted by users
*
*/
#define LVGL_PORT_H_RES (480)
#define LVGL_PORT_V_RES (800)
/* Native Panel-Aufloesung - kommt aus pins_config.h, damit beide Panel-Typen
* (JC_PANEL_43: 480x800 / JC_PANEL_70: 1024x600) denselben Port nutzen koennen. */
#define LVGL_PORT_H_RES (LCD_H_RES)
#define LVGL_PORT_V_RES (LCD_V_RES)
#define LVGL_PORT_TICK_PERIOD_MS (EXAMPLE_LVGL_PORT_TICK)
/**
+11 -1
View File
@@ -72,11 +72,21 @@ struct MachineState {
bool cxEnabled = false; // Funktion auf der S3 freigeschaltet (sonst Bedienelement ausblenden)
bool cxActive = false; // Modus scharf: Wasserkreis heizt nicht (persistent, seit S3 5.3.1)
bool cxShot = false; // der laufende Bezug ist eine kalte Extraktion
bool cxAllowed = false; // Freigabe jetzt (Wasserkessel kalt genug, kein Standby/Wartung/Tuning)
bool cxAllowed = false; // Bezug jetzt moeglich (Wasserkessel kalt genug, kein Standby/Wartung/Tuning)
bool cxArmable = false; // Modus umschaltbar - auch im Standby und bei warmem Kessel (seit S3 5.3.5)
bool cxResting = false; // Zwangspause des Pumpenschutzes laeuft (Bezug laeuft weiter)
float cxMaxTemp = 0; // Freigabeschwelle in C
float cxTarget = 0; // Zielgewicht in g
float cxPreInfSec = 0; // Vorbenetzung in s (fuer die Phasenleiste des kalten Bezugs)
// Restliche Parameter fuer die Einstellseite am Display (seit S3 5.4.0)
float cxPreInfDuty = 0; // Pumpenleistung Vorbenetzung in %
float cxDuty = 0; // Pumpenleistung Hauptbezug in %
uint16_t cxPulseMs = 0; // Pulsperiode in ms
float cxPumpMaxRun = 0; // Pumpenschutz: max. kumulierte Laufzeit in s
float cxPumpRest = 0; // Pumpenschutz: Zwangspause in s
bool cxAskOnWake = false; // Beim Aufwecken aus dem Standby fragen (seit S3 5.5.0)
bool cxWakeChoice = false; // Auswahlfenster nach dem Aufwecken offen (seit S3 5.6.0)
long cxWakeChoiceSec = 0; // Restsekunden der Bedenkzeit
float cxMaxSecs = 0; // Sicherheits-Timeout des Bezugs in s
// Sperrgrund: 0=frei, 1=nicht freigeschaltet, 2=Standby, 3=Wartung, 4=Reinigungsassistent,
// 5=PID-Tuning, 6=Sensorfehler, 7=Wasserkessel zu warm
+63 -2
View File
@@ -1,5 +1,7 @@
#pragma once
#include "config.h" // liefert JC_PANEL_TYPE (C-sicher, keine Rueckabhaengigkeit)
#define EXAMPLE_LVGL_PORT_TASK_MAX_DELAY_MS 500 //range 2 to 2000
#define EXAMPLE_LVGL_PORT_TASK_MIN_DELAY_MS 5 //range 1 to 100
#define EXAMPLE_LVGL_PORT_TASK_PRIORITY 4
@@ -16,18 +18,77 @@
// Bei Rotation 90 nutzen beide Modi ohnehin 3 Framebuffer -> kein Mehrverbrauch.
#define EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE 2 //range 1 to 3
#if JC_PANEL_TYPE == WS_PANEL_7H
// Waveshare 7" (H): Panel ist nativ 1280x720 Querformat -> keine Rotation, kein PPA noetig.
#define EXAMPLE_LVGL_PORT_ROTATION_DEGREE_ 0 // 0,90,180,270
#define EXAMPLE_LVGL_PORT_PPA_ROTATION_ENABLE 0
#elif JC_PANEL_TYPE == JC_PANEL_70
// 7,0" JD9165: Panel ist nativ 1024x600 Querformat -> keine Rotation, kein PPA noetig.
#define EXAMPLE_LVGL_PORT_ROTATION_DEGREE_ 0 // 0,90,180,270
#define EXAMPLE_LVGL_PORT_PPA_ROTATION_ENABLE 0
#else
// 4,3" ST7701: Panel ist physisch 480x800 Hochformat.
#define EXAMPLE_LVGL_PORT_ROTATION_DEGREE_ 270 // 0,90,180,270 (270 = Querformat 800x480, um 180 Grad gedreht vs. 90)
#define EXAMPLE_LVGL_PORT_PPA_ROTATION_ENABLE 1
#endif
#endif
// LCD_H_RES/LCD_V_RES = NATIVE Panel-Aufloesung (vor der LVGL-Rotation).
#if JC_PANEL_TYPE == WS_PANEL_7H
#define LCD_H_RES 1280
#define LCD_V_RES 720
// Das Waveshare-Panel hat weder einen Reset-Pin noch eine PWM-Hintergrundbeleuchtung:
// Reset macht die Bridge selbst, die Helligkeit laeuft ueber deren I2C-Register.
#define LCD_RST (-1)
#define LCD_LED (-1)
// ---- MIPI-DSI-Parameter des Waveshare 7inch DSI LCD (H) -----------------------------
// Uebernommen aus dem funktionierenden Funktionstest-Sketch. Wichtig ist die Legacy-
// Referenztaktquelle PLL_F20M: nur damit liefert der ESP32-P4 Rev. 1.3 / ECO2 ein Bild.
// Das Panel akzeptiert nur RGB888 (24 Bit) - deshalb rechnet LVGL bei diesem Panel
// ebenfalls mit 24 Bit (lv_conf.h).
#define WS7_DSI_LANES 2
#define WS7_DSI_LANE_MBPS 1250
#define WS7_DPI_CLOCK_MHZ 80 // 1472 x 912 Gesamtraster -> ca. 60 Hz
#define WS7_HSYNC_BACK 64
#define WS7_HSYNC_PULSE 64
#define WS7_HSYNC_FRONT 64
#define WS7_VSYNC_BACK 64
#define WS7_VSYNC_PULSE 64
#define WS7_VSYNC_FRONT 64
// Bridge (Helligkeit/Panel-Freigabe) und Touch-Controller am selben I2C-Bus wie GT911.
#define WS7_BRIDGE_I2C_ADDR 0x45
#define WS7_TOUCH_I2C_ADDR 0x14 // GT9271
// Der GT9271 meldet im Hochformat: Roh-X 0..720, Roh-Y 0..1280. Fuer das Querformat
// werden die Achsen getauscht. Zeigt der Cursor spiegelverkehrt, hier die passende
// Achse auf 1 setzen (die Spiegelung geschieht VOR dem Tausch, auf den Rohwerten).
#define WS7_TOUCH_RAW_X_MAX 720
#define WS7_TOUCH_RAW_Y_MAX 1280
#define WS7_TOUCH_MIRROR_RAW_X 0
#define WS7_TOUCH_MIRROR_RAW_Y 0
#elif JC_PANEL_TYPE == JC_PANEL_70
#define LCD_H_RES 1024
#define LCD_V_RES 600
#define LCD_RST 27
#define LCD_LED 23
#else
#define LCD_H_RES 480
#define LCD_V_RES 800
#define LCD_RST -1
#define LCD_LED -1
#define LCD_RST 5
#define LCD_LED 23
#endif
#define TP_I2C_SDA 7
#define TP_I2C_SCL 8
+10
View File
@@ -225,10 +225,20 @@ void ProtocolClient::applyState(const String& json) {
s.cxActive = doc["cxActive"] | false;
s.cxShot = doc["cxShot"] | false;
s.cxAllowed = doc["cxAllowed"] | false;
// Fehlt bei S3-FW < 5.3.5 -> auf cxEnabled zurueckfallen, sonst waere der Schalter tot
s.cxArmable = doc["cxArmable"] | (doc["cxEnabled"] | false);
s.cxResting = doc["cxResting"] | false;
s.cxMaxTemp = doc["cxMaxTemp"] | s.cxMaxTemp;
s.cxTarget = doc["cxTarget"] | s.cxTarget;
s.cxPreInfSec= doc["cxPreInf"] | s.cxPreInfSec;
s.cxPreInfDuty = doc["cxPreInfDuty"] | s.cxPreInfDuty;
s.cxDuty = doc["cxDuty"] | s.cxDuty;
s.cxPulseMs = doc["cxPulseMs"] | s.cxPulseMs;
s.cxPumpMaxRun = doc["cxPumpMaxRun"] | s.cxPumpMaxRun;
s.cxPumpRest = doc["cxPumpRest"] | s.cxPumpRest;
s.cxAskOnWake = doc["cxAskOnWake"] | false;
s.cxWakeChoice = doc["cxWakeChoice"] | false;
s.cxWakeChoiceSec = doc["cxWakeChoiceSec"] | (long)0;
s.cxMaxSecs = doc["cxMaxSecs"] | s.cxMaxSecs;
s.cxBlock = doc["cxBlock"] | (uint8_t)0;
s.cxNotice = doc["cxNotice"] | (uint8_t)0;
@@ -0,0 +1,341 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "soc/soc_caps.h"
#if SOC_MIPI_DSI_SUPPORTED
#include "esp_check.h"
#include "esp_log.h"
#include "esp_lcd_panel_commands.h"
#include "esp_lcd_panel_interface.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_mipi_dsi.h"
#include "esp_lcd_panel_vendor.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_lcd_jd9165.h"
#define JD9165_CMD_GS_BIT (1 << 0)
#define JD9165_CMD_SS_BIT (1 << 1)
typedef struct {
esp_lcd_panel_io_handle_t io;
int reset_gpio_num;
uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register
uint8_t colmod_val; // save surrent value of LCD_CMD_COLMOD register
const jd9165_lcd_init_cmd_t *init_cmds;
uint16_t init_cmds_size;
struct {
unsigned int reset_level: 1;
} flags;
// To save the original functions of MIPI DPI panel
esp_err_t (*del)(esp_lcd_panel_t *panel);
esp_err_t (*init)(esp_lcd_panel_t *panel);
} jd9165_panel_t;
static const char *TAG = "jd9165";
static esp_err_t panel_jd9165_del(esp_lcd_panel_t *panel);
static esp_err_t panel_jd9165_init(esp_lcd_panel_t *panel);
static esp_err_t panel_jd9165_reset(esp_lcd_panel_t *panel);
static esp_err_t panel_jd9165_invert_color(esp_lcd_panel_t *panel, bool invert_color_data);
static esp_err_t panel_jd9165_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y);
static esp_err_t panel_jd9165_disp_on_off(esp_lcd_panel_t *panel, bool on_off);
esp_err_t esp_lcd_new_panel_jd9165(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config,
esp_lcd_panel_handle_t *ret_panel)
{
ESP_LOGI(TAG,"jd9165:1.0.1");
ESP_RETURN_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, TAG, "invalid arguments");
jd9165_vendor_config_t *vendor_config = (jd9165_vendor_config_t *)panel_dev_config->vendor_config;
ESP_RETURN_ON_FALSE(vendor_config && vendor_config->mipi_config.dpi_config && vendor_config->mipi_config.dsi_bus, ESP_ERR_INVALID_ARG, TAG,
"invalid vendor config");
esp_err_t ret = ESP_OK;
jd9165_panel_t *jd9165 = (jd9165_panel_t *)calloc(1, sizeof(jd9165_panel_t));
ESP_RETURN_ON_FALSE(jd9165, ESP_ERR_NO_MEM, TAG, "no mem for jd9165 panel");
if (panel_dev_config->reset_gpio_num >= 0) {
gpio_config_t io_conf = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num,
};
ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed");
}
switch (panel_dev_config->color_space) {
case LCD_RGB_ELEMENT_ORDER_RGB:
jd9165->madctl_val = 0;
break;
case LCD_RGB_ELEMENT_ORDER_BGR:
jd9165->madctl_val |= LCD_CMD_BGR_BIT;
break;
default:
ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space");
break;
}
jd9165->io = io;
jd9165->init_cmds = vendor_config->init_cmds;
jd9165->init_cmds_size = vendor_config->init_cmds_size;
jd9165->reset_gpio_num = panel_dev_config->reset_gpio_num;
jd9165->flags.reset_level = panel_dev_config->flags.reset_active_high;
// Create MIPI DPI panel
esp_lcd_panel_handle_t panel_handle = NULL;
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_dpi(vendor_config->mipi_config.dsi_bus, vendor_config->mipi_config.dpi_config, &panel_handle), err, TAG,
"create MIPI DPI panel failed");
ESP_LOGD(TAG, "new MIPI DPI panel @%p", panel_handle);
// Save the original functions of MIPI DPI panel
jd9165->del = panel_handle->del;
jd9165->init = panel_handle->init;
// Overwrite the functions of MIPI DPI panel
panel_handle->del = panel_jd9165_del;
panel_handle->init = panel_jd9165_init;
panel_handle->reset = panel_jd9165_reset;
panel_handle->mirror = panel_jd9165_mirror;
panel_handle->invert_color = panel_jd9165_invert_color;
panel_handle->disp_on_off = panel_jd9165_disp_on_off;
panel_handle->user_data = jd9165;
*ret_panel = panel_handle;
ESP_LOGD(TAG, "new jd9165 panel @%p", jd9165);
return ESP_OK;
err:
if (jd9165) {
if (panel_dev_config->reset_gpio_num >= 0) {
gpio_reset_pin(panel_dev_config->reset_gpio_num);
}
free(jd9165);
}
return ret;
}
static const jd9165_lcd_init_cmd_t vendor_specific_init_default[] = {
// {cmd, { data }, data_size, delay_ms}
//{0x11, (uint8_t []){0x00}, 1, 120},
//{0x29, (uint8_t []){0x00}, 1, 20},
{0x30, (uint8_t[]){0x00}, 1, 0},
{0xF7, (uint8_t[]){0x49,0x61,0x02,0x00}, 4, 0},
{0x30, (uint8_t[]){0x01}, 1, 0},
{0x04, (uint8_t[]){0x0C}, 1, 0},
{0x05, (uint8_t[]){0x00}, 1, 0},
{0x06, (uint8_t[]){0x00}, 1, 0},
{0x0B, (uint8_t[]){0x11}, 1, 0},
{0x17, (uint8_t[]){0x00}, 1, 0},
{0x20, (uint8_t[]){0x04}, 1, 0},
{0x1F, (uint8_t[]){0x05}, 1, 0},
{0x23, (uint8_t[]){0x00}, 1, 0},
{0x25, (uint8_t[]){0x19}, 1, 0},
{0x28, (uint8_t[]){0x18}, 1, 0},
{0x29, (uint8_t[]){0x04}, 1, 0},
{0x2A, (uint8_t[]){0x01}, 1, 0},
{0x2B, (uint8_t[]){0x04}, 1, 0},
{0x2C, (uint8_t[]){0x01}, 1, 0},
{0x30, (uint8_t[]){0x02}, 1, 0},
{0x01, (uint8_t[]){0x22}, 1, 0},
{0x03, (uint8_t[]){0x12}, 1, 0},
{0x04, (uint8_t[]){0x00}, 1, 0},
{0x05, (uint8_t[]){0x64}, 1, 0},
{0x0A, (uint8_t[]){0x08}, 1, 0},
{0x0B, (uint8_t[]){0x0A,0x1A,0x0B,0x0D,0x0D,0x11,0x10,0x06,0x08,0x1F,0x1D}, 11, 0},
{0x0C, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x0D, (uint8_t[]){0x16,0x1B,0x0B,0x0D,0x0D,0x11,0x10,0x07,0x09,0x1E,0x1C}, 11, 0},
{0x0E, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x0F, (uint8_t[]){0x16,0x1B,0x0D,0x0B,0x0D,0x11,0x10,0x1C,0x1E,0x09,0x07}, 11, 0},
{0x10, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x11, (uint8_t[]){0x0A,0x1A,0x0D,0x0B,0x0D,0x11,0x10,0x1D,0x1F,0x08,0x06}, 11, 0},
{0x12, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x14, (uint8_t[]){0x00,0x00,0x11,0x11}, 4, 0},
{0x18, (uint8_t[]){0x99}, 1, 0},
{0x30, (uint8_t[]){0x06}, 1, 0},
{0x12, (uint8_t[]){0x36,0x2C,0x2E,0x3C,0x38,0x35,0x35,0x32,0x2E,0x1D,0x2B,0x21,0x16,0x29}, 14, 0},
{0x13, (uint8_t[]){0x36,0x2C,0x2E,0x3C,0x38,0x35,0x35,0x32,0x2E,0x1D,0x2B,0x21,0x16,0x29}, 14, 0},
// {0x30, (uint8_t[]){0x08}, 1, 0},
// {0x05, (uint8_t[]){0x01}, 1, 0},
// {0x0C, (uint8_t[]){0x1A}, 1, 0},
// {0x0D, (uint8_t[]){0x0E}, 1, 0},
// {0x30, (uint8_t[]){0x07}, 1, 0},
// {0x01, (uint8_t[]){0x04}, 1, 0},
{0x30, (uint8_t[]){0x0A}, 1, 0},
{0x02, (uint8_t[]){0x4F}, 1, 0},
{0x0B, (uint8_t[]){0x40}, 1, 0},
{0x12, (uint8_t[]){0x3E}, 1, 0},
{0x13, (uint8_t[]){0x78}, 1, 0},
{0x30, (uint8_t[]){0x0D}, 1, 0},
{0x0D, (uint8_t[]){0x04}, 1, 0},
{0x10, (uint8_t[]){0x0C}, 1, 0},
{0x11, (uint8_t[]){0x0C}, 1, 0},
{0x12, (uint8_t[]){0x0C}, 1, 0},
{0x13, (uint8_t[]){0x0C}, 1, 0},
{0x30, (uint8_t[]){0x00}, 1, 0},
// {0X3A, (uint8_t[]){0x55}, 1, 0},
{0x11, (uint8_t[]){0x00}, 1, 120},
{0x29, (uint8_t[]){0x00}, 1, 50},
};
static esp_err_t panel_jd9165_del(esp_lcd_panel_t *panel)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
if (jd9165->reset_gpio_num >= 0) {
gpio_reset_pin(jd9165->reset_gpio_num);
}
// Delete MIPI DPI panel
jd9165->del(panel);
ESP_LOGD(TAG, "del jd9165 panel @%p", jd9165);
free(jd9165);
return ESP_OK;
}
static esp_err_t panel_jd9165_init(esp_lcd_panel_t *panel)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
const jd9165_lcd_init_cmd_t *init_cmds = NULL;
uint16_t init_cmds_size = 0;
bool is_cmd_overwritten = false;
uint8_t ID[3];
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_rx_param(io, 0x04, ID, 3), TAG, "read ID failed");
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) {
jd9165->madctl_val,
}, 1), TAG, "send command failed");
// vendor specific initialization, it can be different between manufacturers
// should consult the LCD supplier for initialization sequence code
if (jd9165->init_cmds) {
init_cmds = jd9165->init_cmds;
init_cmds_size = jd9165->init_cmds_size;
} else {
init_cmds = vendor_specific_init_default;
init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(jd9165_lcd_init_cmd_t);
}
for (int i = 0; i < init_cmds_size; i++) {
// Check if the command has been used or conflicts with the internal
if (init_cmds[i].data_bytes > 0) {
switch (init_cmds[i].cmd) {
case LCD_CMD_MADCTL:
is_cmd_overwritten = true;
jd9165->madctl_val = ((uint8_t *)init_cmds[i].data)[0];
break;
default:
is_cmd_overwritten = false;
break;
}
if (is_cmd_overwritten) {
is_cmd_overwritten = false;
ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence",
init_cmds[i].cmd);
}
}
// Send command
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed");
vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms));
}
ESP_LOGD(TAG, "send init commands success");
ESP_RETURN_ON_ERROR(jd9165->init(panel), TAG, "init MIPI DPI panel failed");
return ESP_OK;
}
static esp_err_t panel_jd9165_reset(esp_lcd_panel_t *panel)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
// Perform hardware reset
if (jd9165->reset_gpio_num >= 0) {
gpio_set_level(jd9165->reset_gpio_num, !jd9165->flags.reset_level);
vTaskDelay(pdMS_TO_TICKS(5));
gpio_set_level(jd9165->reset_gpio_num, jd9165->flags.reset_level);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(jd9165->reset_gpio_num, !jd9165->flags.reset_level);
vTaskDelay(pdMS_TO_TICKS(120));
} else if (io) { // Perform software reset
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed");
vTaskDelay(pdMS_TO_TICKS(120));
}
return ESP_OK;
}
static esp_err_t panel_jd9165_invert_color(esp_lcd_panel_t *panel, bool invert_color_data)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
uint8_t command = 0;
ESP_RETURN_ON_FALSE(io, ESP_ERR_INVALID_STATE, TAG, "invalid panel IO");
if (invert_color_data) {
command = LCD_CMD_INVON;
} else {
command = LCD_CMD_INVOFF;
}
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed");
return ESP_OK;
}
static esp_err_t panel_jd9165_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
uint8_t madctl_val = jd9165->madctl_val;
ESP_RETURN_ON_FALSE(io, ESP_ERR_INVALID_STATE, TAG, "invalid panel IO");
// Control mirror through LCD command
if (mirror_x) {
madctl_val |= JD9165_CMD_GS_BIT;
} else {
madctl_val &= ~JD9165_CMD_GS_BIT;
}
if (mirror_y) {
madctl_val |= JD9165_CMD_SS_BIT;
} else {
madctl_val &= ~JD9165_CMD_SS_BIT;
}
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t []) {
madctl_val
}, 1), TAG, "send command failed");
jd9165->madctl_val = madctl_val;
return ESP_OK;
}
static esp_err_t panel_jd9165_disp_on_off(esp_lcd_panel_t *panel, bool on_off)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
int command = 0;
if (on_off) {
command = LCD_CMD_DISPON;
} else {
command = LCD_CMD_DISPOFF;
}
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed");
return ESP_OK;
}
#endif
@@ -0,0 +1,125 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "soc/soc_caps.h"
#if SOC_MIPI_DSI_SUPPORTED
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_mipi_dsi.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief LCD panel initialization commands.
*
*/
typedef struct {
int cmd; /*<! The specific LCD command */
const void *data; /*<! Buffer that holds the command specific data */
size_t data_bytes; /*<! Size of `data` in memory, in bytes */
unsigned int delay_ms; /*<! Delay in milliseconds after this command */
} jd9165_lcd_init_cmd_t;
/**
* @brief LCD panel vendor configuration.
*
* @note This structure needs to be passed to the `vendor_config` field in `esp_lcd_panel_dev_config_t`.
*
*/
typedef struct {
const jd9165_lcd_init_cmd_t *init_cmds; /*!< Pointer to initialization commands array. Set to NULL if using default commands.
* The array should be declared as `static const` and positioned outside the function.
* Please refer to `vendor_specific_init_default` in source file.
*/
uint16_t init_cmds_size; /*<! Number of commands in above array */
struct {
esp_lcd_dsi_bus_handle_t dsi_bus; /*!< MIPI-DSI bus configuration */
const esp_lcd_dpi_panel_config_t *dpi_config; /*!< MIPI-DPI panel configuration */
} mipi_config;
} jd9165_vendor_config_t;
/**
* @brief Create LCD panel for model JD9165
*
* @note Vendor specific initialization can be different between manufacturers, should consult the LCD supplier for initialization sequence code.
*
* @param[in] io LCD panel IO handle
* @param[in] panel_dev_config General panel device configuration
* @param[out] ret_panel Returned LCD panel handle
* @return
* - ESP_ERR_INVALID_ARG if parameter is invalid
* - ESP_OK on success
* - Otherwise on fail
*/
esp_err_t esp_lcd_new_panel_jd9165(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config,
esp_lcd_panel_handle_t *ret_panel);
/**
* @brief MIPI-DSI bus configuration structure
*
* @param[in] lane_num Number of data lanes
* @param[in] lane_mbps Lane bit rate in Mbps
*
*/
#define JD9165_PANEL_BUS_DSI_2CH_CONFIG() \
{ \
.bus_id = 0, \
.num_data_lanes = 2, \
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, \
.lane_bit_rate_mbps = 550, \
}
/**
* @brief MIPI-DBI panel IO configuration structure
*
*/
#define JD9165_PANEL_IO_DBI_CONFIG() \
{ \
.virtual_channel = 0, \
.lcd_cmd_bits = 8, \
.lcd_param_bits = 8, \
}
/**
* @brief MIPI DPI configuration structure
*
* @note refresh_rate = (dpi_clock_freq_mhz * 1000000) / (h_res + hsync_pulse_width + hsync_back_porch + hsync_front_porch)
* / (v_res + vsync_pulse_width + vsync_back_porch + vsync_front_porch)
*
* @param[in] px_format Pixel format of the panel
*
*/
#define JD9165_1024_600_PANEL_60HZ_DPI_CONFIG(px_format) \
{ \
.virtual_channel = 0, \
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, \
.dpi_clock_freq_mhz = 56, \
.pixel_format = px_format, \
.num_fbs = 1, \
.video_timing = { \
.h_size = 1024, \
.v_size = 600, \
.hsync_pulse_width = 40, \
.hsync_back_porch = 160, \
.hsync_front_porch = 160, \
.vsync_pulse_width = 10, \
.vsync_back_porch = 23, \
.vsync_front_porch = 12, \
}, \
.flags= { \
.use_dma2d = true, \
}, \
}
#endif
#ifdef __cplusplus
}
#endif
+310 -69
View File
@@ -1,5 +1,6 @@
// =====================================================================================
// ui.cpp - LVGL-Oberflaeche (Querformat 800x480), Design an die Web-UI angelehnt
// ui.cpp - LVGL-Oberflaeche (Querformat, 800x480 bzw. 1024x600 je nach Panel),
// Design an die Web-UI angelehnt
// =====================================================================================
// Navigation ueber ein Hamburger-Menue (Icon oben links -> Schublade von links).
// Seiten:
@@ -38,6 +39,10 @@ static lv_timer_t* g_toastTimer = nullptr;
static lv_obj_t* g_standbyScreen = nullptr; // Vollbild-Overlay im Standby (Uhr)
static lv_obj_t* g_standbyClock = nullptr;
static lv_obj_t* g_wakeConfirm = nullptr; // Bestaetigungsdialog "Aus dem Standby aufwecken?"
static lv_obj_t* g_wakeColdBtn = nullptr; // "Cold Extraction" im Aufweck-Dialog
static lv_obj_t* g_wakeMsg = nullptr; // Text des Aufweck-Dialogs (wechselt bei Abfrage-Modus)
static lv_obj_t* g_wakeYesLbl = nullptr; // Beschriftung des Bestaetigen-Knopfs
static lv_obj_t* g_wakeNoBtn = nullptr; // "Abbrechen" (entfaellt beim Auswahlfenster nach dem Aufwecken)
static bool g_wakeDialogOpen = false; // Dialog sichtbar -> Display hell schalten (lesbar)
static bool g_standbyShown = false; // Standby-Uhr-Overlay zuletzt als sichtbar geschaltet (fuer Ein-/Ausblend-Uebergang)
@@ -49,8 +54,8 @@ static lv_obj_t* g_otaStatus = nullptr;
static lv_obj_t* g_otaBytes = nullptr;
// Navigation
enum { PG_DASH = 0, PG_CHART, PG_STATS, PG_TEMP, PG_BREW, PG_SERVICE, PG_PROFILE, PG_LIGHT, PG_WIFI, PG_INFO, PG_COUNT };
static const char* PAGE_NAMES[PG_COUNT] = { "Dashboard", "Verlauf", "Statistik", "Temperatur", "Brew-Control", "Reinigung/Wartung", "Profile", "Licht", "WiFi", "Info" };
enum { PG_DASH = 0, PG_CHART, PG_STATS, PG_TEMP, PG_BREW, PG_COLDEX, PG_SERVICE, PG_PROFILE, PG_LIGHT, PG_WIFI, PG_INFO, PG_COUNT };
static const char* PAGE_NAMES[PG_COUNT] = { "Dashboard", "Verlauf", "Statistik", "Temperatur", "Brew-Control", "Cold Extraction", "Reinigung/Wartung", "Profile", "Licht", "WiFi", "Info" };
static lv_obj_t* g_content = nullptr;
static lv_obj_t* g_pages[PG_COUNT] = { nullptr };
static lv_obj_t* g_drawer = nullptr;
@@ -250,7 +255,16 @@ static lv_obj_t* swPI; static lv_obj_t* taPiDur; static lv_obj_t* taPiPause;
static lv_obj_t* swBBT; static lv_obj_t* taBbtSecs;
static lv_obj_t* swBBW; static lv_obj_t* taBbwTarget; static lv_obj_t* taBbwOffset;
static lv_obj_t* swSBT; static lv_obj_t* taSbtSecs;
static lv_obj_t* lblBrewColdEx = nullptr; // Cold-Extraction-Parameter (nur Anzeige)
// Cold-Extraction-Seite (eigener Menuepunkt)
static lv_obj_t* taCxMaxTemp = nullptr; static lv_obj_t* taCxPreInf = nullptr;
static lv_obj_t* taCxPreInfDuty = nullptr; static lv_obj_t* taCxPulseMs = nullptr;
static lv_obj_t* taCxDuty = nullptr; static lv_obj_t* taCxTarget = nullptr;
static lv_obj_t* taCxMaxSecs = nullptr; static lv_obj_t* taCxPumpMaxRun = nullptr;
static lv_obj_t* taCxPumpRest = nullptr;
static lv_obj_t* swCxEnabled = nullptr; // Freischaltung (entspricht dem Web-Toggle)
static lv_obj_t* swCxAskOnWake = nullptr; // Abfrage beim Aufwecken aus dem Standby
static lv_obj_t* lblCxPageHint = nullptr;
static uint32_t g_cxEditMs = 0; // waehrend der Eingabe die Felder nicht ueberschreiben
// WiFi
static lv_obj_t* wifiStatusLbl; static lv_obj_t* wifiHintLbl; static lv_obj_t* wifiList;
@@ -399,6 +413,29 @@ static lv_obj_t* add_num(lv_obj_t* parent, const char* label) {
attach_kb(ta, false);
return ta;
}
// Gruppen-Karte: abgesetzter Block mit Titel, in den Zeilen (row/add_switch/add_num)
// gelegt werden. Deutlich uebersichtlicher als eine lange Liste gleich aussehender
// Zeilen unter Zwischenueberschriften.
static lv_obj_t* group_card(lv_obj_t* parent, const char* title, const char* subtitle = nullptr) {
lv_obj_t* c = th_card(parent);
lv_obj_set_width(c, LV_PCT(100));
lv_obj_set_height(c, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(c, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(c, 2, 0);
lv_obj_clear_flag(c, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* t = lv_label_create(c);
lv_label_set_text(t, title);
lv_obj_set_style_text_color(t, COL_ACCENT, 0);
lv_obj_set_style_text_font(t, &lv_font_maven_pro_18, 0);
if (subtitle) {
lv_obj_t* s = lv_label_create(c);
lv_label_set_long_mode(s, LV_LABEL_LONG_WRAP);
lv_obj_set_width(s, LV_PCT(100));
lv_label_set_text(s, subtitle);
lv_obj_set_style_text_color(s, COL_TEXT_MUT, 0);
}
return c;
}
static lv_obj_t* section_title(lv_obj_t* parent, const char* txt) {
lv_obj_t* l = lv_label_create(parent);
lv_label_set_text(l, txt);
@@ -429,6 +466,7 @@ static lv_obj_t* make_page(lv_obj_t* parent) {
// =====================================================================================
// Brew-Control-Felder einmalig aus dem Zustand fuellen (nur beim Oeffnen der Seite,
// damit laufende State-Pushes die Eingaben nicht vor dem Speichern ueberschreiben).
static void populate_coldex(const MachineState& st); // Definition auf der Cold-Extraction-Seite
static void populate_brew(const MachineState& st) {
if (!swPI) return;
sw_set(swPI, st.piEnabled); ta_setf(taPiDur, st.piDurSecs, 1); ta_setf(taPiPause, st.piPauseSecs, 1);
@@ -462,6 +500,7 @@ static void show_page(int idx) {
if (idx == PG_PROFILE && g_client) g_client->sendListProfiles();
if (idx == PG_STATS) stats_request(); // Nutzungsstatistik beim Oeffnen frisch anfordern
if (idx == PG_BREW) populate_brew(g_state); // Felder beim Oeffnen frisch laden
if (idx == PG_COLDEX) populate_coldex(g_state);
if (idx == PG_SERVICE) populate_service(g_state);
}
static void drawer_open() { if (g_drawer) { lv_obj_remove_flag(g_drawer, LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(g_drawer); } }
@@ -484,7 +523,7 @@ static void err_icon_cb(lv_event_t*) { show_page(PG_DASH); } // Tipp auf Fehl
// --- Dezente Ein-/Ausblendungen (Opazitaets-Fade) -------------------------------------
// Bewusst ueber das 'opa'-Style (faellt via opa_recursive auf die Kinder durch) und NICHT
// ueber 'opa_layered' - Letzteres wuerde einen Vollbild-Zwischenpuffer (800x480) allozieren
// ueber 'opa_layered' - Letzteres wuerde einen Vollbild-Zwischenpuffer allozieren
// und koennte den LVGL-Speicherpool sprengen. So bleibt das Fade speicher-neutral.
static const uint32_t UI_FADE_MS = 450; // ruhige, unaufdringliche Dauer
@@ -560,6 +599,16 @@ static void wake_confirm_show() {
lv_obj_move_foreground(g_wakeConfirm);
}
static void wake_confirm_hide() { if (g_wakeConfirm) lv_obj_add_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN); }
// Einzige Quelle der Wahrheit dafuer, ob der Aufweck-Dialog die Auswahl "Espresso oder Cold
// Extraction" wirklich anbietet. Wird an ZWEI Stellen gebraucht: beim Aufbau des Dialogs und
// beim Absenden. Standen dort verschiedene Bedingungen, konnte der Dialog nur "Aufwecken"
// zeigen, aber trotzdem "wakeEspresso" senden - und damit das Auswahlfenster der S3
// schliessen. Dann war die Frage auf beiden Seiten weg (der Schalterweg hat diese Luecke
// nicht, weil er allein am S3 haengt).
static bool wake_choice_offered() {
return g_state.cxWakeChoice ||
(g_state.cxEnabled && g_state.cxArmable && g_state.cxAskOnWake);
}
static void standby_screen_cb(lv_event_t*) { wake_confirm_show(); } // Tipp auf Standby-Uhr -> nachfragen
static void wake_no_cb(lv_event_t*) { // Abbrechen -> zurueck in die Standby-Helligkeit
wake_confirm_hide();
@@ -570,7 +619,24 @@ static void wake_yes_cb(lv_event_t*) { // Aufwecken -> hell lassen, S3 bee
wake_confirm_hide();
// g_wakeDialogOpen bleibt true, bis der Standby wirklich endet (ui_update) - sonst wuerde
// das Display in der Uebergangsphase kurz auf Standby-Helligkeit abdunkeln.
if (g_client) { g_client->sendAction("deactivateStandby"); showToast("Maschine wird aufgeweckt...", false); }
if (!g_client) return;
// "wakeEspresso" beendet den Standby UND schliesst das Auswahlfenster der S3 - das ist
// nur dann richtig, wenn dieser Dialog die Auswahl gerade wirklich angeboten hat. Sonst
// "deactivateStandby": dann oeffnet die S3 ihr eigenes Auswahlfenster, und die Frage
// kommt trotzdem (gleicher Weg wie beim Aufwecken ueber den Schalter).
g_client->sendAction(wake_choice_offered() ? "wakeEspresso" : "deactivateStandby");
showToast(g_state.standbyActive ? "Maschine wird aufgeweckt..." : "Espresso: Kessel heizt auf.", false);
}
// Aufwecken direkt in die kalte Extraktion. Reihenfolge ist wichtig: erst den Modus scharf
// stellen, dann den Standby beenden - sonst heizt der Wasserkreis in der Zwischenzeit an,
// und genau das soll die Funktion ja verhindern.
static void wake_cold_cb(lv_event_t*) {
wake_confirm_hide();
if (g_client) {
g_client->sendAction("startColdExtraction");
if (g_state.standbyActive) g_client->sendAction("deactivateStandby");
showToast("Cold Extraction aktiviert.", false);
}
}
bool ui_wake_dialog_open() { return g_wakeDialogOpen; } // fuer die Helligkeitssteuerung in onState
@@ -1315,16 +1381,7 @@ static void build_temp(lv_obj_t* p) {
lv_obj_add_event_cb(swEco, eco_cb, LV_EVENT_VALUE_CHANGED, nullptr);
swMaint = add_switch(p, "Wartungsmodus");
lv_obj_add_event_cb(swMaint, maint_cb, LV_EVENT_VALUE_CHANGED, nullptr);
swColdEx = add_switch(p, "Cold Extraction");
lv_obj_add_event_cb(swColdEx, coldex_cb, LV_EVENT_VALUE_CHANGED, nullptr);
lblColdExHint = lv_label_create(p);
lv_label_set_long_mode(lblColdExHint, LV_LABEL_LONG_WRAP);
lv_obj_set_width(lblColdExHint, LV_PCT(100));
lv_obj_set_style_text_color(lblColdExHint, COL_TEXT_DIM, 0);
lv_label_set_text(lblColdExHint, "");
// Beide Zeilen bleiben versteckt, solange die S3 die Funktion nicht freigeschaltet meldet
lv_obj_add_flag(lv_obj_get_parent(swColdEx), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(lblColdExHint, LV_OBJ_FLAG_HIDDEN);
// Cold Extraction hat eine eigene Seite (PG_COLDEX) - Schalter und Parameter stehen dort.
}
// =====================================================================================
@@ -1822,12 +1879,16 @@ static void brew_live_tick() {
}
// Zielfortschritt: Brew-by-Weight (Gewicht) vor Brew-by-Time (Zeit), sonst versteckt
if (g_bzBarRow && g_bzBar && g_bzBarLbl) {
bool bbw = scale && g_state.bbwEnabled && g_state.bbwTarget > 0.0f;
// Ein kalter Bezug hat sein eigenes Ziel (cxTarget) und ersetzt Brew-by-Weight -
// sonst stuende hier das Espresso-Ziel.
const bool cxShotNow = g_state.cxShot && g_state.cxTarget > 0.0f;
const float wTarget = cxShotNow ? g_state.cxTarget : g_state.bbwTarget;
bool bbw = scale && (cxShotNow || (g_state.bbwEnabled && g_state.bbwTarget > 0.0f));
if (bbw || (g_shotBbtMode && g_bbtTarget > 0.0f)) {
int pct;
if (bbw) {
pct = (int)(w / g_state.bbwTarget * 100.0f + 0.5f);
snprintf(b, sizeof(b), "%.1f g / %.1f g", w, g_state.bbwTarget);
pct = (int)(w / wTarget * 100.0f + 0.5f);
snprintf(b, sizeof(b), "%.1f g / %.1f g", w, wTarget);
} else {
pct = (int)(el / g_bbtTarget * 100.0f + 0.5f);
snprintf(b, sizeof(b), "%.1f s / %.1f s", el, g_bbtTarget);
@@ -1998,28 +2059,112 @@ static void send_save_brew(lv_event_t*) {
showToast("Speichere Brühen...", false);
}
static void build_brew(lv_obj_t* p) {
section_title(p, "Pre-Infusion");
swPI = add_switch(p, "Aktiv");
taPiDur = add_num(p, "Dauer (s)");
taPiPause = add_num(p, "Pause (s)");
section_title(p, "Brew-by-Time / Weight / Steam-by-Time");
swBBT = add_switch(p, "Brew-by-Time aktiv");
taBbtSecs = add_num(p, "Zielzeit (s)");
swBBW = add_switch(p, "Brew-by-Weight aktiv");
taBbwTarget= add_num(p, "Zielgewicht (g)");
taBbwOffset= add_num(p, "Offset (g)");
swSBT = add_switch(p, "Steam-by-Time aktiv");
taSbtSecs = add_num(p, "Dampf-Zielzeit (s)");
// Jede Funktion in einer eigenen Karte - vorher standen alle Zeilen unter blossen
// Zwischenueberschriften untereinander und waren kaum auseinanderzuhalten.
lv_obj_t* cPI = group_card(p, "Pre-Infusion", "Pumpe an - aus - an, vor dem eigentlichen Bezug");
swPI = add_switch(cPI, "Aktiv");
taPiDur = add_num(cPI, "Dauer (s)");
taPiPause = add_num(cPI, "Pause (s)");
lv_obj_t* cBBT = group_card(p, "Brew-by-Time", "Beendet den Bezug nach der Zielzeit");
swBBT = add_switch(cBBT, "Aktiv");
taBbtSecs = add_num(cBBT, "Zielzeit (s)");
lv_obj_t* cBBW = group_card(p, "Brew-by-Weight", "Beendet den Bezug beim Zielgewicht (braucht Waage)");
swBBW = add_switch(cBBW, "Aktiv");
taBbwTarget= add_num(cBBW, "Zielgewicht (g)");
taBbwOffset= add_num(cBBW, "Offset (g)");
lv_obj_t* cSBT = group_card(p, "Dampf-Timer", "Beendet den Dampfbezug automatisch");
swSBT = add_switch(cSBT, "Aktiv");
taSbtSecs = add_num(cSBT, "Zielzeit (s)");
add_save_button(p, "Brühen speichern", send_save_brew);
// Cold Extraction: nur Anzeige. Die Parameter liegen bewusst nur in der Web-UI der S3
// (maschinenabhaengig, nicht ueber saveBrew erreichbar); geschaltet wird der Modus
// auf der Temperatur-Seite unter "Modi".
lblBrewColdEx = lv_label_create(p);
lv_label_set_long_mode(lblBrewColdEx, LV_LABEL_LONG_WRAP);
lv_obj_set_width(lblBrewColdEx, LV_PCT(100));
lv_obj_set_style_text_color(lblBrewColdEx, COL_TEXT_DIM, 0);
lv_label_set_text(lblBrewColdEx, "");
lv_obj_add_flag(lblBrewColdEx, LV_OBJ_FLAG_HIDDEN);
}
// =====================================================================================
// Cold Extraction (eigene Seite)
// =====================================================================================
static void send_save_coldex(lv_event_t*) {
if (!g_client) return;
String f;
f += "\"cxEnabled\":"; f += boolStr(sw_get(swCxEnabled)); f += ",";
f += "\"cxMaxTemp\":"; f += String(ta_f(taCxMaxTemp), 1); f += ",";
f += "\"cxPreInf\":"; f += String(ta_f(taCxPreInf), 1); f += ",";
f += "\"cxPreInfDuty\":"; f += String(ta_f(taCxPreInfDuty), 1); f += ",";
f += "\"cxPulseMs\":"; f += String((int)ta_f(taCxPulseMs)); f += ",";
f += "\"cxDuty\":"; f += String(ta_f(taCxDuty), 1); f += ",";
f += "\"cxTarget\":"; f += String(ta_f(taCxTarget), 1); f += ",";
f += "\"cxMaxSecs\":"; f += String(ta_f(taCxMaxSecs), 1); f += ",";
f += "\"cxPumpMaxRun\":"; f += String(ta_f(taCxPumpMaxRun), 1); f += ",";
f += "\"cxPumpRest\":"; f += String(ta_f(taCxPumpRest), 1); f += ",";
f += "\"cxAskOnWake\":"; f += boolStr(sw_get(swCxAskOnWake));
g_client->sendCommand("saveBrew", f);
g_cxEditMs = millis();
showToast("Speichere Cold Extraction...", false);
}
static void build_coldex(lv_obj_t* p) {
lv_obj_t* cMode = group_card(p, "Modus",
"Schaltet die Wasserheizung ab. Auch im Standby und bei warmem Kessel schaltbar; "
"bezogen werden kann erst unterhalb der Freigabetemperatur.");
swColdEx = add_switch(cMode, "Cold Extraction aktiv");
lv_obj_add_event_cb(swColdEx, coldex_cb, LV_EVENT_VALUE_CHANGED, nullptr);
lblColdExHint = lv_label_create(cMode);
lv_label_set_long_mode(lblColdExHint, LV_LABEL_LONG_WRAP);
lv_obj_set_width(lblColdExHint, LV_PCT(100));
lv_obj_set_style_text_color(lblColdExHint, COL_TEXT_DIM, 0);
lv_label_set_text(lblColdExHint, "");
lv_obj_t* cBase = group_card(p, "Grundeinstellung");
swCxEnabled = add_switch(cBase, "Funktion freigeschaltet");
taCxMaxTemp = add_num(cBase, "Freigabe bis (°C)");
swCxAskOnWake = add_switch(cBase, "Beim Aufwecken fragen");
lv_obj_t* cPre = group_card(p, "Vorbenetzung",
"Deutlich schwächer als der Hauptbezug: Kaltes Mehl quillt nicht, volle Leistung legt "
"Kanäle an, die sich nie wieder schließen.");
taCxPreInf = add_num(cPre, "Dauer (s)");
taCxPreInfDuty = add_num(cPre, "Pumpenleistung (%)");
lv_obj_t* cMain = group_card(p, "Hauptbezug",
"Das kalte Kaffeebett ist viel durchlässiger als ein heißes - niedrige Werte sind normal.");
taCxPulseMs = add_num(cMain, "Pulsperiode (ms)");
taCxDuty = add_num(cMain, "Pumpenleistung (%)");
lv_obj_t* cEnd = group_card(p, "Bezugsende",
"Ohne Waage ist das Zielgewicht wirkungslos, dann zählt die maximale Dauer.");
taCxTarget = add_num(cEnd, "Zielgewicht (g)");
taCxMaxSecs = add_num(cEnd, "Max. Dauer (s)");
lv_obj_t* cPump = group_card(p, "Pumpenschutz",
"Gezählt wird die kumulierte Einschaltzeit der Pumpe, nicht die Bezugsdauer.");
taCxPumpMaxRun = add_num(cPump, "Max. Laufzeit (s)");
taCxPumpRest = add_num(cPump, "Zwangspause (s)");
lblCxPageHint = lv_label_create(p);
lv_label_set_long_mode(lblCxPageHint, LV_LABEL_LONG_WRAP);
lv_obj_set_width(lblCxPageHint, LV_PCT(100));
lv_obj_set_style_text_color(lblCxPageHint, COL_WARN, 0);
lv_label_set_text(lblCxPageHint, "");
lv_obj_add_flag(lblCxPageHint, LV_OBJ_FLAG_HIDDEN);
add_save_button(p, "Cold Extraction speichern", send_save_coldex);
}
// Felder nur beim Oeffnen der Seite fuellen - laufende State-Pushes duerfen eine
// begonnene Eingabe nicht ueberschreiben (gleiches Muster wie populate_brew).
static void populate_coldex(const MachineState& st) {
if (!taCxMaxTemp) return;
sw_set(swCxEnabled, st.cxEnabled);
sw_set(swCxAskOnWake, st.cxAskOnWake);
ta_setf(taCxMaxTemp, st.cxMaxTemp, 1);
ta_setf(taCxPreInf, st.cxPreInfSec, 0);
ta_setf(taCxPreInfDuty, st.cxPreInfDuty, 0);
ta_setf(taCxPulseMs, (float)st.cxPulseMs, 0);
ta_setf(taCxDuty, st.cxDuty, 0);
ta_setf(taCxTarget, st.cxTarget, 0);
ta_setf(taCxMaxSecs, st.cxMaxSecs, 0);
ta_setf(taCxPumpMaxRun, st.cxPumpMaxRun, 0);
ta_setf(taCxPumpRest, st.cxPumpRest, 0);
}
// =====================================================================================
@@ -3087,7 +3232,7 @@ void ui_init(ProtocolClient* client) {
// --- Content-Bereich (Seiten gestapelt) ---
g_content = lv_obj_create(scr);
lv_obj_set_size(g_content, LV_PCT(100), 480 - 44);
lv_obj_set_size(g_content, LV_PCT(100), DISP_VER_RES - 44);
lv_obj_set_pos(g_content, 0, 44);
lv_obj_set_style_bg_opa(g_content, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(g_content, 0, 0);
@@ -3100,6 +3245,7 @@ void ui_init(ProtocolClient* client) {
g_pages[PG_TEMP] = make_page(g_content); build_temp(g_pages[PG_TEMP]);
g_pages[PG_LIGHT] = make_page(g_content); build_light(g_pages[PG_LIGHT]);
g_pages[PG_BREW] = make_page(g_content); build_brew(g_pages[PG_BREW]);
g_pages[PG_COLDEX] = make_page(g_content); build_coldex(g_pages[PG_COLDEX]);
g_pages[PG_SERVICE] = make_page(g_content); build_service(g_pages[PG_SERVICE]);
g_pages[PG_WIFI] = make_page(g_content); build_wifi(g_pages[PG_WIFI]);
g_pages[PG_PROFILE] = make_page(g_content); build_profiles(g_pages[PG_PROFILE]);
@@ -3107,7 +3253,7 @@ void ui_init(ProtocolClient* client) {
// --- Schublade (Scrim + Menue-Panel) ---
g_drawer = lv_obj_create(scr);
lv_obj_set_size(g_drawer, LV_PCT(100), 480 - 44);
lv_obj_set_size(g_drawer, LV_PCT(100), DISP_VER_RES - 44);
lv_obj_set_pos(g_drawer, 0, 44);
lv_obj_set_style_bg_color(g_drawer, lv_color_hex(0x000000), 0);
lv_obj_set_style_bg_opa(g_drawer, LV_OPA_50, 0);
@@ -3202,12 +3348,12 @@ void ui_init(ProtocolClient* client) {
lv_obj_set_flex_align(wakeCard, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(wakeCard, 18, 0);
lv_obj_clear_flag(wakeCard, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* wakeMsg = lv_label_create(wakeCard);
lv_obj_set_width(wakeMsg, LV_PCT(100));
lv_obj_set_style_text_align(wakeMsg, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(wakeMsg, COL_TEXT, 0);
lv_obj_set_style_text_font(wakeMsg, &lv_font_maven_pro_18, 0);
lv_label_set_text(wakeMsg, "Die Maschine befindet sich im Standby.\nAus dem Standby aufwecken?");
g_wakeMsg = lv_label_create(wakeCard);
lv_obj_set_width(g_wakeMsg, LV_PCT(100));
lv_obj_set_style_text_align(g_wakeMsg, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(g_wakeMsg, COL_TEXT, 0);
lv_obj_set_style_text_font(g_wakeMsg, &lv_font_maven_pro_18, 0);
lv_label_set_text(g_wakeMsg, "Die Maschine befindet sich im Standby.\nAus dem Standby aufwecken?");
lv_obj_t* wakeBtns = lv_obj_create(wakeCard);
lv_obj_set_width(wakeBtns, LV_PCT(100));
lv_obj_set_height(wakeBtns, LV_SIZE_CONTENT);
@@ -3221,9 +3367,18 @@ void ui_init(ProtocolClient* client) {
lv_obj_t* wakeNo = th_button(wakeBtns, "Abbrechen", COL_CARD2, COL_TEXT);
lv_obj_set_width(wakeNo, 150);
lv_obj_add_event_cb(wakeNo, wake_no_cb, LV_EVENT_CLICKED, nullptr);
g_wakeNoBtn = wakeNo;
lv_obj_t* wakeYes = th_accent_button(wakeBtns, "Aufwecken");
lv_obj_set_width(wakeYes, 150);
lv_obj_add_event_cb(wakeYes, wake_yes_cb, LV_EVENT_CLICKED, nullptr);
g_wakeYesLbl = lv_obj_get_child(wakeYes, 0); // Label des Knopfs (fuer "Espresso" im Abfrage-Modus)
// Zweite Zeile: direkt in die kalte Extraktion aufwecken. Ohne diesen Weg muesste man
// erst normal aufwecken (Maschine heizt an) und den Modus danach umschalten.
// Nur sichtbar, wenn die S3 die Funktion freigeschaltet meldet (siehe ui_update).
g_wakeColdBtn = th_button(wakeCard, "Cold Extraction (Wasser bleibt kalt)", COL_CARD2, COL_ACCENT);
lv_obj_set_width(g_wakeColdBtn, LV_PCT(100));
lv_obj_add_event_cb(g_wakeColdBtn, wake_cold_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_add_flag(g_wakeColdBtn, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN);
// OTA-Overlay (Vollbild, waehrend Firmware-Update)
@@ -3419,7 +3574,9 @@ void ui_update(const MachineState& st) {
}
// Aufweck-Ablauf: nach beendetem Standby Dialog schliessen und Helligkeits-Flag loeschen
// (auch wenn der Dialog schon ausgeblendet ist, weil "Aufwecken" das Flag bewusst haelt).
if (!st.standbyActive) {
// Bei offenem Auswahlfenster (cxWakeChoice) bleibt der Dialog stehen, obwohl der Standby
// schon beendet ist - genau dafuer ist er da.
if (!st.standbyActive && !st.cxWakeChoice) {
g_wakeDialogOpen = false;
if (g_wakeConfirm && !lv_obj_has_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN))
lv_obj_add_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN);
@@ -3446,6 +3603,67 @@ void ui_update(const MachineState& st) {
// Cold-Extraction-Hinweis: erklaert dauerhaft, warum der Wasserkreis nicht heizt.
// Transiente Meldung (abgewiesener Start / Abbruch) hat Vorrang vor dem Dauerzustand.
// Ohne nutzbare Waage laeuft die kalte Extraktion rein zeitgesteuert (Zielgewicht und
// Stillstands-Erkennung greifen nicht) - das wird dauerhaft mit angezeigt.
const bool cxScaleOk = st.scaleEnabled && st.scaleConnected;
// "Aufwecken mit Cold Extraction" nur anbieten, wenn die Funktion freigeschaltet und
// umschaltbar ist und der Modus nicht ohnehin schon laeuft.
// Abfrage-Modus: Der Aufweck-Dialog wird zur Auswahl "Espresso oder Cold Extraction".
// Abschaltbar ueber die Einstellung cxAskOnWake, damit man sich den Zwischenschritt
// sparen kann, wenn gerade keine kalten Bezuege anstehen.
{
// Zwei Faelle: Vorab-Abfrage beim Antippen im Standby (askMode) und das von der S3
// geoeffnete Auswahlfenster NACH dem Aufwecken (choiceOpen) - letzteres deckt auch
// das Aufwecken per Schalter ab, wo es keine Vorab-Abfrage geben kann.
// Kein "!st.cxActive": Der Modus ist persistent, die Auswahl muss in beide
// Richtungen gehen - sonst haengt man nach dem ersten kalten Bezug dauerhaft fest.
const bool choiceOpen = st.cxWakeChoice;
const bool askMode = wake_choice_offered(); // g_state == st (oben zugewiesen)
if (g_wakeColdBtn) {
if (askMode) lv_obj_remove_flag(g_wakeColdBtn, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(g_wakeColdBtn, LV_OBJ_FLAG_HIDDEN);
}
// Im Auswahlfenster gibt es nichts abzubrechen - laeuft die Zeit ab, wird geheizt.
if (g_wakeNoBtn) {
if (choiceOpen) lv_obj_add_flag(g_wakeNoBtn, LV_OBJ_FLAG_HIDDEN);
else lv_obj_remove_flag(g_wakeNoBtn, LV_OBJ_FLAG_HIDDEN);
}
if (g_wakeMsg) {
if (choiceOpen) {
char b[128];
snprintf(b, sizeof(b),
"Womit weiter?\nEspresso heizt auf, Cold Extraction bleibt kalt.\n"
"Ohne Auswahl wird in %ld s geheizt.", st.cxWakeChoiceSec);
label_set_if_changed(g_wakeMsg, b);
} else {
label_set_if_changed(g_wakeMsg, askMode
? "Die Maschine befindet sich im Standby.\nWomit aufwecken?"
: "Die Maschine befindet sich im Standby.\nAus dem Standby aufwecken?");
}
}
if (g_wakeYesLbl) {
label_set_if_changed(g_wakeYesLbl, askMode ? "Espresso" : "Aufwecken");
}
// Aktive Wahl hervorheben: Bei laufendem Modus ist "Cold Extraction" der Ist-Zustand,
// "Espresso" schaltet ihn ab - andersherum genauso.
if (g_wakeColdBtn && askMode) {
lv_obj_set_style_bg_color(g_wakeColdBtn, st.cxActive ? COL_ACCENT : COL_CARD2, 0);
lv_obj_t* coldLbl = lv_obj_get_child(g_wakeColdBtn, 0);
if (coldLbl) {
lv_obj_set_style_text_color(coldLbl, st.cxActive ? lv_color_hex(0x000000) : COL_ACCENT, 0);
label_set_if_changed(coldLbl, st.cxActive ? "Cold Extraction (aktiv lassen)"
: "Cold Extraction (Wasser bleibt kalt)");
}
}
// Auswahlfenster von der S3 -> Dialog von sich aus zeigen (kein Antippen noetig)
if (choiceOpen && g_wakeConfirm && lv_obj_has_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN)) {
wake_confirm_show();
} else if (!choiceOpen && !st.standbyActive && g_wakeConfirm &&
!lv_obj_has_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN)) {
wake_confirm_hide(); // Fenster abgelaufen oder entschieden
g_wakeDialogOpen = false;
}
}
if (st.cxNotice == 2) {
status_set(ST_COLDEX, true, LV_SYMBOL_WARNING " Cold Extraction beendet: kein Zulauf",
COL_WARN, lv_color_hex(0x000000));
@@ -3456,6 +3674,9 @@ void ui_update(const MachineState& st) {
String t = "Cold Extraction nicht möglich - ";
t += coldex_block_text(st.cxBlock, st.cxMaxTemp, st.tempW);
status_set(ST_COLDEX, true, t.c_str(), COL_WARN, lv_color_hex(0x000000));
} else if (st.cxNotice == 4) {
status_set(ST_COLDEX, true, LV_SYMBOL_WARNING " Cold Extraction ohne Waage: Bezug endet nach Zeit",
COL_WARN, lv_color_hex(0x000000));
} else if (st.cxActive && st.cxResting) {
status_set(ST_COLDEX, true, "Cold Extraction: Pumpenpause (Pumpenschutz)",
COL_ACCENT, lv_color_hex(0x000000));
@@ -3463,6 +3684,11 @@ void ui_update(const MachineState& st) {
String t = "Cold Extraction aktiv, Bezug gesperrt - ";
t += coldex_block_text(st.cxBlock, st.cxMaxTemp, st.tempW);
status_set(ST_COLDEX, true, t.c_str(), COL_WARN, lv_color_hex(0x000000));
} else if (st.cxActive && !cxScaleOk) {
// Dauerhinweis, solange ohne Waage gefahren wird: das Zielgewicht greift nicht,
// der Bezug endet erst nach der maximalen Bezugsdauer.
status_set(ST_COLDEX, true, "Cold Extraction aktiv - ohne Waage, Ende nach Zeit",
COL_WARN, lv_color_hex(0x000000));
} else if (st.cxActive) {
status_set(ST_COLDEX, true, "Cold Extraction aktiv - Wasserkreis heizt nicht",
lv_color_hex(0x1976d2), COL_TEXT);
@@ -3773,7 +3999,10 @@ void ui_update(const MachineState& st) {
else lv_obj_add_flag(cellBezug, LV_OBJ_FLAG_HIDDEN);
}
if (lblBezug) {
bool bbwShot = st.shotActive && st.bbwEnabled && st.bbwTarget > 0.0f;
// Bei kaltem Bezug zaehlt cxTarget, nicht das Brew-by-Weight-Ziel
const bool cxShotNow = st.cxShot && st.cxTarget > 0.0f;
const float shotTarget = cxShotNow ? st.cxTarget : st.bbwTarget;
bool bbwShot = st.shotActive && (cxShotNow || (st.bbwEnabled && st.bbwTarget > 0.0f));
// Winzige negative Messwerte (z.B. -0.04 g) oder negatives Float-Null wuerden von
// "%.1f" als "-0.0" dargestellt -> auf 0.0 normalisieren (kein fuehrendes Minus).
float wDisp = st.weight;
@@ -3782,14 +4011,14 @@ void ui_update(const MachineState& st) {
lv_label_set_text(lblBezug, "--");
} else if (bbwShot) {
// Waehrend des Bezugs mit Brew-by-Weight: Ist oben, Ziel darunter (zweizeilig)
lv_label_set_text_fmt(lblBezug, "%.1f g\n/ %.1f g", wDisp, st.bbwTarget);
lv_label_set_text_fmt(lblBezug, "%.1f g\n/ %.1f g", wDisp, shotTarget);
} else {
lv_label_set_text_fmt(lblBezug, "%.1f g", wDisp);
}
// Fortschrittsbalken Gewicht/Ziel (nur waehrend BBW-Bezug)
if (g_bbwBar) {
if (bbwShot) {
int pct = (int)(st.weight / st.bbwTarget * 100.0f + 0.5f);
int pct = (int)(st.weight / shotTarget * 100.0f + 0.5f);
if (pct < 0) pct = 0; if (pct > 100) pct = 100;
lv_bar_set_value(g_bbwBar, pct, LV_ANIM_OFF);
lv_obj_remove_flag(g_bbwBar, LV_OBJ_FLAG_HIDDEN);
@@ -3840,18 +4069,27 @@ void ui_update(const MachineState& st) {
if (lblColdExHint) lv_obj_add_flag(lblColdExHint, LV_OBJ_FLAG_HIDDEN);
}
sw_set(swColdEx, st.cxActive);
// Einschalten nur bei Freigabe; Ausschalten immer - ausser waehrend eines kalten Bezugs
bool usable = st.cxEnabled && (st.cxActive ? !st.cxShot : st.cxAllowed);
// Umschalten haengt an cxArmable, NICHT an cxAllowed: Der Modus muss auch im Standby
// und bei warmem Kessel scharf zu stellen sein. Waehrend eines kalten Bezugs gesperrt.
bool usable = st.cxEnabled && st.cxArmable && !(st.cxActive && st.cxShot);
if (usable) lv_obj_remove_state(swColdEx, LV_STATE_DISABLED);
else lv_obj_add_state(swColdEx, LV_STATE_DISABLED);
if (lblColdExHint && st.cxEnabled) {
const bool scaleOk = st.scaleEnabled && st.scaleConnected;
String hint;
if (st.cxActive) {
hint = "Wasserkreis heizt nicht (bleibt über Standby und Neustart aktiv). ";
if (st.cxAllowed) {
if (st.standbyActive) {
hint += "Nach dem Aufwecken bleibt der Wasserkreis aus.";
} else if (st.cxAllowed) {
char b[96];
snprintf(b, sizeof(b), "Bezug regulär starten - Ziel %.0f g, max %.0f s.",
(double)st.cxTarget, (double)st.cxMaxSecs);
if (scaleOk) {
snprintf(b, sizeof(b), "Bezug regulär starten - Ziel %.0f g, max %.0f s.",
(double)st.cxTarget, (double)st.cxMaxSecs);
} else {
snprintf(b, sizeof(b), "Bezug regulär starten - Ende nach %.0f s.",
(double)st.cxMaxSecs);
}
hint += b;
} else {
hint += coldex_block_text(st.cxBlock, st.cxMaxTemp, st.tempW);
@@ -3865,22 +4103,25 @@ void ui_update(const MachineState& st) {
(double)st.tempW, (double)st.cxMaxTemp);
hint = b;
}
if (!scaleOk) {
hint += " Ohne Waage: Zielgewicht und Stillstands-Erkennung greifen nicht, "
"der Bezug endet erst nach der maximalen Bezugsdauer.";
}
label_set_if_changed(lblColdExHint, hint.c_str());
}
}
if (lblBrewColdEx) {
if (st.cxEnabled) {
char b[256]; // der Text ist ~205 Zeichen lang (das ° zaehlt als 2 Bytes)
snprintf(b, sizeof(b),
"Cold Extraction (nur Anzeige, Einstellung in der Web-UI):\n"
"Freigabe < %.1f °C | Vorbenetzung %.0f s | Ziel %.0f g | max. %.0f s\n"
"Geschaltet wird der Modus auf der Seite \"Temperaturen\" unter \"Modi\".",
(double)st.cxMaxTemp, (double)st.cxPreInfSec,
(double)st.cxTarget, (double)st.cxMaxSecs);
label_set_if_changed(lblBrewColdEx, b);
lv_obj_remove_flag(lblBrewColdEx, LV_OBJ_FLAG_HIDDEN);
if (lblCxPageHint) {
// Warnhinweis auf der Cold-Extraction-Seite (Waage fehlt bzw. Funktion gesperrt)
String h;
if (st.cxEnabled && !cxScaleOk) {
h = "Keine Waage aktiv/verbunden: Zielgewicht und Stillstands-Erkennung greifen nicht, "
"der Bezug endet erst nach der maximalen Dauer.";
}
if (h.length() > 0) {
label_set_if_changed(lblCxPageHint, h.c_str());
lv_obj_remove_flag(lblCxPageHint, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(lblBrewColdEx, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(lblCxPageHint, LV_OBJ_FLAG_HIDDEN);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

@@ -0,0 +1,17 @@
# 请使用arduino_esp32_v3.1版本
# lv_conf.h文件必要设置(lvgl_v8)
```c
#define LV_COLOR_DEPTH 16
#define LV_COLOR_16_SWAP 0
#define LV_MEM_CUSTOM 1
#define LV_TICK_CUSTOM 1
#define LV_MEMCPY_MEMSET_STD 1
#define LV_ATTRIBUTE_FAST_MEM IRAM_ATTR
```
@@ -0,0 +1,115 @@
#pragma GCC push_options
#pragma GCC optimize("O3")
#include <Arduino.h>
#include "lvgl.h"
#include "demos/lv_demos.h"
#include "pins_config.h"
#include "src/lcd/jd9165_lcd.h"
#include "src/touch/gt911_touch.h"123
jd9165_lcd lcd = jd9165_lcd(LCD_RST);
gt911_touch touch = gt911_touch(TP_I2C_SDA, TP_I2C_SCL, TP_RST, TP_INT);
static lv_disp_draw_buf_t draw_buf;
static lv_color_t *buf;
static lv_color_t *buf1;
// 显示刷新
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
const int offsetx1 = area->x1;
const int offsetx2 = area->x2;
const int offsety1 = area->y1;
const int offsety2 = area->y2;
lcd.lcd_draw_bitmap(offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, &color_p->full);
lv_disp_flush_ready(disp); // 告诉lvgl刷新完成
}
void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data)
{
bool touched;
uint16_t touchX, touchY;
touched = touch.getTouch(&touchX, &touchY);
if (!touched)
{
data->state = LV_INDEV_STATE_REL;
}
else
{
data->state = LV_INDEV_STATE_PR;
// 设置坐标
data->point.x = touchX;
data->point.y = touchY;
Serial.printf("x=%d,y=%d \r\n",touchX,touchY);
}
}
static void lvgl_port_update_callback(lv_disp_drv_t *drv)
{
switch (drv->rotated) {
case LV_DISP_ROT_NONE:
touch.set_rotation(0);
break;
case LV_DISP_ROT_90:
touch.set_rotation(1);
break;
case LV_DISP_ROT_180:
touch.set_rotation(2);
break;
case LV_DISP_ROT_270:
touch.set_rotation(3);
break;
}
}
void setup()
{
Serial.begin(115200);
Serial.println("ESP32P4 MIPI DSI LVGL");
lcd.begin();
touch.begin();
lv_init();
size_t buffer_size = sizeof(int32_t) * LCD_H_RES * LCD_V_RES;
// buf = (int32_t *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);
// buf1 = (int32_t *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);
buf = (lv_color_t *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);
buf1 = (lv_color_t *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);
assert(buf);
assert(buf1);
lv_disp_draw_buf_init(&draw_buf, buf, buf1, LCD_H_RES * LCD_V_RES);
static lv_disp_drv_t disp_drv;
/*Initialize the display*/
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = LCD_H_RES;
disp_drv.ver_res = LCD_V_RES;
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &draw_buf;
disp_drv.full_refresh = true;
lv_disp_drv_register(&disp_drv);
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = my_touchpad_read;
lv_indev_drv_register(&indev_drv);
// lv_disp_set_rotation(NULL, 0);
lv_demo_widgets(); /* 小部件示例 */
// lv_demo_music(); /* 类似智能手机的现代音乐播放器演示 */
// lv_demo_stress(); /* LVGL 压力测试 */
// lv_demo_benchmark(); /* 用于测量 LVGL 性能或比较不同设置的演示 */
}
void loop()
{
lv_timer_handler();
delay(5);
}
@@ -0,0 +1,12 @@
#pragma once
#define LCD_H_RES 1024
#define LCD_V_RES 600
#define LCD_RST 27
#define LCD_LED 23
#define TP_I2C_SDA 7
#define TP_I2C_SCL 8
#define TP_RST 22
#define TP_INT 21
@@ -0,0 +1,339 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "soc/soc_caps.h"
#if SOC_MIPI_DSI_SUPPORTED
#include "esp_check.h"
#include "esp_log.h"
#include "esp_lcd_panel_commands.h"
#include "esp_lcd_panel_interface.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_mipi_dsi.h"
#include "esp_lcd_panel_vendor.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_lcd_jd9165.h"
#define JD9165_CMD_GS_BIT (1 << 0)
#define JD9165_CMD_SS_BIT (1 << 1)
typedef struct {
esp_lcd_panel_io_handle_t io;
int reset_gpio_num;
uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register
uint8_t colmod_val; // save surrent value of LCD_CMD_COLMOD register
const jd9165_lcd_init_cmd_t *init_cmds;
uint16_t init_cmds_size;
struct {
unsigned int reset_level: 1;
} flags;
// To save the original functions of MIPI DPI panel
esp_err_t (*del)(esp_lcd_panel_t *panel);
esp_err_t (*init)(esp_lcd_panel_t *panel);
} jd9165_panel_t;
static const char *TAG = "jd9165";
static esp_err_t panel_jd9165_del(esp_lcd_panel_t *panel);
static esp_err_t panel_jd9165_init(esp_lcd_panel_t *panel);
static esp_err_t panel_jd9165_reset(esp_lcd_panel_t *panel);
static esp_err_t panel_jd9165_invert_color(esp_lcd_panel_t *panel, bool invert_color_data);
static esp_err_t panel_jd9165_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y);
static esp_err_t panel_jd9165_disp_on_off(esp_lcd_panel_t *panel, bool on_off);
esp_err_t esp_lcd_new_panel_jd9165(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config,
esp_lcd_panel_handle_t *ret_panel)
{
ESP_RETURN_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, TAG, "invalid arguments");
jd9165_vendor_config_t *vendor_config = (jd9165_vendor_config_t *)panel_dev_config->vendor_config;
ESP_RETURN_ON_FALSE(vendor_config && vendor_config->mipi_config.dpi_config && vendor_config->mipi_config.dsi_bus, ESP_ERR_INVALID_ARG, TAG,
"invalid vendor config");
esp_err_t ret = ESP_OK;
jd9165_panel_t *jd9165 = (jd9165_panel_t *)calloc(1, sizeof(jd9165_panel_t));
ESP_RETURN_ON_FALSE(jd9165, ESP_ERR_NO_MEM, TAG, "no mem for jd9165 panel");
if (panel_dev_config->reset_gpio_num >= 0) {
gpio_config_t io_conf = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num,
};
ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed");
}
switch (panel_dev_config->color_space) {
case LCD_RGB_ELEMENT_ORDER_RGB:
jd9165->madctl_val = 0;
break;
case LCD_RGB_ELEMENT_ORDER_BGR:
jd9165->madctl_val |= LCD_CMD_BGR_BIT;
break;
default:
ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space");
break;
}
jd9165->io = io;
jd9165->init_cmds = vendor_config->init_cmds;
jd9165->init_cmds_size = vendor_config->init_cmds_size;
jd9165->reset_gpio_num = panel_dev_config->reset_gpio_num;
jd9165->flags.reset_level = panel_dev_config->flags.reset_active_high;
// Create MIPI DPI panel
esp_lcd_panel_handle_t panel_handle = NULL;
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_dpi(vendor_config->mipi_config.dsi_bus, vendor_config->mipi_config.dpi_config, &panel_handle), err, TAG,
"create MIPI DPI panel failed");
ESP_LOGD(TAG, "new MIPI DPI panel @%p", panel_handle);
// Save the original functions of MIPI DPI panel
jd9165->del = panel_handle->del;
jd9165->init = panel_handle->init;
// Overwrite the functions of MIPI DPI panel
panel_handle->del = panel_jd9165_del;
panel_handle->init = panel_jd9165_init;
panel_handle->reset = panel_jd9165_reset;
panel_handle->mirror = panel_jd9165_mirror;
panel_handle->invert_color = panel_jd9165_invert_color;
panel_handle->disp_on_off = panel_jd9165_disp_on_off;
panel_handle->user_data = jd9165;
*ret_panel = panel_handle;
ESP_LOGD(TAG, "new jd9165 panel @%p", jd9165);
return ESP_OK;
err:
if (jd9165) {
if (panel_dev_config->reset_gpio_num >= 0) {
gpio_reset_pin(panel_dev_config->reset_gpio_num);
}
free(jd9165);
}
return ret;
}
static const jd9165_lcd_init_cmd_t vendor_specific_init_default[] = {
// {cmd, { data }, data_size, delay_ms}
//{0x11, (uint8_t []){0x00}, 1, 120},
//{0x29, (uint8_t []){0x00}, 1, 20},
{0x30, (uint8_t[]){0x00}, 1, 0},
{0xF7, (uint8_t[]){0x49,0x61,0x02,0x00}, 4, 0},
{0x30, (uint8_t[]){0x01}, 1, 0},
{0x04, (uint8_t[]){0x0C}, 1, 0},
{0x05, (uint8_t[]){0x00}, 1, 0},
{0x06, (uint8_t[]){0x00}, 1, 0},
{0x0B, (uint8_t[]){0x11}, 1, 0},
{0x17, (uint8_t[]){0x00}, 1, 0},
{0x20, (uint8_t[]){0x04}, 1, 0},
{0x1F, (uint8_t[]){0x05}, 1, 0},
{0x23, (uint8_t[]){0x00}, 1, 0},
{0x25, (uint8_t[]){0x19}, 1, 0},
{0x28, (uint8_t[]){0x18}, 1, 0},
{0x29, (uint8_t[]){0x04}, 1, 0},
{0x2A, (uint8_t[]){0x01}, 1, 0},
{0x2B, (uint8_t[]){0x04}, 1, 0},
{0x2C, (uint8_t[]){0x01}, 1, 0},
{0x30, (uint8_t[]){0x02}, 1, 0},
{0x01, (uint8_t[]){0x22}, 1, 0},
{0x03, (uint8_t[]){0x12}, 1, 0},
{0x04, (uint8_t[]){0x00}, 1, 0},
{0x05, (uint8_t[]){0x64}, 1, 0},
{0x0A, (uint8_t[]){0x08}, 1, 0},
{0x0B, (uint8_t[]){0x0A,0x1A,0x0B,0x0D,0x0D,0x11,0x10,0x06,0x08,0x1F,0x1D}, 11, 0},
{0x0C, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x0D, (uint8_t[]){0x16,0x1B,0x0B,0x0D,0x0D,0x11,0x10,0x07,0x09,0x1E,0x1C}, 11, 0},
{0x0E, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x0F, (uint8_t[]){0x16,0x1B,0x0D,0x0B,0x0D,0x11,0x10,0x1C,0x1E,0x09,0x07}, 11, 0},
{0x10, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x11, (uint8_t[]){0x0A,0x1A,0x0D,0x0B,0x0D,0x11,0x10,0x1D,0x1F,0x08,0x06}, 11, 0},
{0x12, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x14, (uint8_t[]){0x00,0x00,0x11,0x11}, 4, 0},
{0x18, (uint8_t[]){0x99}, 1, 0},
{0x30, (uint8_t[]){0x06}, 1, 0},
{0x12, (uint8_t[]){0x36,0x2C,0x2E,0x3C,0x38,0x35,0x35,0x32,0x2E,0x1D,0x2B,0x21,0x16,0x29}, 14, 0},
{0x13, (uint8_t[]){0x36,0x2C,0x2E,0x3C,0x38,0x35,0x35,0x32,0x2E,0x1D,0x2B,0x21,0x16,0x29}, 14, 0},
// {0x30, (uint8_t[]){0x08}, 1, 0},
// {0x05, (uint8_t[]){0x01}, 1, 0},
// {0x0C, (uint8_t[]){0x1A}, 1, 0},
// {0x0D, (uint8_t[]){0x0E}, 1, 0},
// {0x30, (uint8_t[]){0x07}, 1, 0},
// {0x01, (uint8_t[]){0x04}, 1, 0},
{0x30, (uint8_t[]){0x0A}, 1, 0},
{0x02, (uint8_t[]){0x4F}, 1, 0},
{0x0B, (uint8_t[]){0x40}, 1, 0},
{0x12, (uint8_t[]){0x3E}, 1, 0},
{0x13, (uint8_t[]){0x78}, 1, 0},
{0x30, (uint8_t[]){0x0D}, 1, 0},
{0x0D, (uint8_t[]){0x04}, 1, 0},
{0x10, (uint8_t[]){0x0C}, 1, 0},
{0x11, (uint8_t[]){0x0C}, 1, 0},
{0x12, (uint8_t[]){0x0C}, 1, 0},
{0x13, (uint8_t[]){0x0C}, 1, 0},
{0x30, (uint8_t[]){0x00}, 1, 0},
// {0X3A, (uint8_t[]){0x55}, 1, 0},
{0x11, (uint8_t[]){0x00}, 1, 120},
{0x29, (uint8_t[]){0x00}, 1, 50},
};
static esp_err_t panel_jd9165_del(esp_lcd_panel_t *panel)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
if (jd9165->reset_gpio_num >= 0) {
gpio_reset_pin(jd9165->reset_gpio_num);
}
// Delete MIPI DPI panel
jd9165->del(panel);
ESP_LOGD(TAG, "del jd9165 panel @%p", jd9165);
free(jd9165);
return ESP_OK;
}
static esp_err_t panel_jd9165_init(esp_lcd_panel_t *panel)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
const jd9165_lcd_init_cmd_t *init_cmds = NULL;
uint16_t init_cmds_size = 0;
bool is_cmd_overwritten = false;
uint8_t ID[3];
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_rx_param(io, 0x04, ID, 3), TAG, "read ID failed");
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) {
jd9165->madctl_val,
}, 1), TAG, "send command failed");
// vendor specific initialization, it can be different between manufacturers
// should consult the LCD supplier for initialization sequence code
if (jd9165->init_cmds) {
init_cmds = jd9165->init_cmds;
init_cmds_size = jd9165->init_cmds_size;
} else {
init_cmds = vendor_specific_init_default;
init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(jd9165_lcd_init_cmd_t);
}
for (int i = 0; i < init_cmds_size; i++) {
// Check if the command has been used or conflicts with the internal
if (init_cmds[i].data_bytes > 0) {
switch (init_cmds[i].cmd) {
case LCD_CMD_MADCTL:
is_cmd_overwritten = true;
jd9165->madctl_val = ((uint8_t *)init_cmds[i].data)[0];
break;
default:
is_cmd_overwritten = false;
break;
}
if (is_cmd_overwritten) {
is_cmd_overwritten = false;
ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence",
init_cmds[i].cmd);
}
}
// Send command
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed");
vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms));
}
ESP_LOGD(TAG, "send init commands success");
ESP_RETURN_ON_ERROR(jd9165->init(panel), TAG, "init MIPI DPI panel failed");
return ESP_OK;
}
static esp_err_t panel_jd9165_reset(esp_lcd_panel_t *panel)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
// Perform hardware reset
if (jd9165->reset_gpio_num >= 0) {
gpio_set_level(jd9165->reset_gpio_num, !jd9165->flags.reset_level);
vTaskDelay(pdMS_TO_TICKS(5));
gpio_set_level(jd9165->reset_gpio_num, jd9165->flags.reset_level);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(jd9165->reset_gpio_num, !jd9165->flags.reset_level);
vTaskDelay(pdMS_TO_TICKS(120));
} else if (io) { // Perform software reset
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed");
vTaskDelay(pdMS_TO_TICKS(120));
}
return ESP_OK;
}
static esp_err_t panel_jd9165_invert_color(esp_lcd_panel_t *panel, bool invert_color_data)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
uint8_t command = 0;
ESP_RETURN_ON_FALSE(io, ESP_ERR_INVALID_STATE, TAG, "invalid panel IO");
if (invert_color_data) {
command = LCD_CMD_INVON;
} else {
command = LCD_CMD_INVOFF;
}
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed");
return ESP_OK;
}
static esp_err_t panel_jd9165_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
uint8_t madctl_val = jd9165->madctl_val;
ESP_RETURN_ON_FALSE(io, ESP_ERR_INVALID_STATE, TAG, "invalid panel IO");
// Control mirror through LCD command
if (mirror_x) {
madctl_val |= JD9165_CMD_GS_BIT;
} else {
madctl_val &= ~JD9165_CMD_GS_BIT;
}
if (mirror_y) {
madctl_val |= JD9165_CMD_SS_BIT;
} else {
madctl_val &= ~JD9165_CMD_SS_BIT;
}
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t []) {
madctl_val
}, 1), TAG, "send command failed");
jd9165->madctl_val = madctl_val;
return ESP_OK;
}
static esp_err_t panel_jd9165_disp_on_off(esp_lcd_panel_t *panel, bool on_off)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
int command = 0;
if (on_off) {
command = LCD_CMD_DISPON;
} else {
command = LCD_CMD_DISPOFF;
}
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed");
return ESP_OK;
}
#endif
@@ -0,0 +1,125 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "soc/soc_caps.h"
#if SOC_MIPI_DSI_SUPPORTED
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_mipi_dsi.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief LCD panel initialization commands.
*
*/
typedef struct {
int cmd; /*<! The specific LCD command */
const void *data; /*<! Buffer that holds the command specific data */
size_t data_bytes; /*<! Size of `data` in memory, in bytes */
unsigned int delay_ms; /*<! Delay in milliseconds after this command */
} jd9165_lcd_init_cmd_t;
/**
* @brief LCD panel vendor configuration.
*
* @note This structure needs to be passed to the `vendor_config` field in `esp_lcd_panel_dev_config_t`.
*
*/
typedef struct {
const jd9165_lcd_init_cmd_t *init_cmds; /*!< Pointer to initialization commands array. Set to NULL if using default commands.
* The array should be declared as `static const` and positioned outside the function.
* Please refer to `vendor_specific_init_default` in source file.
*/
uint16_t init_cmds_size; /*<! Number of commands in above array */
struct {
esp_lcd_dsi_bus_handle_t dsi_bus; /*!< MIPI-DSI bus configuration */
const esp_lcd_dpi_panel_config_t *dpi_config; /*!< MIPI-DPI panel configuration */
} mipi_config;
} jd9165_vendor_config_t;
/**
* @brief Create LCD panel for model JD9165
*
* @note Vendor specific initialization can be different between manufacturers, should consult the LCD supplier for initialization sequence code.
*
* @param[in] io LCD panel IO handle
* @param[in] panel_dev_config General panel device configuration
* @param[out] ret_panel Returned LCD panel handle
* @return
* - ESP_ERR_INVALID_ARG if parameter is invalid
* - ESP_OK on success
* - Otherwise on fail
*/
esp_err_t esp_lcd_new_panel_jd9165(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config,
esp_lcd_panel_handle_t *ret_panel);
/**
* @brief MIPI-DSI bus configuration structure
*
* @param[in] lane_num Number of data lanes
* @param[in] lane_mbps Lane bit rate in Mbps
*
*/
#define JD9165_PANEL_BUS_DSI_2CH_CONFIG() \
{ \
.bus_id = 0, \
.num_data_lanes = 2, \
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, \
.lane_bit_rate_mbps = 750, \
}
/**
* @brief MIPI-DBI panel IO configuration structure
*
*/
#define JD9165_PANEL_IO_DBI_CONFIG() \
{ \
.virtual_channel = 0, \
.lcd_cmd_bits = 8, \
.lcd_param_bits = 8, \
}
/**
* @brief MIPI DPI configuration structure
*
* @note refresh_rate = (dpi_clock_freq_mhz * 1000000) / (h_res + hsync_pulse_width + hsync_back_porch + hsync_front_porch)
* / (v_res + vsync_pulse_width + vsync_back_porch + vsync_front_porch)
*
* @param[in] px_format Pixel format of the panel
*
*/
#define JD9165_1024_600_PANEL_60HZ_DPI_CONFIG(px_format) \
{ \
.virtual_channel = 0, \
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, \
.dpi_clock_freq_mhz = 56, \
.pixel_format = px_format, \
.num_fbs = 1, \
.video_timing = { \
.h_size = 1024, \
.v_size = 600, \
.hsync_pulse_width = 40, \
.hsync_back_porch = 160, \
.hsync_front_porch = 160, \
.vsync_pulse_width = 10, \
.vsync_back_porch = 23, \
.vsync_front_porch = 12, \
}, \
.flags= { \
.use_dma2d = true, \
}, \
}
#endif
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,153 @@
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_timer.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_mipi_dsi.h"
#include "esp_lcd_panel_io.h"
#include "esp_ldo_regulator.h"
#include "driver/gpio.h"
#include "esp_err.h"
#include "esp_log.h"
#include "Arduino.h"
#include "esp_lcd_jd9165.h"
#include "jd9165_lcd.h"
#define LCD_H_RES 1024
#define LCD_V_RES 600
#define MIPI_DPI_PX_FORMAT (LCD_COLOR_PIXEL_FORMAT_RGB565)
#define LCD_BIT_PER_PIXEL (16)
// “VDD_MIPI_DPHY”应供电 2.5V,可从内部 LDO 稳压器或外部 LDO 芯片获取电源
#define EXAMPLE_MIPI_DSI_PHY_PWR_LDO_CHAN 3 // LDO_VO3 连接至 VDD_MIPI_DPHY
#define EXAMPLE_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV 2500
#define EXAMPLE_LCD_BK_LIGHT_ON_LEVEL 1
#define EXAMPLE_LCD_BK_LIGHT_OFF_LEVEL !EXAMPLE_LCD_BK_LIGHT_ON_LEVEL
#define EXAMPLE_PIN_NUM_BK_LIGHT GPIO_NUM_23
static const char *TAG = "example";
esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_io_handle_t io_handle = NULL;
jd9165_lcd::jd9165_lcd(int8_t lcd_rst)
{
_lcd_rst = lcd_rst;
}
void jd9165_lcd::example_bsp_enable_dsi_phy_power()
{
// 打开 MIPI DSI PHY 的电源,使其从“无电”状态进入“关机”状态
esp_ldo_channel_handle_t ldo_mipi_phy = NULL;
#ifdef EXAMPLE_MIPI_DSI_PHY_PWR_LDO_CHAN
esp_ldo_channel_config_t ldo_mipi_phy_config = {
.chan_id = EXAMPLE_MIPI_DSI_PHY_PWR_LDO_CHAN,
.voltage_mv = EXAMPLE_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV,
};
ESP_ERROR_CHECK(esp_ldo_acquire_channel(&ldo_mipi_phy_config, &ldo_mipi_phy));
ESP_LOGI(TAG, "MIPI DSI PHY Powered on");
#endif
}
void jd9165_lcd::example_bsp_init_lcd_backlight()
{
#if EXAMPLE_PIN_NUM_BK_LIGHT >= 0
gpio_config_t bk_gpio_config = {
.pin_bit_mask = 1ULL << EXAMPLE_PIN_NUM_BK_LIGHT,
.mode = GPIO_MODE_OUTPUT
};
ESP_ERROR_CHECK(gpio_config(&bk_gpio_config));
#endif
}
void jd9165_lcd::example_bsp_set_lcd_backlight(uint32_t level)
{
#if EXAMPLE_PIN_NUM_BK_LIGHT >= 0
gpio_set_level(EXAMPLE_PIN_NUM_BK_LIGHT, level);
#endif
}
void jd9165_lcd::begin()
{
example_bsp_enable_dsi_phy_power();
example_bsp_init_lcd_backlight();
example_bsp_set_lcd_backlight(EXAMPLE_LCD_BK_LIGHT_OFF_LEVEL);
// 首先创建 MIPI DSI 总线,它还将初始化 DSI PHY
esp_lcd_dsi_bus_handle_t mipi_dsi_bus;
esp_lcd_dsi_bus_config_t bus_config = JD9165_PANEL_BUS_DSI_2CH_CONFIG();
ESP_ERROR_CHECK(esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus));
ESP_LOGI(TAG, "Install MIPI DSI LCD control panel");
// 我们使用DBI接口发送LCD命令和参数
esp_lcd_dbi_io_config_t dbi_config = JD9165_PANEL_IO_DBI_CONFIG();
ESP_ERROR_CHECK(esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &io_handle));
// 创建JD9165控制面板
esp_lcd_dpi_panel_config_t dpi_config = JD9165_1024_600_PANEL_60HZ_DPI_CONFIG(MIPI_DPI_PX_FORMAT);
jd9165_vendor_config_t vendor_config = {
.mipi_config = {
.dsi_bus = mipi_dsi_bus,
.dpi_config = &dpi_config,
},
};
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = _lcd_rst,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = LCD_BIT_PER_PIXEL,
.vendor_config = &vendor_config,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_jd9165(io_handle, &panel_config, &panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
// 打开背光
example_bsp_set_lcd_backlight(EXAMPLE_LCD_BK_LIGHT_ON_LEVEL);
}
void jd9165_lcd::lcd_draw_bitmap(uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end, uint16_t *color_data)
{
esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, color_data);
}
void jd9165_lcd::draw16bitbergbbitmap(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t *color_data)
{
uint16_t x_start = x;
uint16_t y_start = y;
uint16_t x_end = w + x;
uint16_t y_end = h + y;
esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, color_data);
}
void jd9165_lcd::fillScreen(uint16_t color)
{
uint16_t *color_data = (uint16_t *)heap_caps_malloc(480 * 272 * 2, MALLOC_CAP_INTERNAL);
memset(color_data, color, 480 * 272 * 2);
draw16bitbergbbitmap(0, 0, 480, 272, color_data);
free(color_data);
}
void jd9165_lcd::te_on()
{
esp_lcd_panel_io_tx_param(io_handle, 0x35,new (uint8_t[]){0x00}, 1);
}
void jd9165_lcd::te_off()
{
esp_lcd_panel_io_tx_param(io_handle, 0x34,new (uint8_t[]){0x00}, 0);
}
uint16_t jd9165_lcd::width()
{
return LCD_H_RES;
}
uint16_t jd9165_lcd::height()
{
return LCD_V_RES;
}
@@ -0,0 +1,26 @@
#ifndef _JD9165_LCD_H
#define _JD9165_LCD_H
#include <stdio.h>
class jd9165_lcd
{
public:
jd9165_lcd(int8_t lcd_rst);
void begin();
void example_bsp_enable_dsi_phy_power();
void example_bsp_init_lcd_backlight();
void example_bsp_set_lcd_backlight(uint32_t level);
void lcd_draw_bitmap(uint16_t x_start, uint16_t y_start,
uint16_t x_end, uint16_t y_end, uint16_t *color_data);
void draw16bitbergbbitmap(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t *color_data);
void fillScreen(uint16_t color);
void te_on();
void te_off();
uint16_t width();
uint16_t height();
private:
int8_t _lcd_rst;
};
#endif
@@ -0,0 +1,236 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_system.h"
#include "esp_err.h"
#include "esp_check.h"
#include "esp_log.h"
#include "esp_lcd_touch.h"
static const char *TAG = "TP";
/*******************************************************************************
* Function definitions
*******************************************************************************/
/*******************************************************************************
* Local variables
*******************************************************************************/
/*******************************************************************************
* Public API functions
*******************************************************************************/
esp_err_t esp_lcd_touch_read_data(esp_lcd_touch_handle_t tp)
{
assert(tp != NULL);
assert(tp->read_data != NULL);
return tp->read_data(tp);
}
bool esp_lcd_touch_get_coordinates(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num)
{
bool touched = false;
assert(tp != NULL);
assert(x != NULL);
assert(y != NULL);
assert(tp->get_xy != NULL);
touched = tp->get_xy(tp, x, y, strength, point_num, max_point_num);
if (!touched) {
return false;
}
/* Process coordinates by user */
if (tp->config.process_coordinates != NULL) {
tp->config.process_coordinates(tp, x, y, strength, point_num, max_point_num);
}
/* Software coordinates adjustment needed */
bool sw_adj_needed = ((tp->config.flags.mirror_x && (tp->set_mirror_x == NULL)) ||
(tp->config.flags.mirror_y && (tp->set_mirror_y == NULL)) ||
(tp->config.flags.swap_xy && (tp->set_swap_xy == NULL)));
/* Adjust all coordinates */
for (int i = 0; (sw_adj_needed && i < *point_num); i++) {
/* Mirror X coordinates (if not supported by HW) */
if (tp->config.flags.mirror_x && tp->set_mirror_x == NULL) {
x[i] = tp->config.x_max - x[i];
}
/* Mirror Y coordinates (if not supported by HW) */
if (tp->config.flags.mirror_y && tp->set_mirror_y == NULL) {
y[i] = tp->config.y_max - y[i];
}
/* Swap X and Y coordinates (if not supported by HW) */
if (tp->config.flags.swap_xy && tp->set_swap_xy == NULL) {
uint16_t tmp = x[i];
x[i] = y[i];
y[i] = tmp;
}
}
return touched;
}
#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0)
esp_err_t esp_lcd_touch_get_button_state(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state)
{
assert(tp != NULL);
assert(state != NULL);
*state = 0;
if (tp->get_button_state) {
return tp->get_button_state(tp, n, state);
} else {
return ESP_ERR_NOT_SUPPORTED;
}
return ESP_OK;
}
#endif
esp_err_t esp_lcd_touch_set_swap_xy(esp_lcd_touch_handle_t tp, bool swap)
{
assert(tp != NULL);
tp->config.flags.swap_xy = swap;
/* Is swap supported by HW? */
if (tp->set_swap_xy) {
return tp->set_swap_xy(tp, swap);
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_get_swap_xy(esp_lcd_touch_handle_t tp, bool *swap)
{
assert(tp != NULL);
assert(swap != NULL);
/* Is swap supported by HW? */
if (tp->get_swap_xy) {
return tp->get_swap_xy(tp, swap);
} else {
*swap = tp->config.flags.swap_xy;
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_set_mirror_x(esp_lcd_touch_handle_t tp, bool mirror)
{
assert(tp != NULL);
tp->config.flags.mirror_x = mirror;
/* Is mirror supported by HW? */
if (tp->set_mirror_x) {
return tp->set_mirror_x(tp, mirror);
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_get_mirror_x(esp_lcd_touch_handle_t tp, bool *mirror)
{
assert(tp != NULL);
assert(mirror != NULL);
/* Is swap supported by HW? */
if (tp->get_mirror_x) {
return tp->get_mirror_x(tp, mirror);
} else {
*mirror = tp->config.flags.mirror_x;
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_set_mirror_y(esp_lcd_touch_handle_t tp, bool mirror)
{
assert(tp != NULL);
tp->config.flags.mirror_y = mirror;
/* Is mirror supported by HW? */
if (tp->set_mirror_y) {
return tp->set_mirror_y(tp, mirror);
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_get_mirror_y(esp_lcd_touch_handle_t tp, bool *mirror)
{
assert(tp != NULL);
assert(mirror != NULL);
/* Is swap supported by HW? */
if (tp->get_mirror_y) {
return tp->get_mirror_y(tp, mirror);
} else {
*mirror = tp->config.flags.mirror_y;
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_del(esp_lcd_touch_handle_t tp)
{
assert(tp != NULL);
if (tp->del != NULL) {
return tp->del(tp);
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_register_interrupt_callback(esp_lcd_touch_handle_t tp, esp_lcd_touch_interrupt_callback_t callback)
{
esp_err_t ret = ESP_OK;
assert(tp != NULL);
/* Interrupt pin is not selected */
if (tp->config.int_gpio_num == GPIO_NUM_NC) {
return ESP_ERR_INVALID_ARG;
}
tp->config.interrupt_callback = callback;
if (callback != NULL) {
ret = gpio_install_isr_service(0);
/* ISR service can be installed from user before, then it returns invalid state */
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "GPIO ISR install failed");
return ret;
}
/* Add GPIO ISR handler */
ret = gpio_intr_enable(tp->config.int_gpio_num);
ESP_RETURN_ON_ERROR(ret, TAG, "GPIO ISR install failed");
ret = gpio_isr_handler_add(tp->config.int_gpio_num, (gpio_isr_t)tp->config.interrupt_callback, tp);
ESP_RETURN_ON_ERROR(ret, TAG, "GPIO ISR install failed");
} else {
/* Remove GPIO ISR handler */
ret = gpio_isr_handler_remove(tp->config.int_gpio_num);
ESP_RETURN_ON_ERROR(ret, TAG, "GPIO ISR remove handler failed");
ret = gpio_intr_disable(tp->config.int_gpio_num);
ESP_RETURN_ON_ERROR(ret, TAG, "GPIO ISR disable failed");
}
return ESP_OK;
}
@@ -0,0 +1,370 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file
* @brief ESP LCD touch
*/
#pragma once
#include <stdbool.h>
#include "sdkconfig.h"
#include "esp_err.h"
#include "driver/gpio.h"
#include "esp_lcd_panel_io.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#ifdef __cplusplus
extern "C" {
#endif
#define CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS (1)
#define CONFIG_ESP_LCD_TOUCH_MAX_POINTS (5)
/**
* @brief Touch controller type
*
*/
typedef struct esp_lcd_touch_s esp_lcd_touch_t;
typedef esp_lcd_touch_t *esp_lcd_touch_handle_t;
/**
* @brief Touch controller interrupt callback type
*
*/
typedef void (*esp_lcd_touch_interrupt_callback_t)(esp_lcd_touch_handle_t tp);
/**
* @brief Touch Configuration Type
*
*/
typedef struct {
uint16_t x_max; /*!< X coordinates max (for mirroring) */
uint16_t y_max; /*!< Y coordinates max (for mirroring) */
gpio_num_t rst_gpio_num; /*!< GPIO number of reset pin */
gpio_num_t int_gpio_num; /*!< GPIO number of interrupt pin */
struct {
unsigned int reset: 1; /*!< Level of reset pin in reset */
unsigned int interrupt: 1;/*!< Active Level of interrupt pin */
} levels;
struct {
unsigned int swap_xy: 1; /*!< Swap X and Y after read coordinates */
unsigned int mirror_x: 1; /*!< Mirror X after read coordinates */
unsigned int mirror_y: 1; /*!< Mirror Y after read coordinates */
} flags;
/*!< User callback called after get coordinates from touch controller for apply user adjusting */
void (*process_coordinates)(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num);
/*!< User callback called after the touch interrupt occured */
esp_lcd_touch_interrupt_callback_t interrupt_callback;
} esp_lcd_touch_config_t;
typedef struct {
uint8_t points; /*!< Count of touch points saved */
struct {
uint16_t x; /*!< X coordinate */
uint16_t y; /*!< Y coordinate */
uint16_t strength; /*!< Strength */
} coords[CONFIG_ESP_LCD_TOUCH_MAX_POINTS];
#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0)
uint8_t buttons; /*!< Count of buttons states saved */
struct {
uint8_t status; /*!< Status of button */
} button[CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS];
#endif
portMUX_TYPE lock; /*!< Lock for read/write */
} esp_lcd_touch_data_t;
/**
* @brief Declare of Touch Type
*
*/
struct esp_lcd_touch_s {
/**
* @brief Read data from touch controller (mandatory)
*
* @note This function is usually blocking.
*
* @param tp: Touch handler
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*read_data)(esp_lcd_touch_handle_t tp);
/**
* @brief Get coordinates from touch controller (mandatory)
*
* @param tp: Touch handler
* @param x: Array of X coordinates
* @param y: Array of Y coordinates
* @param strength: Array of strengths
* @param point_num: Count of points touched (equals with count of items in x and y array)
* @param max_point_num: Maximum count of touched points to return (equals with max size of x and y array)
*
* @return
* - Returns true, when touched and coordinates readed. Otherwise returns false.
*/
bool (*get_xy)(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num);
#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0)
/**
* @brief Get button state (optional)
*
* @param tp: Touch handler
* @param n: Button index
* @param state: Button state
*
* @return
* - Returns true, when touched and coordinates readed. Otherwise returns false.
*/
esp_err_t (*get_button_state)(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state);
#endif
/**
* @brief Swap X and Y after read coordinates (optional)
* If set, then not used SW swapping.
*
* @param tp: Touch handler
* @param swap: Set swap value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*set_swap_xy)(esp_lcd_touch_handle_t tp, bool swap);
/**
* @brief Are X and Y coordinates swapped (optional)
*
* @param tp: Touch handler
* @param swap: Get swap value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*get_swap_xy)(esp_lcd_touch_handle_t tp, bool *swap);
/**
* @brief Mirror X after read coordinates
* If set, then not used SW mirroring.
*
* @param tp: Touch handler
* @param mirror: Set X mirror value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*set_mirror_x)(esp_lcd_touch_handle_t tp, bool mirror);
/**
* @brief Is mirrored X (optional)
*
* @param tp: Touch handler
* @param mirror: Get X mirror value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*get_mirror_x)(esp_lcd_touch_handle_t tp, bool *mirror);
/**
* @brief Mirror Y after read coordinates
* If set, then not used SW mirroring.
*
* @param tp: Touch handler
* @param mirror: Set Y mirror value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*set_mirror_y)(esp_lcd_touch_handle_t tp, bool mirror);
/**
* @brief Is mirrored Y (optional)
*
* @param tp: Touch handler
* @param mirror: Get Y mirror value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*get_mirror_y)(esp_lcd_touch_handle_t tp, bool *mirror);
/**
* @brief Delete Touch
*
* @param tp: Touch handler
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*del)(esp_lcd_touch_handle_t tp);
/**
* @brief Configuration structure
*/
esp_lcd_touch_config_t config;
/**
* @brief Communication interface
*/
esp_lcd_panel_io_handle_t io;
/**
* @brief Data structure
*/
esp_lcd_touch_data_t data;
};
/**
* @brief Read data from touch controller
*
* @note This function is usually blocking.
*
* @param tp: Touch handler
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG parameter error
* - ESP_FAIL sending command error, slave hasn't ACK the transfer
* - ESP_ERR_INVALID_STATE I2C driver not installed or not in master mode
* - ESP_ERR_TIMEOUT operation timeout because the bus is busy
*/
esp_err_t esp_lcd_touch_read_data(esp_lcd_touch_handle_t tp);
/**
* @brief Read coordinates from touch controller
*
* @param tp: Touch handler
* @param x: Array of X coordinates
* @param y: Array of Y coordinates
* @param strength: Array of the strengths (can be NULL)
* @param point_num: Count of points touched (equals with count of items in x and y array)
* @param max_point_num: Maximum count of touched points to return (equals with max size of x and y array)
*
* @return
* - Returns true, when touched and coordinates readed. Otherwise returns false.
*/
bool esp_lcd_touch_get_coordinates(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num);
#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0)
/**
* @brief Get button state
*
* @param tp: Touch handler
* @param n: Button index
* @param state: Button state
*
* @return
* - ESP_OK on success
* - ESP_ERR_NOT_SUPPORTED if this function is not supported by controller
* - ESP_ERR_INVALID_ARG if bad button index
*/
esp_err_t esp_lcd_touch_get_button_state(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state);
#endif
/**
* @brief Swap X and Y after read coordinates
*
* @param tp: Touch handler
* @param swap: Set swap value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_set_swap_xy(esp_lcd_touch_handle_t tp, bool swap);
/**
* @brief Are X and Y coordinates swapped
*
* @param tp: Touch handler
* @param swap: Get swap value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_get_swap_xy(esp_lcd_touch_handle_t tp, bool *swap);
/**
* @brief Mirror X after read coordinates
*
* @param tp: Touch handler
* @param mirror: Set X mirror value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_set_mirror_x(esp_lcd_touch_handle_t tp, bool mirror);
/**
* @brief Is mirrored X
*
* @param tp: Touch handler
* @param mirror: Get X mirror value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_get_mirror_x(esp_lcd_touch_handle_t tp, bool *mirror);
/**
* @brief Mirror Y after read coordinates
*
* @param tp: Touch handler
* @param mirror: Set Y mirror value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_set_mirror_y(esp_lcd_touch_handle_t tp, bool mirror);
/**
* @brief Is mirrored Y
*
* @param tp: Touch handler
* @param mirror: Get Y mirror value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_get_mirror_y(esp_lcd_touch_handle_t tp, bool *mirror);
/**
* @brief Delete touch (free all allocated memory and restart HW)
*
* @param tp: Touch handler
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_del(esp_lcd_touch_handle_t tp);
/**
* @brief Register user callback called after the touch interrupt occured
*
* @param tp: Touch handler
* @param callback: Interrupt callback
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_register_interrupt_callback(esp_lcd_touch_handle_t tp, esp_lcd_touch_interrupt_callback_t callback);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,284 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_check.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_touch.h"
static const char *TAG = "GT911";
/* GT911 registers */
#define ESP_LCD_TOUCH_GT911_READ_XY_REG (0x814E)
#define ESP_LCD_TOUCH_GT911_CONFIG_REG (0x8047)
#define ESP_LCD_TOUCH_GT911_PRODUCT_ID_REG (0x8140)
/*******************************************************************************
* Function definitions
*******************************************************************************/
static esp_err_t esp_lcd_touch_gt911_read_data(esp_lcd_touch_handle_t tp);
static bool esp_lcd_touch_gt911_get_xy(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num);
static esp_err_t esp_lcd_touch_gt911_del(esp_lcd_touch_handle_t tp);
/* I2C read/write */
static esp_err_t touch_gt911_i2c_read(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t *data, uint8_t len);
static esp_err_t touch_gt911_i2c_write(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t data);
/* GT911 reset */
static esp_err_t touch_gt911_reset(esp_lcd_touch_handle_t tp);
/* Read status and config register */
static esp_err_t touch_gt911_read_cfg(esp_lcd_touch_handle_t tp);
static void touch_scale(uint16_t *x_coordinate,uint16_t *y_coordinate);
/*******************************************************************************
* Public API functions
*******************************************************************************/
esp_err_t esp_lcd_touch_new_i2c_gt911(const esp_lcd_panel_io_handle_t io, const esp_lcd_touch_config_t *config, esp_lcd_touch_handle_t *out_touch)
{
esp_err_t ret = ESP_OK;
assert(io != NULL);
assert(config != NULL);
assert(out_touch != NULL);
/* Prepare main structure */
esp_lcd_touch_handle_t esp_lcd_touch_gt911 = heap_caps_calloc(1, sizeof(esp_lcd_touch_t), MALLOC_CAP_DEFAULT);
ESP_GOTO_ON_FALSE(esp_lcd_touch_gt911, ESP_ERR_NO_MEM, err, TAG, "no mem for GT911 controller");
/* Communication interface */
esp_lcd_touch_gt911->io = io;
/* Only supported callbacks are set */
esp_lcd_touch_gt911->read_data = esp_lcd_touch_gt911_read_data;
esp_lcd_touch_gt911->get_xy = esp_lcd_touch_gt911_get_xy;
esp_lcd_touch_gt911->del = esp_lcd_touch_gt911_del;
/* Mutex */
esp_lcd_touch_gt911->data.lock.owner = portMUX_FREE_VAL;
/* Save config */
memcpy(&esp_lcd_touch_gt911->config, config, sizeof(esp_lcd_touch_config_t));
/* Prepare pin for touch interrupt */
if (esp_lcd_touch_gt911->config.int_gpio_num != GPIO_NUM_NC) {
const gpio_config_t int_gpio_config = {
.mode = GPIO_MODE_INPUT,
.intr_type = GPIO_INTR_NEGEDGE,
.pin_bit_mask = BIT64(esp_lcd_touch_gt911->config.int_gpio_num)
};
ret = gpio_config(&int_gpio_config);
ESP_GOTO_ON_ERROR(ret, err, TAG, "GPIO config failed");
/* Register interrupt callback */
if (esp_lcd_touch_gt911->config.interrupt_callback) {
esp_lcd_touch_register_interrupt_callback(esp_lcd_touch_gt911, esp_lcd_touch_gt911->config.interrupt_callback);
}
}
/* Prepare pin for touch controller reset */
if (esp_lcd_touch_gt911->config.rst_gpio_num != GPIO_NUM_NC) {
const gpio_config_t rst_gpio_config = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = BIT64(esp_lcd_touch_gt911->config.rst_gpio_num)
};
ret = gpio_config(&rst_gpio_config);
ESP_GOTO_ON_ERROR(ret, err, TAG, "GPIO config failed");
}
/* Reset controller */
ret = touch_gt911_reset(esp_lcd_touch_gt911);
ESP_GOTO_ON_ERROR(ret, err, TAG, "GT911 reset failed");
/* Read status and config info */
ret = touch_gt911_read_cfg(esp_lcd_touch_gt911);
ESP_GOTO_ON_ERROR(ret, err, TAG, "GT911 init failed");
err:
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (0x%x)! Touch controller GT911 initialization failed!", ret);
if (esp_lcd_touch_gt911) {
esp_lcd_touch_gt911_del(esp_lcd_touch_gt911);
}
}
*out_touch = esp_lcd_touch_gt911;
return ret;
}
static esp_err_t esp_lcd_touch_gt911_read_data(esp_lcd_touch_handle_t tp)
{
esp_err_t err;
uint8_t buf[41];
uint8_t touch_cnt = 0;
uint8_t clear = 0;
size_t i = 0;
assert(tp != NULL);
err = touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, buf, 1);
ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!");
/* Any touch data? */
if ((buf[0] & 0x80) == 0x00) {
touch_gt911_i2c_write(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, clear);
} else {
/* Count of touched points */
touch_cnt = buf[0] & 0x0f;
if (touch_cnt > 5 || touch_cnt == 0) {
touch_gt911_i2c_write(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, clear);
return ESP_OK;
}
/* Read all points */
err = touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG + 1, &buf[1], touch_cnt * 8);
ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!");
/* Clear all */
err = touch_gt911_i2c_write(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, clear);
ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!");
portENTER_CRITICAL(&tp->data.lock);
/* Number of touched points */
touch_cnt = (touch_cnt > CONFIG_ESP_LCD_TOUCH_MAX_POINTS ? CONFIG_ESP_LCD_TOUCH_MAX_POINTS : touch_cnt);
tp->data.points = touch_cnt;
/* Fill all coordinates */
for (i = 0; i < touch_cnt; i++) {
tp->data.coords[i].x = ((uint16_t)buf[(i * 8) + 3] << 8) + buf[(i * 8) + 2];
tp->data.coords[i].y = (((uint16_t)buf[(i * 8) + 5] << 8) + buf[(i * 8) + 4]);
tp->data.coords[i].strength = (((uint16_t)buf[(i * 8) + 7] << 8) + buf[(i * 8) + 6]);
}
portEXIT_CRITICAL(&tp->data.lock);
}
return ESP_OK;
}
static bool esp_lcd_touch_gt911_get_xy(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num)
{
assert(tp != NULL);
assert(x != NULL);
assert(y != NULL);
assert(point_num != NULL);
assert(max_point_num > 0);
portENTER_CRITICAL(&tp->data.lock);
/* Count of points */
*point_num = (tp->data.points > max_point_num ? max_point_num : tp->data.points);
for (size_t i = 0; i < *point_num; i++) {
x[i] = tp->data.coords[i].x;
y[i] = tp->data.coords[i].y;
touch_scale(&x[i],&y[i]);
if (strength) {
strength[i] = tp->data.coords[i].strength;
}
}
/* Invalidate */
tp->data.points = 0;
portEXIT_CRITICAL(&tp->data.lock);
return (*point_num > 0);
}
static esp_err_t esp_lcd_touch_gt911_del(esp_lcd_touch_handle_t tp)
{
assert(tp != NULL);
/* Reset GPIO pin settings */
if (tp->config.int_gpio_num != GPIO_NUM_NC) {
gpio_reset_pin(tp->config.int_gpio_num);
}
/* Reset GPIO pin settings */
if (tp->config.rst_gpio_num != GPIO_NUM_NC) {
gpio_reset_pin(tp->config.rst_gpio_num);
}
free(tp);
return ESP_OK;
}
/*******************************************************************************
* Private API function
*******************************************************************************/
/* Reset controller */
static esp_err_t touch_gt911_reset(esp_lcd_touch_handle_t tp)
{
assert(tp != NULL);
if (tp->config.rst_gpio_num != GPIO_NUM_NC) {
ESP_RETURN_ON_ERROR(gpio_set_level(tp->config.rst_gpio_num, tp->config.levels.reset), TAG, "GPIO set level error!");
vTaskDelay(pdMS_TO_TICKS(10));
ESP_RETURN_ON_ERROR(gpio_set_level(tp->config.rst_gpio_num, !tp->config.levels.reset), TAG, "GPIO set level error!");
vTaskDelay(pdMS_TO_TICKS(10));
}
return ESP_OK;
}
static esp_err_t touch_gt911_read_cfg(esp_lcd_touch_handle_t tp)
{
uint8_t buf[4];
assert(tp != NULL);
ESP_RETURN_ON_ERROR(touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_PRODUCT_ID_REG, (uint8_t *)&buf[0], 3), TAG, "GT911 read error!");
ESP_RETURN_ON_ERROR(touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_CONFIG_REG, (uint8_t *)&buf[3], 1), TAG, "GT911 read error!");
ESP_LOGI(TAG, "TouchPad_ID:0x%02x,0x%02x,0x%02x", buf[0], buf[1], buf[2]);
ESP_LOGI(TAG, "TouchPad_Config_Version:%d", buf[3]);
return ESP_OK;
}
static esp_err_t touch_gt911_i2c_read(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t *data, uint8_t len)
{
assert(tp != NULL);
assert(data != NULL);
/* Read data */
return esp_lcd_panel_io_rx_param(tp->io, reg, data, len);
}
static esp_err_t touch_gt911_i2c_write(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t data)
{
assert(tp != NULL);
// *INDENT-OFF*
/* Write data */
return esp_lcd_panel_io_tx_param(tp->io, reg, (uint8_t[]){data}, 1);
// *INDENT-ON*
}
static void touch_scale(uint16_t *x_coordinate,uint16_t *y_coordinate)
{
uint32_t temp_x;
uint32_t temp_y;
temp_x = (*x_coordinate) * 1024 *10 / 800 / 10;
temp_y = (*y_coordinate) * 600 * 10 / 480 / 10;
*x_coordinate = (uint16_t)temp_x;
*y_coordinate = (uint16_t)temp_y;
}
@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file
* @brief ESP LCD touch: GT911
*/
#pragma once
#include "esp_lcd_touch.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Create a new GT911 touch driver
*
* @note The I2C communication should be initialized before use this function.
*
* @param io LCD/Touch panel IO handle
* @param config: Touch configuration
* @param out_touch: Touch instance handle
* @return
* - ESP_OK on success
* - ESP_ERR_NO_MEM if there is no memory for allocating main structure
*/
esp_err_t esp_lcd_touch_new_i2c_gt911(const esp_lcd_panel_io_handle_t io, const esp_lcd_touch_config_t *config, esp_lcd_touch_handle_t *out_touch);
/**
* @brief I2C address of the GT911 controller
*
*/
#define ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS (0x5D)
/**
* @brief Touch IO configuration structure
*
*/
#define ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG() \
{ \
.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS, \
.control_phase_bytes = 1, \
.dc_bit_offset = 0, \
.lcd_cmd_bits = 16, \
.flags = \
{ \
.disable_control_phase = 1, \
} \
}
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,99 @@
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_log.h"
#include "driver/i2c.h"
#include "esp_lcd_touch_gt911.h"
#include "gt911_touch.h"
#define CONFIG_LCD_HRES 600
#define CONFIG_LCD_VRES 1024
static const char *TAG = "example";
esp_lcd_touch_handle_t tp;
esp_lcd_panel_io_handle_t tp_io_handle;
uint16_t touch_strength[1];
uint8_t touch_cnt = 0;
gt911_touch::gt911_touch(int8_t sda_pin, int8_t scl_pin, int8_t rst_pin, int8_t int_pin)
{
_sda = sda_pin;
_scl = scl_pin;
_rst = rst_pin;
_int = int_pin;
}
void gt911_touch::begin()
{
i2c_config_t i2c_conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = (gpio_num_t)_sda,
.scl_io_num = (gpio_num_t)_scl,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
};
i2c_conf.master.clk_speed = 400000; // 400kHz
ESP_ERROR_CHECK(i2c_param_config(I2C_NUM_0, &i2c_conf));
ESP_ERROR_CHECK(i2c_driver_install(I2C_NUM_0, i2c_conf.mode, 0, 0, 0));
esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG();
ESP_LOGI(TAG, "Initialize touch IO (I2C)");
esp_lcd_new_panel_io_i2c((esp_lcd_i2c_bus_handle_t)I2C_NUM_0, &tp_io_config, &tp_io_handle);
esp_lcd_touch_config_t tp_cfg = {
.x_max = CONFIG_LCD_HRES,
.y_max = CONFIG_LCD_VRES,
.rst_gpio_num = (gpio_num_t)_rst,
.int_gpio_num = (gpio_num_t)_int,
.levels = {
.reset = 0,
.interrupt = 0,
},
.flags = {
.swap_xy = 0,
.mirror_x = 0,
.mirror_y = 0,
},
};
ESP_LOGI(TAG, "Initialize touch controller gt911");
ESP_ERROR_CHECK(esp_lcd_touch_new_i2c_gt911(tp_io_handle, &tp_cfg, &tp));
}
bool gt911_touch::getTouch(uint16_t *x, uint16_t *y)
{
esp_lcd_touch_read_data(tp);
bool touchpad_pressed = esp_lcd_touch_get_coordinates(tp, x, y, touch_strength, &touch_cnt, 1);
return touchpad_pressed;
}
void gt911_touch::set_rotation(uint8_t r){
switch(r){
case 0:
esp_lcd_touch_set_swap_xy(tp, false);
esp_lcd_touch_set_mirror_x(tp, false);
esp_lcd_touch_set_mirror_y(tp, false);
break;
case 1:
esp_lcd_touch_set_swap_xy(tp, false);
esp_lcd_touch_set_mirror_x(tp, true);
esp_lcd_touch_set_mirror_y(tp, true);
break;
case 2:
esp_lcd_touch_set_swap_xy(tp, false);
esp_lcd_touch_set_mirror_x(tp, false);
esp_lcd_touch_set_mirror_y(tp, false);
break;
case 3:
esp_lcd_touch_set_swap_xy(tp, false);
esp_lcd_touch_set_mirror_x(tp, true);
esp_lcd_touch_set_mirror_y(tp, true);
break;
}
}
@@ -0,0 +1,18 @@
#ifndef _GT911_TOUCH_H
#define _GT911_TOUCH_H
#include <stdio.h>
class gt911_touch
{
public:
gt911_touch(int8_t sda_pin, int8_t scl_pin, int8_t rst_pin = -1, int8_t int_pin = -1);
void begin();
bool getTouch(uint16_t *x, uint16_t *y);
void set_rotation(uint8_t r);
private:
int8_t _sda, _scl, _rst, _int;
};
#endif
@@ -0,0 +1,703 @@
/**
* @file lv_conf.h
* Configuration file for v8.3.0-dev
*/
/*
* Copy this file as `lv_conf.h`
* 1. simply next to the `lvgl` folder
* 2. or any other places and
* - define `LV_CONF_INCLUDE_SIMPLE`
* - add the path as include path
*/
/* clang-format off */
#if 1 /*Set it to "1" to enable content*/
#ifndef LV_CONF_H
#define LV_CONF_H
#include <stdint.h>
/*====================
COLOR SETTINGS
*====================*/
/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/
#define LV_COLOR_DEPTH 16
/*Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)*/
#define LV_COLOR_16_SWAP 0
/*Enable more complex drawing routines to manage screens transparency.
*Can be used if the UI is above another layer, e.g. an OSD menu or video player.
*Requires `LV_COLOR_DEPTH = 32` colors and the screen's `bg_opa` should be set to non LV_OPA_COVER value*/
#define LV_COLOR_SCREEN_TRANSP 0
/* Adjust color mix functions rounding. GPUs might calculate color mix (blending) differently.
* 0: round down, 64: round up from x.75, 128: round up from half, 192: round up from x.25, 254: round up */
#define LV_COLOR_MIX_ROUND_OFS (LV_COLOR_DEPTH == 32 ? 0: 128)
/*Images pixels with this color will not be drawn if they are chroma keyed)*/
#define LV_COLOR_CHROMA_KEY lv_color_hex(0x00ff00) /*pure green*/
/*=========================
MEMORY SETTINGS
*=========================*/
/*1: use custom malloc/free, 0: use the built-in `lv_mem_alloc()` and `lv_mem_free()`*/
#define LV_MEM_CUSTOM 1
#if LV_MEM_CUSTOM == 0
/*Size of the memory available for `lv_mem_alloc()` in bytes (>= 2kB)*/
#define LV_MEM_SIZE (40U * 1024U) /*[bytes]*/
/*Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too.*/
#define LV_MEM_ADR 0 /*0: unused*/
/*Instead of an address give a memory allocator that will be called to get a memory pool for LVGL. E.g. my_malloc*/
#if LV_MEM_ADR == 0
//#define LV_MEM_POOL_INCLUDE your_alloc_library /* Uncomment if using an external allocator*/
//#define LV_MEM_POOL_ALLOC your_alloc /* Uncomment if using an external allocator*/
#endif
#else /*LV_MEM_CUSTOM*/
#define LV_MEM_CUSTOM_INCLUDE <stdlib.h> /*Header for the dynamic memory function*/
#define LV_MEM_CUSTOM_ALLOC malloc
#define LV_MEM_CUSTOM_FREE free
#define LV_MEM_CUSTOM_REALLOC realloc
#endif /*LV_MEM_CUSTOM*/
/*Number of the intermediate memory buffer used during rendering and other internal processing mechanisms.
*You will see an error log message if there wasn't enough buffers. */
#define LV_MEM_BUF_MAX_NUM 16
/*Use the standard `memcpy` and `memset` instead of LVGL's own functions. (Might or might not be faster).*/
#define LV_MEMCPY_MEMSET_STD 1
/*====================
HAL SETTINGS
*====================*/
/*Default display refresh period. LVG will redraw changed areas with this period time*/
#define LV_DISP_DEF_REFR_PERIOD 10 /*[ms]*/
/*Input device read period in milliseconds*/
#define LV_INDEV_DEF_READ_PERIOD 10 /*[ms]*/
/*Use a custom tick source that tells the elapsed time in milliseconds.
*It removes the need to manually update the tick with `lv_tick_inc()`)*/
#define LV_TICK_CUSTOM 1
#if LV_TICK_CUSTOM
#define LV_TICK_CUSTOM_INCLUDE "Arduino.h" /*Header for the system time function*/
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (millis()) /*Expression evaluating to current system time in ms*/
#endif /*LV_TICK_CUSTOM*/
/*Default Dot Per Inch. Used to initialize default sizes such as widgets sized, style paddings.
*(Not so important, you can adjust it to modify default sizes and spaces)*/
#define LV_DPI_DEF 130 /*[px/inch]*/
/*=======================
* FEATURE CONFIGURATION
*=======================*/
/*-------------
* Drawing
*-----------*/
/*Enable complex draw engine.
*Required to draw shadow, gradient, rounded corners, circles, arc, skew lines, image transformations or any masks*/
#define LV_DRAW_COMPLEX 1
#if LV_DRAW_COMPLEX != 0
/*Allow buffering some shadow calculation.
*LV_SHADOW_CACHE_SIZE is the max. shadow size to buffer, where shadow size is `shadow_width + radius`
*Caching has LV_SHADOW_CACHE_SIZE^2 RAM cost*/
#define LV_SHADOW_CACHE_SIZE 0
/* Set number of maximally cached circle data.
* The circumference of 1/4 circle are saved for anti-aliasing
* radius * 4 bytes are used per circle (the most often used radiuses are saved)
* 0: to disable caching */
#define LV_CIRCLE_CACHE_SIZE 4
#endif /*LV_DRAW_COMPLEX*/
/*Default image cache size. Image caching keeps the images opened.
*If only the built-in image formats are used there is no real advantage of caching. (I.e. if no new image decoder is added)
*With complex image decoders (e.g. PNG or JPG) caching can save the continuous open/decode of images.
*However the opened images might consume additional RAM.
*0: to disable caching*/
#define LV_IMG_CACHE_DEF_SIZE 0
/*Number of stops allowed per gradient. Increase this to allow more stops.
*This adds (sizeof(lv_color_t) + 1) bytes per additional stop*/
#define LV_GRADIENT_MAX_STOPS 2
/*Default gradient buffer size.
*When LVGL calculates the gradient "maps" it can save them into a cache to avoid calculating them again.
*LV_GRAD_CACHE_DEF_SIZE sets the size of this cache in bytes.
*If the cache is too small the map will be allocated only while it's required for the drawing.
*0 mean no caching.*/
#define LV_GRAD_CACHE_DEF_SIZE 0
/*Allow dithering the gradients (to achieve visual smooth color gradients on limited color depth display)
*LV_DITHER_GRADIENT implies allocating one or two more lines of the object's rendering surface
*The increase in memory consumption is (32 bits * object width) plus 24 bits * object width if using error diffusion */
#define LV_DITHER_GRADIENT 0
#if LV_DITHER_GRADIENT
/*Add support for error diffusion dithering.
*Error diffusion dithering gets a much better visual result, but implies more CPU consumption and memory when drawing.
*The increase in memory consumption is (24 bits * object's width)*/
#define LV_DITHER_ERROR_DIFFUSION 0
#endif
/*Maximum buffer size to allocate for rotation.
*Only used if software rotation is enabled in the display driver.*/
#define LV_DISP_ROT_MAX_BUF (10*1024)
/*-------------
* GPU
*-----------*/
/*Use STM32's DMA2D (aka Chrom Art) GPU*/
#define LV_USE_GPU_STM32_DMA2D 0
#if LV_USE_GPU_STM32_DMA2D
/*Must be defined to include path of CMSIS header of target processor
e.g. "stm32f769xx.h" or "stm32f429xx.h"*/
#define LV_GPU_DMA2D_CMSIS_INCLUDE
#endif
/*Use NXP's PXP GPU iMX RTxxx platforms*/
#define LV_USE_GPU_NXP_PXP 0
#if LV_USE_GPU_NXP_PXP
/*1: Add default bare metal and FreeRTOS interrupt handling routines for PXP (lv_gpu_nxp_pxp_osa.c)
* and call lv_gpu_nxp_pxp_init() automatically during lv_init(). Note that symbol SDK_OS_FREE_RTOS
* has to be defined in order to use FreeRTOS OSA, otherwise bare-metal implementation is selected.
*0: lv_gpu_nxp_pxp_init() has to be called manually before lv_init()
*/
#define LV_USE_GPU_NXP_PXP_AUTO_INIT 0
#endif
/*Use NXP's VG-Lite GPU iMX RTxxx platforms*/
#define LV_USE_GPU_NXP_VG_LITE 0
/*Use SDL renderer API*/
#define LV_USE_GPU_SDL 0
#if LV_USE_GPU_SDL
#define LV_GPU_SDL_INCLUDE_PATH <SDL2/SDL.h>
/*Texture cache size, 8MB by default*/
#define LV_GPU_SDL_LRU_SIZE (1024 * 1024 * 8)
/*Custom blend mode for mask drawing, disable if you need to link with older SDL2 lib*/
#define LV_GPU_SDL_CUSTOM_BLEND_MODE (SDL_VERSION_ATLEAST(2, 0, 6))
#endif
/*-------------
* Logging
*-----------*/
/*Enable the log module*/
#define LV_USE_LOG 0
#if LV_USE_LOG
/*How important log should be added:
*LV_LOG_LEVEL_TRACE A lot of logs to give detailed information
*LV_LOG_LEVEL_INFO Log important events
*LV_LOG_LEVEL_WARN Log if something unwanted happened but didn't cause a problem
*LV_LOG_LEVEL_ERROR Only critical issue, when the system may fail
*LV_LOG_LEVEL_USER Only logs added by the user
*LV_LOG_LEVEL_NONE Do not log anything*/
#define LV_LOG_LEVEL LV_LOG_LEVEL_WARN
/*1: Print the log with 'printf';
*0: User need to register a callback with `lv_log_register_print_cb()`*/
#define LV_LOG_PRINTF 0
/*Enable/disable LV_LOG_TRACE in modules that produces a huge number of logs*/
#define LV_LOG_TRACE_MEM 1
#define LV_LOG_TRACE_TIMER 1
#define LV_LOG_TRACE_INDEV 1
#define LV_LOG_TRACE_DISP_REFR 1
#define LV_LOG_TRACE_EVENT 1
#define LV_LOG_TRACE_OBJ_CREATE 1
#define LV_LOG_TRACE_LAYOUT 1
#define LV_LOG_TRACE_ANIM 1
#endif /*LV_USE_LOG*/
/*-------------
* Asserts
*-----------*/
/*Enable asserts if an operation is failed or an invalid data is found.
*If LV_USE_LOG is enabled an error message will be printed on failure*/
#define LV_USE_ASSERT_NULL 1 /*Check if the parameter is NULL. (Very fast, recommended)*/
#define LV_USE_ASSERT_MALLOC 1 /*Checks is the memory is successfully allocated or no. (Very fast, recommended)*/
#define LV_USE_ASSERT_STYLE 0 /*Check if the styles are properly initialized. (Very fast, recommended)*/
#define LV_USE_ASSERT_MEM_INTEGRITY 0 /*Check the integrity of `lv_mem` after critical operations. (Slow)*/
#define LV_USE_ASSERT_OBJ 0 /*Check the object's type and existence (e.g. not deleted). (Slow)*/
/*Add a custom handler when assert happens e.g. to restart the MCU*/
#define LV_ASSERT_HANDLER_INCLUDE <stdint.h>
#define LV_ASSERT_HANDLER while(1); /*Halt by default*/
/*-------------
* Others
*-----------*/
/*1: Show CPU usage and FPS count*/
#define LV_USE_PERF_MONITOR 1
#if LV_USE_PERF_MONITOR
#define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT
#endif
/*1: Show the used memory and the memory fragmentation
* Requires LV_MEM_CUSTOM = 0*/
#define LV_USE_MEM_MONITOR 0
#if LV_USE_MEM_MONITOR
#define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT
#endif
/*1: Draw random colored rectangles over the redrawn areas*/
#define LV_USE_REFR_DEBUG 0
/*Change the built in (v)snprintf functions*/
#define LV_SPRINTF_CUSTOM 0
#if LV_SPRINTF_CUSTOM
#define LV_SPRINTF_INCLUDE <stdio.h>
#define lv_snprintf snprintf
#define lv_vsnprintf vsnprintf
#else /*LV_SPRINTF_CUSTOM*/
#define LV_SPRINTF_USE_FLOAT 0
#endif /*LV_SPRINTF_CUSTOM*/
#define LV_USE_USER_DATA 1
/*Garbage Collector settings
*Used if lvgl is bound to higher level language and the memory is managed by that language*/
#define LV_ENABLE_GC 0
#if LV_ENABLE_GC != 0
#define LV_GC_INCLUDE "gc.h" /*Include Garbage Collector related things*/
#endif /*LV_ENABLE_GC*/
/*=====================
* COMPILER SETTINGS
*====================*/
/*For big endian systems set to 1*/
#define LV_BIG_ENDIAN_SYSTEM 0
/*Define a custom attribute to `lv_tick_inc` function*/
#define LV_ATTRIBUTE_TICK_INC
/*Define a custom attribute to `lv_timer_handler` function*/
#define LV_ATTRIBUTE_TIMER_HANDLER
/*Define a custom attribute to `lv_disp_flush_ready` function*/
#define LV_ATTRIBUTE_FLUSH_READY
/*Required alignment size for buffers*/
#define LV_ATTRIBUTE_MEM_ALIGN_SIZE 1
/*Will be added where memories needs to be aligned (with -Os data might not be aligned to boundary by default).
* E.g. __attribute__((aligned(4)))*/
#define LV_ATTRIBUTE_MEM_ALIGN
/*Attribute to mark large constant arrays for example font's bitmaps*/
#define LV_ATTRIBUTE_LARGE_CONST
/*Compiler prefix for a big array declaration in RAM*/
#define LV_ATTRIBUTE_LARGE_RAM_ARRAY
/*Place performance critical functions into a faster memory (e.g RAM)*/
#define LV_ATTRIBUTE_FAST_MEM IRAM_ATTR
/*Prefix variables that are used in GPU accelerated operations, often these need to be placed in RAM sections that are DMA accessible*/
#define LV_ATTRIBUTE_DMA
/*Export integer constant to binding. This macro is used with constants in the form of LV_<CONST> that
*should also appear on LVGL binding API such as Micropython.*/
#define LV_EXPORT_CONST_INT(int_value) struct _silence_gcc_warning /*The default value just prevents GCC warning*/
/*Extend the default -32k..32k coordinate range to -4M..4M by using int32_t for coordinates instead of int16_t*/
#define LV_USE_LARGE_COORD 0
/*==================
* FONT USAGE
*===================*/
/*Montserrat fonts with ASCII range and some symbols using bpp = 4
*https://fonts.google.com/specimen/Montserrat*/
#define LV_FONT_MONTSERRAT_8 0
#define LV_FONT_MONTSERRAT_10 0
#define LV_FONT_MONTSERRAT_12 1
#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_16 1
#define LV_FONT_MONTSERRAT_18 1
#define LV_FONT_MONTSERRAT_20 0
#define LV_FONT_MONTSERRAT_22 0
#define LV_FONT_MONTSERRAT_24 0
#define LV_FONT_MONTSERRAT_26 0
#define LV_FONT_MONTSERRAT_28 0
#define LV_FONT_MONTSERRAT_30 0
#define LV_FONT_MONTSERRAT_32 0
#define LV_FONT_MONTSERRAT_34 0
#define LV_FONT_MONTSERRAT_36 0
#define LV_FONT_MONTSERRAT_38 0
#define LV_FONT_MONTSERRAT_40 0
#define LV_FONT_MONTSERRAT_42 0
#define LV_FONT_MONTSERRAT_44 0
#define LV_FONT_MONTSERRAT_46 0
#define LV_FONT_MONTSERRAT_48 0
/*Demonstrate special features*/
#define LV_FONT_MONTSERRAT_12_SUBPX 0
#define LV_FONT_MONTSERRAT_28_COMPRESSED 0 /*bpp = 3*/
#define LV_FONT_DEJAVU_16_PERSIAN_HEBREW 0 /*Hebrew, Arabic, Persian letters and all their forms*/
#define LV_FONT_SIMSUN_16_CJK 0 /*1000 most common CJK radicals*/
/*Pixel perfect monospace fonts*/
#define LV_FONT_UNSCII_8 0
#define LV_FONT_UNSCII_16 0
/*Optionally declare custom fonts here.
*You can use these fonts as default font too and they will be available globally.
*E.g. #define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(my_font_1) LV_FONT_DECLARE(my_font_2)*/
#define LV_FONT_CUSTOM_DECLARE
/*Always set a default font*/
#define LV_FONT_DEFAULT &lv_font_montserrat_14
/*Enable handling large font and/or fonts with a lot of characters.
*The limit depends on the font size, font face and bpp.
*Compiler error will be triggered if a font needs it.*/
#define LV_FONT_FMT_TXT_LARGE 0
/*Enables/disables support for compressed fonts.*/
#define LV_USE_FONT_COMPRESSED 0
/*Enable subpixel rendering*/
#define LV_USE_FONT_SUBPX 0
#if LV_USE_FONT_SUBPX
/*Set the pixel order of the display. Physical order of RGB channels. Doesn't matter with "normal" fonts.*/
#define LV_FONT_SUBPX_BGR 0 /*0: RGB; 1:BGR order*/
#endif
/*=================
* TEXT SETTINGS
*=================*/
/**
* Select a character encoding for strings.
* Your IDE or editor should have the same character encoding
* - LV_TXT_ENC_UTF8
* - LV_TXT_ENC_ASCII
*/
#define LV_TXT_ENC LV_TXT_ENC_UTF8
/*Can break (wrap) texts on these chars*/
#define LV_TXT_BREAK_CHARS " ,.;:-_"
/*If a word is at least this long, will break wherever "prettiest"
*To disable, set to a value <= 0*/
#define LV_TXT_LINE_BREAK_LONG_LEN 0
/*Minimum number of characters in a long word to put on a line before a break.
*Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/
#define LV_TXT_LINE_BREAK_LONG_PRE_MIN_LEN 3
/*Minimum number of characters in a long word to put on a line after a break.
*Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/
#define LV_TXT_LINE_BREAK_LONG_POST_MIN_LEN 3
/*The control character to use for signalling text recoloring.*/
#define LV_TXT_COLOR_CMD "#"
/*Support bidirectional texts. Allows mixing Left-to-Right and Right-to-Left texts.
*The direction will be processed according to the Unicode Bidirectional Algorithm:
*https://www.w3.org/International/articles/inline-bidi-markup/uba-basics*/
#define LV_USE_BIDI 0
#if LV_USE_BIDI
/*Set the default direction. Supported values:
*`LV_BASE_DIR_LTR` Left-to-Right
*`LV_BASE_DIR_RTL` Right-to-Left
*`LV_BASE_DIR_AUTO` detect texts base direction*/
#define LV_BIDI_BASE_DIR_DEF LV_BASE_DIR_AUTO
#endif
/*Enable Arabic/Persian processing
*In these languages characters should be replaced with an other form based on their position in the text*/
#define LV_USE_ARABIC_PERSIAN_CHARS 0
/*==================
* WIDGET USAGE
*================*/
/*Documentation of the widgets: https://docs.lvgl.io/latest/en/html/widgets/index.html*/
#define LV_USE_ARC 1
#define LV_USE_ANIMIMG 1
#define LV_USE_BAR 1
#define LV_USE_BTN 1
#define LV_USE_BTNMATRIX 1
#define LV_USE_CANVAS 1
#define LV_USE_CHECKBOX 1
#define LV_USE_DROPDOWN 1 /*Requires: lv_label*/
#define LV_USE_IMG 1 /*Requires: lv_label*/
#define LV_USE_LABEL 1
#if LV_USE_LABEL
#define LV_LABEL_TEXT_SELECTION 1 /*Enable selecting text of the label*/
#define LV_LABEL_LONG_TXT_HINT 1 /*Store some extra info in labels to speed up drawing of very long texts*/
#endif
#define LV_USE_LINE 1
#define LV_USE_ROLLER 1 /*Requires: lv_label*/
#if LV_USE_ROLLER
#define LV_ROLLER_INF_PAGES 7 /*Number of extra "pages" when the roller is infinite*/
#endif
#define LV_USE_SLIDER 1 /*Requires: lv_bar*/
#define LV_USE_SWITCH 1
#define LV_USE_TEXTAREA 1 /*Requires: lv_label*/
#if LV_USE_TEXTAREA != 0
#define LV_TEXTAREA_DEF_PWD_SHOW_TIME 1500 /*ms*/
#endif
#define LV_USE_TABLE 1
/*==================
* EXTRA COMPONENTS
*==================*/
/*-----------
* Widgets
*----------*/
#define LV_USE_CALENDAR 1
#if LV_USE_CALENDAR
#define LV_CALENDAR_WEEK_STARTS_MONDAY 0
#if LV_CALENDAR_WEEK_STARTS_MONDAY
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}
#else
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}
#endif
#define LV_CALENDAR_DEFAULT_MONTH_NAMES {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
#define LV_USE_CALENDAR_HEADER_ARROW 1
#define LV_USE_CALENDAR_HEADER_DROPDOWN 1
#endif /*LV_USE_CALENDAR*/
#define LV_USE_CHART 1
#define LV_USE_COLORWHEEL 1
#define LV_USE_IMGBTN 1
#define LV_USE_KEYBOARD 1
#define LV_USE_LED 1
#define LV_USE_LIST 1
#define LV_USE_MENU 1
#define LV_USE_METER 1
#define LV_USE_MSGBOX 1
#define LV_USE_SPINBOX 1
#define LV_USE_SPINNER 1
#define LV_USE_TABVIEW 1
#define LV_USE_TILEVIEW 1
#define LV_USE_WIN 1
#define LV_USE_SPAN 1
#if LV_USE_SPAN
/*A line text can contain maximum num of span descriptor */
#define LV_SPAN_SNIPPET_STACK_SIZE 64
#endif
/*-----------
* Themes
*----------*/
/*A simple, impressive and very complete theme*/
#define LV_USE_THEME_DEFAULT 1
#if LV_USE_THEME_DEFAULT
/*0: Light mode; 1: Dark mode*/
#define LV_THEME_DEFAULT_DARK 0
/*1: Enable grow on press*/
#define LV_THEME_DEFAULT_GROW 1
/*Default transition time in [ms]*/
#define LV_THEME_DEFAULT_TRANSITION_TIME 80
#endif /*LV_USE_THEME_DEFAULT*/
/*A very simple theme that is a good starting point for a custom theme*/
#define LV_USE_THEME_BASIC 1
/*A theme designed for monochrome displays*/
#define LV_USE_THEME_MONO 1
/*-----------
* Layouts
*----------*/
/*A layout similar to Flexbox in CSS.*/
#define LV_USE_FLEX 1
/*A layout similar to Grid in CSS.*/
#define LV_USE_GRID 1
/*---------------------
* 3rd party libraries
*--------------------*/
/*File system interfaces for common APIs */
/*API for fopen, fread, etc*/
#define LV_USE_FS_STDIO 0
#if LV_USE_FS_STDIO
#define LV_FS_STDIO_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_STDIO_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_STDIO_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for open, read, etc*/
#define LV_USE_FS_POSIX 0
#if LV_USE_FS_POSIX
#define LV_FS_POSIX_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_POSIX_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_POSIX_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for CreateFile, ReadFile, etc*/
#define LV_USE_FS_WIN32 0
#if LV_USE_FS_WIN32
#define LV_FS_WIN32_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_WIN32_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_WIN32_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for FATFS (needs to be added separately). Uses f_open, f_read, etc*/
#define LV_USE_FS_FATFS 0
#if LV_USE_FS_FATFS
#define LV_FS_FATFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_FATFS_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*PNG decoder library*/
#define LV_USE_PNG 0
/*BMP decoder library*/
#define LV_USE_BMP 0
/* JPG + split JPG decoder library.
* Split JPG is a custom format optimized for embedded systems. */
#define LV_USE_SJPG 0
/*GIF decoder library*/
#define LV_USE_GIF 0
/*QR code library*/
#define LV_USE_QRCODE 0
/*FreeType library*/
#define LV_USE_FREETYPE 0
#if LV_USE_FREETYPE
/*Memory used by FreeType to cache characters [bytes] (-1: no caching)*/
#define LV_FREETYPE_CACHE_SIZE (16 * 1024)
#if LV_FREETYPE_CACHE_SIZE >= 0
/* 1: bitmap cache use the sbit cache, 0:bitmap cache use the image cache. */
/* sbit cache:it is much more memory efficient for small bitmaps(font size < 256) */
/* if font size >= 256, must be configured as image cache */
#define LV_FREETYPE_SBIT_CACHE 0
/* Maximum number of opened FT_Face/FT_Size objects managed by this cache instance. */
/* (0:use system defaults) */
#define LV_FREETYPE_CACHE_FT_FACES 0
#define LV_FREETYPE_CACHE_FT_SIZES 0
#endif
#endif
/*Rlottie library*/
#define LV_USE_RLOTTIE 0
/*FFmpeg library for image decoding and playing videos
*Supports all major image formats so do not enable other image decoder with it*/
#define LV_USE_FFMPEG 0
#if LV_USE_FFMPEG
/*Dump input information to stderr*/
#define LV_FFMPEG_AV_DUMP_FORMAT 0
#endif
/*-----------
* Others
*----------*/
/*1: Enable API to take snapshot for object*/
#define LV_USE_SNAPSHOT 0
/*1: Enable Monkey test*/
#define LV_USE_MONKEY 0
/*1: Enable grid navigation*/
#define LV_USE_GRIDNAV 0
/*1: Enable lv_obj fragment*/
#define LV_USE_FRAGMENT 0
/*==================
* EXAMPLES
*==================*/
/*Enable the examples to be built with the library*/
#define LV_BUILD_EXAMPLES 1
/*===================
* DEMO USAGE
====================*/
/*Show some widget. It might be required to increase `LV_MEM_SIZE` */
#define LV_USE_DEMO_WIDGETS 1
#if LV_USE_DEMO_WIDGETS
#define LV_DEMO_WIDGETS_SLIDESHOW 1
#endif
/*Demonstrate the usage of encoder and keyboard*/
#define LV_USE_DEMO_KEYPAD_AND_ENCODER 1
/*Benchmark your system*/
#define LV_USE_DEMO_BENCHMARK 1
/*Stress test for LVGL*/
#define LV_USE_DEMO_STRESS 1
/*Music player demo*/
#define LV_USE_DEMO_MUSIC 1
#if LV_USE_DEMO_MUSIC
# define LV_DEMO_MUSIC_SQUARE 0
# define LV_DEMO_MUSIC_LANDSCAPE 0
# define LV_DEMO_MUSIC_ROUND 0
# define LV_DEMO_MUSIC_LARGE 0
# define LV_DEMO_MUSIC_AUTO_PLAY 0
#endif
/*--END OF LV_CONF_H--*/
#endif /*LV_CONF_H*/
#endif /*End of "Content enable"*/
@@ -0,0 +1 @@
ESP32 version V3.1.0 LVGL version V8.4
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

@@ -0,0 +1,17 @@
# 请使用arduino_esp32_v3.1版本
# lv_conf.h文件必要设置(lvgl_v8)
```c
#define LV_COLOR_DEPTH 16
#define LV_COLOR_16_SWAP 0
#define LV_MEM_CUSTOM 1
#define LV_TICK_CUSTOM 1
#define LV_MEMCPY_MEMSET_STD 1
#define LV_ATTRIBUTE_FAST_MEM IRAM_ATTR
```
@@ -0,0 +1,87 @@
#pragma GCC push_options
#pragma GCC optimize("O3")
#include <Arduino.h>
#include "lvgl.h"
#include "demos/lv_demos.h"
#include "pins_config.h"
#include "src/lcd/jd9165_lcd.h"
#include "src/touch/gt911_touch.h"
jd9165_lcd lcd = jd9165_lcd(LCD_RST);
gt911_touch touch = gt911_touch(TP_I2C_SDA, TP_I2C_SCL, TP_RST, TP_INT);
// static lv_disp_draw_buf_t draw_buf;
lv_display_t * disp_drv;
static uint32_t *buf;
static uint32_t *buf1;
// 显示刷新
void my_disp_flush( lv_display_t *disp, const lv_area_t *area, uint8_t * color_map)
{
const int offsetx1 = area->x1;
const int offsetx2 = area->x2;
const int offsety1 = area->y1;
const int offsety2 = area->y2;
lcd.lcd_draw_bitmap(offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, color_map);
lv_display_flush_ready(disp); // 告诉lvgl刷新完成
}
void my_touchpad_read(lv_indev_t *indev_driver, lv_indev_data_t *data)
{
bool touched;
uint16_t touchX, touchY;
touched = touch.getTouch(&touchX, &touchY);
if (!touched)
{
data->state = LV_INDEV_STATE_REL;
}
else
{
data->state = LV_INDEV_STATE_PR;
// 设置坐标
data->point.x = touchX;
data->point.y = touchY;
Serial.printf("x=%d,y=%d \r\n",touchX,touchY);
}
}
void setup()
{
Serial.begin(115200);
Serial.println("ESP32P4 MIPI DSI LVGL");
lcd.begin();
touch.begin();
lv_init();
uint32_t buffer_size = LCD_H_RES * LCD_V_RES;
buf = (uint32_t *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);
buf1 =(uint32_t *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);
assert(buf);
assert(buf1);
disp_drv = lv_display_create(LCD_H_RES, LCD_V_RES);
lv_display_set_flush_cb(disp_drv, my_disp_flush);
lv_display_set_buffers(disp_drv, buf, buf1, buffer_size * sizeof(uint32_t), LV_DISPLAY_RENDER_MODE_FULL);
/*Initialize the display*/
lv_indev_t * indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, my_touchpad_read);
lv_demo_widgets(); /* 小部件示例 */
// lv_demo_music(); /* 类似智能手机的现代音乐播放器演示 */
// lv_demo_stress(); /* LVGL 压力测试 */
// lv_demo_benchmark(); /* 用于测量 LVGL 性能或比较不同设置的演示 */
Serial.println("setup ");
}
void loop()
{
lv_timer_handler();
delay(5);
}
@@ -0,0 +1,12 @@
#pragma once
#define LCD_H_RES 1024
#define LCD_V_RES 600
#define LCD_RST 27
#define LCD_LED 23
#define TP_I2C_SDA 7
#define TP_I2C_SCL 8
#define TP_RST -1
#define TP_INT -1
@@ -0,0 +1,341 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "soc/soc_caps.h"
#if SOC_MIPI_DSI_SUPPORTED
#include "esp_check.h"
#include "esp_log.h"
#include "esp_lcd_panel_commands.h"
#include "esp_lcd_panel_interface.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_mipi_dsi.h"
#include "esp_lcd_panel_vendor.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_lcd_jd9165.h"
#define JD9165_CMD_GS_BIT (1 << 0)
#define JD9165_CMD_SS_BIT (1 << 1)
typedef struct {
esp_lcd_panel_io_handle_t io;
int reset_gpio_num;
uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register
uint8_t colmod_val; // save surrent value of LCD_CMD_COLMOD register
const jd9165_lcd_init_cmd_t *init_cmds;
uint16_t init_cmds_size;
struct {
unsigned int reset_level: 1;
} flags;
// To save the original functions of MIPI DPI panel
esp_err_t (*del)(esp_lcd_panel_t *panel);
esp_err_t (*init)(esp_lcd_panel_t *panel);
} jd9165_panel_t;
static const char *TAG = "jd9165";
static esp_err_t panel_jd9165_del(esp_lcd_panel_t *panel);
static esp_err_t panel_jd9165_init(esp_lcd_panel_t *panel);
static esp_err_t panel_jd9165_reset(esp_lcd_panel_t *panel);
static esp_err_t panel_jd9165_invert_color(esp_lcd_panel_t *panel, bool invert_color_data);
static esp_err_t panel_jd9165_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y);
static esp_err_t panel_jd9165_disp_on_off(esp_lcd_panel_t *panel, bool on_off);
esp_err_t esp_lcd_new_panel_jd9165(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config,
esp_lcd_panel_handle_t *ret_panel)
{
ESP_LOGI(TAG,"jd9165:1.0.1");
ESP_RETURN_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, TAG, "invalid arguments");
jd9165_vendor_config_t *vendor_config = (jd9165_vendor_config_t *)panel_dev_config->vendor_config;
ESP_RETURN_ON_FALSE(vendor_config && vendor_config->mipi_config.dpi_config && vendor_config->mipi_config.dsi_bus, ESP_ERR_INVALID_ARG, TAG,
"invalid vendor config");
esp_err_t ret = ESP_OK;
jd9165_panel_t *jd9165 = (jd9165_panel_t *)calloc(1, sizeof(jd9165_panel_t));
ESP_RETURN_ON_FALSE(jd9165, ESP_ERR_NO_MEM, TAG, "no mem for jd9165 panel");
if (panel_dev_config->reset_gpio_num >= 0) {
gpio_config_t io_conf = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num,
};
ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed");
}
switch (panel_dev_config->color_space) {
case LCD_RGB_ELEMENT_ORDER_RGB:
jd9165->madctl_val = 0;
break;
case LCD_RGB_ELEMENT_ORDER_BGR:
jd9165->madctl_val |= LCD_CMD_BGR_BIT;
break;
default:
ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space");
break;
}
jd9165->io = io;
jd9165->init_cmds = vendor_config->init_cmds;
jd9165->init_cmds_size = vendor_config->init_cmds_size;
jd9165->reset_gpio_num = panel_dev_config->reset_gpio_num;
jd9165->flags.reset_level = panel_dev_config->flags.reset_active_high;
// Create MIPI DPI panel
esp_lcd_panel_handle_t panel_handle = NULL;
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_dpi(vendor_config->mipi_config.dsi_bus, vendor_config->mipi_config.dpi_config, &panel_handle), err, TAG,
"create MIPI DPI panel failed");
ESP_LOGD(TAG, "new MIPI DPI panel @%p", panel_handle);
// Save the original functions of MIPI DPI panel
jd9165->del = panel_handle->del;
jd9165->init = panel_handle->init;
// Overwrite the functions of MIPI DPI panel
panel_handle->del = panel_jd9165_del;
panel_handle->init = panel_jd9165_init;
panel_handle->reset = panel_jd9165_reset;
panel_handle->mirror = panel_jd9165_mirror;
panel_handle->invert_color = panel_jd9165_invert_color;
panel_handle->disp_on_off = panel_jd9165_disp_on_off;
panel_handle->user_data = jd9165;
*ret_panel = panel_handle;
ESP_LOGD(TAG, "new jd9165 panel @%p", jd9165);
return ESP_OK;
err:
if (jd9165) {
if (panel_dev_config->reset_gpio_num >= 0) {
gpio_reset_pin(panel_dev_config->reset_gpio_num);
}
free(jd9165);
}
return ret;
}
static const jd9165_lcd_init_cmd_t vendor_specific_init_default[] = {
// {cmd, { data }, data_size, delay_ms}
//{0x11, (uint8_t []){0x00}, 1, 120},
//{0x29, (uint8_t []){0x00}, 1, 20},
{0x30, (uint8_t[]){0x00}, 1, 0},
{0xF7, (uint8_t[]){0x49,0x61,0x02,0x00}, 4, 0},
{0x30, (uint8_t[]){0x01}, 1, 0},
{0x04, (uint8_t[]){0x0C}, 1, 0},
{0x05, (uint8_t[]){0x00}, 1, 0},
{0x06, (uint8_t[]){0x00}, 1, 0},
{0x0B, (uint8_t[]){0x11}, 1, 0},
{0x17, (uint8_t[]){0x00}, 1, 0},
{0x20, (uint8_t[]){0x04}, 1, 0},
{0x1F, (uint8_t[]){0x05}, 1, 0},
{0x23, (uint8_t[]){0x00}, 1, 0},
{0x25, (uint8_t[]){0x19}, 1, 0},
{0x28, (uint8_t[]){0x18}, 1, 0},
{0x29, (uint8_t[]){0x04}, 1, 0},
{0x2A, (uint8_t[]){0x01}, 1, 0},
{0x2B, (uint8_t[]){0x04}, 1, 0},
{0x2C, (uint8_t[]){0x01}, 1, 0},
{0x30, (uint8_t[]){0x02}, 1, 0},
{0x01, (uint8_t[]){0x22}, 1, 0},
{0x03, (uint8_t[]){0x12}, 1, 0},
{0x04, (uint8_t[]){0x00}, 1, 0},
{0x05, (uint8_t[]){0x64}, 1, 0},
{0x0A, (uint8_t[]){0x08}, 1, 0},
{0x0B, (uint8_t[]){0x0A,0x1A,0x0B,0x0D,0x0D,0x11,0x10,0x06,0x08,0x1F,0x1D}, 11, 0},
{0x0C, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x0D, (uint8_t[]){0x16,0x1B,0x0B,0x0D,0x0D,0x11,0x10,0x07,0x09,0x1E,0x1C}, 11, 0},
{0x0E, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x0F, (uint8_t[]){0x16,0x1B,0x0D,0x0B,0x0D,0x11,0x10,0x1C,0x1E,0x09,0x07}, 11, 0},
{0x10, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x11, (uint8_t[]){0x0A,0x1A,0x0D,0x0B,0x0D,0x11,0x10,0x1D,0x1F,0x08,0x06}, 11, 0},
{0x12, (uint8_t[]){0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}, 11, 0},
{0x14, (uint8_t[]){0x00,0x00,0x11,0x11}, 4, 0},
{0x18, (uint8_t[]){0x99}, 1, 0},
{0x30, (uint8_t[]){0x06}, 1, 0},
{0x12, (uint8_t[]){0x36,0x2C,0x2E,0x3C,0x38,0x35,0x35,0x32,0x2E,0x1D,0x2B,0x21,0x16,0x29}, 14, 0},
{0x13, (uint8_t[]){0x36,0x2C,0x2E,0x3C,0x38,0x35,0x35,0x32,0x2E,0x1D,0x2B,0x21,0x16,0x29}, 14, 0},
// {0x30, (uint8_t[]){0x08}, 1, 0},
// {0x05, (uint8_t[]){0x01}, 1, 0},
// {0x0C, (uint8_t[]){0x1A}, 1, 0},
// {0x0D, (uint8_t[]){0x0E}, 1, 0},
// {0x30, (uint8_t[]){0x07}, 1, 0},
// {0x01, (uint8_t[]){0x04}, 1, 0},
{0x30, (uint8_t[]){0x0A}, 1, 0},
{0x02, (uint8_t[]){0x4F}, 1, 0},
{0x0B, (uint8_t[]){0x40}, 1, 0},
{0x12, (uint8_t[]){0x3E}, 1, 0},
{0x13, (uint8_t[]){0x78}, 1, 0},
{0x30, (uint8_t[]){0x0D}, 1, 0},
{0x0D, (uint8_t[]){0x04}, 1, 0},
{0x10, (uint8_t[]){0x0C}, 1, 0},
{0x11, (uint8_t[]){0x0C}, 1, 0},
{0x12, (uint8_t[]){0x0C}, 1, 0},
{0x13, (uint8_t[]){0x0C}, 1, 0},
{0x30, (uint8_t[]){0x00}, 1, 0},
// {0X3A, (uint8_t[]){0x55}, 1, 0},
{0x11, (uint8_t[]){0x00}, 1, 120},
{0x29, (uint8_t[]){0x00}, 1, 50},
};
static esp_err_t panel_jd9165_del(esp_lcd_panel_t *panel)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
if (jd9165->reset_gpio_num >= 0) {
gpio_reset_pin(jd9165->reset_gpio_num);
}
// Delete MIPI DPI panel
jd9165->del(panel);
ESP_LOGD(TAG, "del jd9165 panel @%p", jd9165);
free(jd9165);
return ESP_OK;
}
static esp_err_t panel_jd9165_init(esp_lcd_panel_t *panel)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
const jd9165_lcd_init_cmd_t *init_cmds = NULL;
uint16_t init_cmds_size = 0;
bool is_cmd_overwritten = false;
uint8_t ID[3];
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_rx_param(io, 0x04, ID, 3), TAG, "read ID failed");
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) {
jd9165->madctl_val,
}, 1), TAG, "send command failed");
// vendor specific initialization, it can be different between manufacturers
// should consult the LCD supplier for initialization sequence code
if (jd9165->init_cmds) {
init_cmds = jd9165->init_cmds;
init_cmds_size = jd9165->init_cmds_size;
} else {
init_cmds = vendor_specific_init_default;
init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(jd9165_lcd_init_cmd_t);
}
for (int i = 0; i < init_cmds_size; i++) {
// Check if the command has been used or conflicts with the internal
if (init_cmds[i].data_bytes > 0) {
switch (init_cmds[i].cmd) {
case LCD_CMD_MADCTL:
is_cmd_overwritten = true;
jd9165->madctl_val = ((uint8_t *)init_cmds[i].data)[0];
break;
default:
is_cmd_overwritten = false;
break;
}
if (is_cmd_overwritten) {
is_cmd_overwritten = false;
ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence",
init_cmds[i].cmd);
}
}
// Send command
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed");
vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms));
}
ESP_LOGD(TAG, "send init commands success");
ESP_RETURN_ON_ERROR(jd9165->init(panel), TAG, "init MIPI DPI panel failed");
return ESP_OK;
}
static esp_err_t panel_jd9165_reset(esp_lcd_panel_t *panel)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
// Perform hardware reset
if (jd9165->reset_gpio_num >= 0) {
gpio_set_level(jd9165->reset_gpio_num, !jd9165->flags.reset_level);
vTaskDelay(pdMS_TO_TICKS(5));
gpio_set_level(jd9165->reset_gpio_num, jd9165->flags.reset_level);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(jd9165->reset_gpio_num, !jd9165->flags.reset_level);
vTaskDelay(pdMS_TO_TICKS(120));
} else if (io) { // Perform software reset
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed");
vTaskDelay(pdMS_TO_TICKS(120));
}
return ESP_OK;
}
static esp_err_t panel_jd9165_invert_color(esp_lcd_panel_t *panel, bool invert_color_data)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
uint8_t command = 0;
ESP_RETURN_ON_FALSE(io, ESP_ERR_INVALID_STATE, TAG, "invalid panel IO");
if (invert_color_data) {
command = LCD_CMD_INVON;
} else {
command = LCD_CMD_INVOFF;
}
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed");
return ESP_OK;
}
static esp_err_t panel_jd9165_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
uint8_t madctl_val = jd9165->madctl_val;
ESP_RETURN_ON_FALSE(io, ESP_ERR_INVALID_STATE, TAG, "invalid panel IO");
// Control mirror through LCD command
if (mirror_x) {
madctl_val |= JD9165_CMD_GS_BIT;
} else {
madctl_val &= ~JD9165_CMD_GS_BIT;
}
if (mirror_y) {
madctl_val |= JD9165_CMD_SS_BIT;
} else {
madctl_val &= ~JD9165_CMD_SS_BIT;
}
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t []) {
madctl_val
}, 1), TAG, "send command failed");
jd9165->madctl_val = madctl_val;
return ESP_OK;
}
static esp_err_t panel_jd9165_disp_on_off(esp_lcd_panel_t *panel, bool on_off)
{
jd9165_panel_t *jd9165 = (jd9165_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = jd9165->io;
int command = 0;
if (on_off) {
command = LCD_CMD_DISPON;
} else {
command = LCD_CMD_DISPOFF;
}
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed");
return ESP_OK;
}
#endif
@@ -0,0 +1,125 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "soc/soc_caps.h"
#if SOC_MIPI_DSI_SUPPORTED
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_mipi_dsi.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief LCD panel initialization commands.
*
*/
typedef struct {
int cmd; /*<! The specific LCD command */
const void *data; /*<! Buffer that holds the command specific data */
size_t data_bytes; /*<! Size of `data` in memory, in bytes */
unsigned int delay_ms; /*<! Delay in milliseconds after this command */
} jd9165_lcd_init_cmd_t;
/**
* @brief LCD panel vendor configuration.
*
* @note This structure needs to be passed to the `vendor_config` field in `esp_lcd_panel_dev_config_t`.
*
*/
typedef struct {
const jd9165_lcd_init_cmd_t *init_cmds; /*!< Pointer to initialization commands array. Set to NULL if using default commands.
* The array should be declared as `static const` and positioned outside the function.
* Please refer to `vendor_specific_init_default` in source file.
*/
uint16_t init_cmds_size; /*<! Number of commands in above array */
struct {
esp_lcd_dsi_bus_handle_t dsi_bus; /*!< MIPI-DSI bus configuration */
const esp_lcd_dpi_panel_config_t *dpi_config; /*!< MIPI-DPI panel configuration */
} mipi_config;
} jd9165_vendor_config_t;
/**
* @brief Create LCD panel for model JD9165
*
* @note Vendor specific initialization can be different between manufacturers, should consult the LCD supplier for initialization sequence code.
*
* @param[in] io LCD panel IO handle
* @param[in] panel_dev_config General panel device configuration
* @param[out] ret_panel Returned LCD panel handle
* @return
* - ESP_ERR_INVALID_ARG if parameter is invalid
* - ESP_OK on success
* - Otherwise on fail
*/
esp_err_t esp_lcd_new_panel_jd9165(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config,
esp_lcd_panel_handle_t *ret_panel);
/**
* @brief MIPI-DSI bus configuration structure
*
* @param[in] lane_num Number of data lanes
* @param[in] lane_mbps Lane bit rate in Mbps
*
*/
#define JD9165_PANEL_BUS_DSI_2CH_CONFIG() \
{ \
.bus_id = 0, \
.num_data_lanes = 2, \
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, \
.lane_bit_rate_mbps = 550, \
}
/**
* @brief MIPI-DBI panel IO configuration structure
*
*/
#define JD9165_PANEL_IO_DBI_CONFIG() \
{ \
.virtual_channel = 0, \
.lcd_cmd_bits = 8, \
.lcd_param_bits = 8, \
}
/**
* @brief MIPI DPI configuration structure
*
* @note refresh_rate = (dpi_clock_freq_mhz * 1000000) / (h_res + hsync_pulse_width + hsync_back_porch + hsync_front_porch)
* / (v_res + vsync_pulse_width + vsync_back_porch + vsync_front_porch)
*
* @param[in] px_format Pixel format of the panel
*
*/
#define JD9165_1024_600_PANEL_60HZ_DPI_CONFIG(px_format) \
{ \
.virtual_channel = 0, \
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, \
.dpi_clock_freq_mhz = 56, \
.pixel_format = px_format, \
.num_fbs = 1, \
.video_timing = { \
.h_size = 1024, \
.v_size = 600, \
.hsync_pulse_width = 40, \
.hsync_back_porch = 160, \
.hsync_front_porch = 160, \
.vsync_pulse_width = 10, \
.vsync_back_porch = 23, \
.vsync_front_porch = 12, \
}, \
.flags= { \
.use_dma2d = true, \
}, \
}
#endif
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,153 @@
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_timer.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_mipi_dsi.h"
#include "esp_lcd_panel_io.h"
#include "esp_ldo_regulator.h"
#include "driver/gpio.h"
#include "esp_err.h"
#include "esp_log.h"
#include "Arduino.h"
#include "esp_lcd_jd9165.h"
#include "jd9165_lcd.h"
#define LCD_H_RES 1024
#define LCD_V_RES 600
#define MIPI_DPI_PX_FORMAT (LCD_COLOR_PIXEL_FORMAT_RGB565)
#define LCD_BIT_PER_PIXEL (16)
// “VDD_MIPI_DPHY”应供电 2.5V,可从内部 LDO 稳压器或外部 LDO 芯片获取电源
#define EXAMPLE_MIPI_DSI_PHY_PWR_LDO_CHAN 3 // LDO_VO3 连接至 VDD_MIPI_DPHY
#define EXAMPLE_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV 2500
#define EXAMPLE_LCD_BK_LIGHT_ON_LEVEL 1
#define EXAMPLE_LCD_BK_LIGHT_OFF_LEVEL !EXAMPLE_LCD_BK_LIGHT_ON_LEVEL
#define EXAMPLE_PIN_NUM_BK_LIGHT GPIO_NUM_23
static const char *TAG = "example";
esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_io_handle_t io_handle = NULL;
jd9165_lcd::jd9165_lcd(int8_t lcd_rst)
{
_lcd_rst = lcd_rst;
}
void jd9165_lcd::example_bsp_enable_dsi_phy_power()
{
// 打开 MIPI DSI PHY 的电源,使其从“无电”状态进入“关机”状态
esp_ldo_channel_handle_t ldo_mipi_phy = NULL;
#ifdef EXAMPLE_MIPI_DSI_PHY_PWR_LDO_CHAN
esp_ldo_channel_config_t ldo_mipi_phy_config = {
.chan_id = EXAMPLE_MIPI_DSI_PHY_PWR_LDO_CHAN,
.voltage_mv = EXAMPLE_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV,
};
ESP_ERROR_CHECK(esp_ldo_acquire_channel(&ldo_mipi_phy_config, &ldo_mipi_phy));
ESP_LOGI(TAG, "MIPI DSI PHY Powered on");
#endif
}
void jd9165_lcd::example_bsp_init_lcd_backlight()
{
#if EXAMPLE_PIN_NUM_BK_LIGHT >= 0
gpio_config_t bk_gpio_config = {
.pin_bit_mask = 1ULL << EXAMPLE_PIN_NUM_BK_LIGHT,
.mode = GPIO_MODE_OUTPUT
};
ESP_ERROR_CHECK(gpio_config(&bk_gpio_config));
#endif
}
void jd9165_lcd::example_bsp_set_lcd_backlight(uint32_t level)
{
#if EXAMPLE_PIN_NUM_BK_LIGHT >= 0
gpio_set_level(EXAMPLE_PIN_NUM_BK_LIGHT, level);
#endif
}
void jd9165_lcd::begin()
{
example_bsp_enable_dsi_phy_power();
example_bsp_init_lcd_backlight();
example_bsp_set_lcd_backlight(EXAMPLE_LCD_BK_LIGHT_OFF_LEVEL);
// 首先创建 MIPI DSI 总线,它还将初始化 DSI PHY
esp_lcd_dsi_bus_handle_t mipi_dsi_bus;
esp_lcd_dsi_bus_config_t bus_config = JD9165_PANEL_BUS_DSI_2CH_CONFIG();
ESP_ERROR_CHECK(esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus));
ESP_LOGI(TAG, "Install MIPI DSI LCD control panel");
// 我们使用DBI接口发送LCD命令和参数
esp_lcd_dbi_io_config_t dbi_config = JD9165_PANEL_IO_DBI_CONFIG();
ESP_ERROR_CHECK(esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &io_handle));
// 创建JD9165控制面板
esp_lcd_dpi_panel_config_t dpi_config = JD9165_1024_600_PANEL_60HZ_DPI_CONFIG(MIPI_DPI_PX_FORMAT);
jd9165_vendor_config_t vendor_config = {
.mipi_config = {
.dsi_bus = mipi_dsi_bus,
.dpi_config = &dpi_config,
},
};
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = _lcd_rst,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = LCD_BIT_PER_PIXEL,
.vendor_config = &vendor_config,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_jd9165(io_handle, &panel_config, &panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
// 打开背光
example_bsp_set_lcd_backlight(EXAMPLE_LCD_BK_LIGHT_ON_LEVEL);
}
void jd9165_lcd::lcd_draw_bitmap(uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end, uint8_t *color_data)
{
esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, color_data);
}
void jd9165_lcd::draw16bitbergbbitmap(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t *color_data)
{
uint16_t x_start = x;
uint16_t y_start = y;
uint16_t x_end = w + x;
uint16_t y_end = h + y;
esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, color_data);
}
void jd9165_lcd::fillScreen(uint16_t color)
{
uint16_t *color_data = (uint16_t *)heap_caps_malloc(480 * 272 * 2, MALLOC_CAP_INTERNAL);
memset(color_data, color, 480 * 272 * 2);
draw16bitbergbbitmap(0, 0, 480, 272, color_data);
free(color_data);
}
void jd9165_lcd::te_on()
{
esp_lcd_panel_io_tx_param(io_handle, 0x35,new (uint8_t[]){0x00}, 1);
}
void jd9165_lcd::te_off()
{
esp_lcd_panel_io_tx_param(io_handle, 0x34,new (uint8_t[]){0x00}, 0);
}
uint16_t jd9165_lcd::width()
{
return LCD_H_RES;
}
uint16_t jd9165_lcd::height()
{
return LCD_V_RES;
}
@@ -0,0 +1,26 @@
#ifndef _JD9165_LCD_H
#define _JD9165_LCD_H
#include <stdio.h>
class jd9165_lcd
{
public:
jd9165_lcd(int8_t lcd_rst);
void begin();
void example_bsp_enable_dsi_phy_power();
void example_bsp_init_lcd_backlight();
void example_bsp_set_lcd_backlight(uint32_t level);
void lcd_draw_bitmap(uint16_t x_start, uint16_t y_start,
uint16_t x_end, uint16_t y_end, uint8_t *color_data);
void draw16bitbergbbitmap(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t *color_data);
void fillScreen(uint16_t color);
void te_on();
void te_off();
uint16_t width();
uint16_t height();
private:
int8_t _lcd_rst;
};
#endif
@@ -0,0 +1,236 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_system.h"
#include "esp_err.h"
#include "esp_check.h"
#include "esp_log.h"
#include "esp_lcd_touch.h"
static const char *TAG = "TP";
/*******************************************************************************
* Function definitions
*******************************************************************************/
/*******************************************************************************
* Local variables
*******************************************************************************/
/*******************************************************************************
* Public API functions
*******************************************************************************/
esp_err_t esp_lcd_touch_read_data(esp_lcd_touch_handle_t tp)
{
assert(tp != NULL);
assert(tp->read_data != NULL);
return tp->read_data(tp);
}
bool esp_lcd_touch_get_coordinates(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num)
{
bool touched = false;
assert(tp != NULL);
assert(x != NULL);
assert(y != NULL);
assert(tp->get_xy != NULL);
touched = tp->get_xy(tp, x, y, strength, point_num, max_point_num);
if (!touched) {
return false;
}
/* Process coordinates by user */
if (tp->config.process_coordinates != NULL) {
tp->config.process_coordinates(tp, x, y, strength, point_num, max_point_num);
}
/* Software coordinates adjustment needed */
bool sw_adj_needed = ((tp->config.flags.mirror_x && (tp->set_mirror_x == NULL)) ||
(tp->config.flags.mirror_y && (tp->set_mirror_y == NULL)) ||
(tp->config.flags.swap_xy && (tp->set_swap_xy == NULL)));
/* Adjust all coordinates */
for (int i = 0; (sw_adj_needed && i < *point_num); i++) {
/* Mirror X coordinates (if not supported by HW) */
if (tp->config.flags.mirror_x && tp->set_mirror_x == NULL) {
x[i] = tp->config.x_max - x[i];
}
/* Mirror Y coordinates (if not supported by HW) */
if (tp->config.flags.mirror_y && tp->set_mirror_y == NULL) {
y[i] = tp->config.y_max - y[i];
}
/* Swap X and Y coordinates (if not supported by HW) */
if (tp->config.flags.swap_xy && tp->set_swap_xy == NULL) {
uint16_t tmp = x[i];
x[i] = y[i];
y[i] = tmp;
}
}
return touched;
}
#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0)
esp_err_t esp_lcd_touch_get_button_state(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state)
{
assert(tp != NULL);
assert(state != NULL);
*state = 0;
if (tp->get_button_state) {
return tp->get_button_state(tp, n, state);
} else {
return ESP_ERR_NOT_SUPPORTED;
}
return ESP_OK;
}
#endif
esp_err_t esp_lcd_touch_set_swap_xy(esp_lcd_touch_handle_t tp, bool swap)
{
assert(tp != NULL);
tp->config.flags.swap_xy = swap;
/* Is swap supported by HW? */
if (tp->set_swap_xy) {
return tp->set_swap_xy(tp, swap);
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_get_swap_xy(esp_lcd_touch_handle_t tp, bool *swap)
{
assert(tp != NULL);
assert(swap != NULL);
/* Is swap supported by HW? */
if (tp->get_swap_xy) {
return tp->get_swap_xy(tp, swap);
} else {
*swap = tp->config.flags.swap_xy;
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_set_mirror_x(esp_lcd_touch_handle_t tp, bool mirror)
{
assert(tp != NULL);
tp->config.flags.mirror_x = mirror;
/* Is mirror supported by HW? */
if (tp->set_mirror_x) {
return tp->set_mirror_x(tp, mirror);
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_get_mirror_x(esp_lcd_touch_handle_t tp, bool *mirror)
{
assert(tp != NULL);
assert(mirror != NULL);
/* Is swap supported by HW? */
if (tp->get_mirror_x) {
return tp->get_mirror_x(tp, mirror);
} else {
*mirror = tp->config.flags.mirror_x;
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_set_mirror_y(esp_lcd_touch_handle_t tp, bool mirror)
{
assert(tp != NULL);
tp->config.flags.mirror_y = mirror;
/* Is mirror supported by HW? */
if (tp->set_mirror_y) {
return tp->set_mirror_y(tp, mirror);
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_get_mirror_y(esp_lcd_touch_handle_t tp, bool *mirror)
{
assert(tp != NULL);
assert(mirror != NULL);
/* Is swap supported by HW? */
if (tp->get_mirror_y) {
return tp->get_mirror_y(tp, mirror);
} else {
*mirror = tp->config.flags.mirror_y;
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_del(esp_lcd_touch_handle_t tp)
{
assert(tp != NULL);
if (tp->del != NULL) {
return tp->del(tp);
}
return ESP_OK;
}
esp_err_t esp_lcd_touch_register_interrupt_callback(esp_lcd_touch_handle_t tp, esp_lcd_touch_interrupt_callback_t callback)
{
esp_err_t ret = ESP_OK;
assert(tp != NULL);
/* Interrupt pin is not selected */
if (tp->config.int_gpio_num == GPIO_NUM_NC) {
return ESP_ERR_INVALID_ARG;
}
tp->config.interrupt_callback = callback;
if (callback != NULL) {
ret = gpio_install_isr_service(0);
/* ISR service can be installed from user before, then it returns invalid state */
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "GPIO ISR install failed");
return ret;
}
/* Add GPIO ISR handler */
ret = gpio_intr_enable(tp->config.int_gpio_num);
ESP_RETURN_ON_ERROR(ret, TAG, "GPIO ISR install failed");
ret = gpio_isr_handler_add(tp->config.int_gpio_num, (gpio_isr_t)tp->config.interrupt_callback, tp);
ESP_RETURN_ON_ERROR(ret, TAG, "GPIO ISR install failed");
} else {
/* Remove GPIO ISR handler */
ret = gpio_isr_handler_remove(tp->config.int_gpio_num);
ESP_RETURN_ON_ERROR(ret, TAG, "GPIO ISR remove handler failed");
ret = gpio_intr_disable(tp->config.int_gpio_num);
ESP_RETURN_ON_ERROR(ret, TAG, "GPIO ISR disable failed");
}
return ESP_OK;
}
@@ -0,0 +1,370 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file
* @brief ESP LCD touch
*/
#pragma once
#include <stdbool.h>
#include "sdkconfig.h"
#include "esp_err.h"
#include "driver/gpio.h"
#include "esp_lcd_panel_io.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#ifdef __cplusplus
extern "C" {
#endif
#define CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS (1)
#define CONFIG_ESP_LCD_TOUCH_MAX_POINTS (5)
/**
* @brief Touch controller type
*
*/
typedef struct esp_lcd_touch_s esp_lcd_touch_t;
typedef esp_lcd_touch_t *esp_lcd_touch_handle_t;
/**
* @brief Touch controller interrupt callback type
*
*/
typedef void (*esp_lcd_touch_interrupt_callback_t)(esp_lcd_touch_handle_t tp);
/**
* @brief Touch Configuration Type
*
*/
typedef struct {
uint16_t x_max; /*!< X coordinates max (for mirroring) */
uint16_t y_max; /*!< Y coordinates max (for mirroring) */
gpio_num_t rst_gpio_num; /*!< GPIO number of reset pin */
gpio_num_t int_gpio_num; /*!< GPIO number of interrupt pin */
struct {
unsigned int reset: 1; /*!< Level of reset pin in reset */
unsigned int interrupt: 1;/*!< Active Level of interrupt pin */
} levels;
struct {
unsigned int swap_xy: 1; /*!< Swap X and Y after read coordinates */
unsigned int mirror_x: 1; /*!< Mirror X after read coordinates */
unsigned int mirror_y: 1; /*!< Mirror Y after read coordinates */
} flags;
/*!< User callback called after get coordinates from touch controller for apply user adjusting */
void (*process_coordinates)(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num);
/*!< User callback called after the touch interrupt occured */
esp_lcd_touch_interrupt_callback_t interrupt_callback;
} esp_lcd_touch_config_t;
typedef struct {
uint8_t points; /*!< Count of touch points saved */
struct {
uint16_t x; /*!< X coordinate */
uint16_t y; /*!< Y coordinate */
uint16_t strength; /*!< Strength */
} coords[CONFIG_ESP_LCD_TOUCH_MAX_POINTS];
#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0)
uint8_t buttons; /*!< Count of buttons states saved */
struct {
uint8_t status; /*!< Status of button */
} button[CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS];
#endif
portMUX_TYPE lock; /*!< Lock for read/write */
} esp_lcd_touch_data_t;
/**
* @brief Declare of Touch Type
*
*/
struct esp_lcd_touch_s {
/**
* @brief Read data from touch controller (mandatory)
*
* @note This function is usually blocking.
*
* @param tp: Touch handler
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*read_data)(esp_lcd_touch_handle_t tp);
/**
* @brief Get coordinates from touch controller (mandatory)
*
* @param tp: Touch handler
* @param x: Array of X coordinates
* @param y: Array of Y coordinates
* @param strength: Array of strengths
* @param point_num: Count of points touched (equals with count of items in x and y array)
* @param max_point_num: Maximum count of touched points to return (equals with max size of x and y array)
*
* @return
* - Returns true, when touched and coordinates readed. Otherwise returns false.
*/
bool (*get_xy)(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num);
#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0)
/**
* @brief Get button state (optional)
*
* @param tp: Touch handler
* @param n: Button index
* @param state: Button state
*
* @return
* - Returns true, when touched and coordinates readed. Otherwise returns false.
*/
esp_err_t (*get_button_state)(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state);
#endif
/**
* @brief Swap X and Y after read coordinates (optional)
* If set, then not used SW swapping.
*
* @param tp: Touch handler
* @param swap: Set swap value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*set_swap_xy)(esp_lcd_touch_handle_t tp, bool swap);
/**
* @brief Are X and Y coordinates swapped (optional)
*
* @param tp: Touch handler
* @param swap: Get swap value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*get_swap_xy)(esp_lcd_touch_handle_t tp, bool *swap);
/**
* @brief Mirror X after read coordinates
* If set, then not used SW mirroring.
*
* @param tp: Touch handler
* @param mirror: Set X mirror value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*set_mirror_x)(esp_lcd_touch_handle_t tp, bool mirror);
/**
* @brief Is mirrored X (optional)
*
* @param tp: Touch handler
* @param mirror: Get X mirror value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*get_mirror_x)(esp_lcd_touch_handle_t tp, bool *mirror);
/**
* @brief Mirror Y after read coordinates
* If set, then not used SW mirroring.
*
* @param tp: Touch handler
* @param mirror: Set Y mirror value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*set_mirror_y)(esp_lcd_touch_handle_t tp, bool mirror);
/**
* @brief Is mirrored Y (optional)
*
* @param tp: Touch handler
* @param mirror: Get Y mirror value
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*get_mirror_y)(esp_lcd_touch_handle_t tp, bool *mirror);
/**
* @brief Delete Touch
*
* @param tp: Touch handler
*
* @return
* - ESP_OK on success, otherwise returns ESP_ERR_xxx
*/
esp_err_t (*del)(esp_lcd_touch_handle_t tp);
/**
* @brief Configuration structure
*/
esp_lcd_touch_config_t config;
/**
* @brief Communication interface
*/
esp_lcd_panel_io_handle_t io;
/**
* @brief Data structure
*/
esp_lcd_touch_data_t data;
};
/**
* @brief Read data from touch controller
*
* @note This function is usually blocking.
*
* @param tp: Touch handler
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG parameter error
* - ESP_FAIL sending command error, slave hasn't ACK the transfer
* - ESP_ERR_INVALID_STATE I2C driver not installed or not in master mode
* - ESP_ERR_TIMEOUT operation timeout because the bus is busy
*/
esp_err_t esp_lcd_touch_read_data(esp_lcd_touch_handle_t tp);
/**
* @brief Read coordinates from touch controller
*
* @param tp: Touch handler
* @param x: Array of X coordinates
* @param y: Array of Y coordinates
* @param strength: Array of the strengths (can be NULL)
* @param point_num: Count of points touched (equals with count of items in x and y array)
* @param max_point_num: Maximum count of touched points to return (equals with max size of x and y array)
*
* @return
* - Returns true, when touched and coordinates readed. Otherwise returns false.
*/
bool esp_lcd_touch_get_coordinates(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num);
#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0)
/**
* @brief Get button state
*
* @param tp: Touch handler
* @param n: Button index
* @param state: Button state
*
* @return
* - ESP_OK on success
* - ESP_ERR_NOT_SUPPORTED if this function is not supported by controller
* - ESP_ERR_INVALID_ARG if bad button index
*/
esp_err_t esp_lcd_touch_get_button_state(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state);
#endif
/**
* @brief Swap X and Y after read coordinates
*
* @param tp: Touch handler
* @param swap: Set swap value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_set_swap_xy(esp_lcd_touch_handle_t tp, bool swap);
/**
* @brief Are X and Y coordinates swapped
*
* @param tp: Touch handler
* @param swap: Get swap value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_get_swap_xy(esp_lcd_touch_handle_t tp, bool *swap);
/**
* @brief Mirror X after read coordinates
*
* @param tp: Touch handler
* @param mirror: Set X mirror value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_set_mirror_x(esp_lcd_touch_handle_t tp, bool mirror);
/**
* @brief Is mirrored X
*
* @param tp: Touch handler
* @param mirror: Get X mirror value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_get_mirror_x(esp_lcd_touch_handle_t tp, bool *mirror);
/**
* @brief Mirror Y after read coordinates
*
* @param tp: Touch handler
* @param mirror: Set Y mirror value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_set_mirror_y(esp_lcd_touch_handle_t tp, bool mirror);
/**
* @brief Is mirrored Y
*
* @param tp: Touch handler
* @param mirror: Get Y mirror value
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_get_mirror_y(esp_lcd_touch_handle_t tp, bool *mirror);
/**
* @brief Delete touch (free all allocated memory and restart HW)
*
* @param tp: Touch handler
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_del(esp_lcd_touch_handle_t tp);
/**
* @brief Register user callback called after the touch interrupt occured
*
* @param tp: Touch handler
* @param callback: Interrupt callback
*
* @return
* - ESP_OK on success
*/
esp_err_t esp_lcd_touch_register_interrupt_callback(esp_lcd_touch_handle_t tp, esp_lcd_touch_interrupt_callback_t callback);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,284 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_check.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_touch.h"
static const char *TAG = "GT911";
/* GT911 registers */
#define ESP_LCD_TOUCH_GT911_READ_XY_REG (0x814E)
#define ESP_LCD_TOUCH_GT911_CONFIG_REG (0x8047)
#define ESP_LCD_TOUCH_GT911_PRODUCT_ID_REG (0x8140)
/*******************************************************************************
* Function definitions
*******************************************************************************/
static esp_err_t esp_lcd_touch_gt911_read_data(esp_lcd_touch_handle_t tp);
static bool esp_lcd_touch_gt911_get_xy(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num);
static esp_err_t esp_lcd_touch_gt911_del(esp_lcd_touch_handle_t tp);
/* I2C read/write */
static esp_err_t touch_gt911_i2c_read(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t *data, uint8_t len);
static esp_err_t touch_gt911_i2c_write(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t data);
/* GT911 reset */
static esp_err_t touch_gt911_reset(esp_lcd_touch_handle_t tp);
/* Read status and config register */
static esp_err_t touch_gt911_read_cfg(esp_lcd_touch_handle_t tp);
static void touch_scale(uint16_t *x_coordinate,uint16_t *y_coordinate);
/*******************************************************************************
* Public API functions
*******************************************************************************/
esp_err_t esp_lcd_touch_new_i2c_gt911(const esp_lcd_panel_io_handle_t io, const esp_lcd_touch_config_t *config, esp_lcd_touch_handle_t *out_touch)
{
esp_err_t ret = ESP_OK;
assert(io != NULL);
assert(config != NULL);
assert(out_touch != NULL);
/* Prepare main structure */
esp_lcd_touch_handle_t esp_lcd_touch_gt911 = heap_caps_calloc(1, sizeof(esp_lcd_touch_t), MALLOC_CAP_DEFAULT);
ESP_GOTO_ON_FALSE(esp_lcd_touch_gt911, ESP_ERR_NO_MEM, err, TAG, "no mem for GT911 controller");
/* Communication interface */
esp_lcd_touch_gt911->io = io;
/* Only supported callbacks are set */
esp_lcd_touch_gt911->read_data = esp_lcd_touch_gt911_read_data;
esp_lcd_touch_gt911->get_xy = esp_lcd_touch_gt911_get_xy;
esp_lcd_touch_gt911->del = esp_lcd_touch_gt911_del;
/* Mutex */
esp_lcd_touch_gt911->data.lock.owner = portMUX_FREE_VAL;
/* Save config */
memcpy(&esp_lcd_touch_gt911->config, config, sizeof(esp_lcd_touch_config_t));
/* Prepare pin for touch interrupt */
if (esp_lcd_touch_gt911->config.int_gpio_num != GPIO_NUM_NC) {
const gpio_config_t int_gpio_config = {
.mode = GPIO_MODE_INPUT,
.intr_type = GPIO_INTR_NEGEDGE,
.pin_bit_mask = BIT64(esp_lcd_touch_gt911->config.int_gpio_num)
};
ret = gpio_config(&int_gpio_config);
ESP_GOTO_ON_ERROR(ret, err, TAG, "GPIO config failed");
/* Register interrupt callback */
if (esp_lcd_touch_gt911->config.interrupt_callback) {
esp_lcd_touch_register_interrupt_callback(esp_lcd_touch_gt911, esp_lcd_touch_gt911->config.interrupt_callback);
}
}
/* Prepare pin for touch controller reset */
if (esp_lcd_touch_gt911->config.rst_gpio_num != GPIO_NUM_NC) {
const gpio_config_t rst_gpio_config = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = BIT64(esp_lcd_touch_gt911->config.rst_gpio_num)
};
ret = gpio_config(&rst_gpio_config);
ESP_GOTO_ON_ERROR(ret, err, TAG, "GPIO config failed");
}
/* Reset controller */
ret = touch_gt911_reset(esp_lcd_touch_gt911);
ESP_GOTO_ON_ERROR(ret, err, TAG, "GT911 reset failed");
/* Read status and config info */
ret = touch_gt911_read_cfg(esp_lcd_touch_gt911);
ESP_GOTO_ON_ERROR(ret, err, TAG, "GT911 init failed");
err:
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (0x%x)! Touch controller GT911 initialization failed!", ret);
if (esp_lcd_touch_gt911) {
esp_lcd_touch_gt911_del(esp_lcd_touch_gt911);
}
}
*out_touch = esp_lcd_touch_gt911;
return ret;
}
static esp_err_t esp_lcd_touch_gt911_read_data(esp_lcd_touch_handle_t tp)
{
esp_err_t err;
uint8_t buf[41];
uint8_t touch_cnt = 0;
uint8_t clear = 0;
size_t i = 0;
assert(tp != NULL);
err = touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, buf, 1);
ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!");
/* Any touch data? */
if ((buf[0] & 0x80) == 0x00) {
touch_gt911_i2c_write(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, clear);
} else {
/* Count of touched points */
touch_cnt = buf[0] & 0x0f;
if (touch_cnt > 5 || touch_cnt == 0) {
touch_gt911_i2c_write(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, clear);
return ESP_OK;
}
/* Read all points */
err = touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG + 1, &buf[1], touch_cnt * 8);
ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!");
/* Clear all */
err = touch_gt911_i2c_write(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, clear);
ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!");
portENTER_CRITICAL(&tp->data.lock);
/* Number of touched points */
touch_cnt = (touch_cnt > CONFIG_ESP_LCD_TOUCH_MAX_POINTS ? CONFIG_ESP_LCD_TOUCH_MAX_POINTS : touch_cnt);
tp->data.points = touch_cnt;
/* Fill all coordinates */
for (i = 0; i < touch_cnt; i++) {
tp->data.coords[i].x = ((uint16_t)buf[(i * 8) + 3] << 8) + buf[(i * 8) + 2];
tp->data.coords[i].y = (((uint16_t)buf[(i * 8) + 5] << 8) + buf[(i * 8) + 4]);
tp->data.coords[i].strength = (((uint16_t)buf[(i * 8) + 7] << 8) + buf[(i * 8) + 6]);
}
portEXIT_CRITICAL(&tp->data.lock);
}
return ESP_OK;
}
static bool esp_lcd_touch_gt911_get_xy(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num)
{
assert(tp != NULL);
assert(x != NULL);
assert(y != NULL);
assert(point_num != NULL);
assert(max_point_num > 0);
portENTER_CRITICAL(&tp->data.lock);
/* Count of points */
*point_num = (tp->data.points > max_point_num ? max_point_num : tp->data.points);
for (size_t i = 0; i < *point_num; i++) {
x[i] = tp->data.coords[i].x;
y[i] = tp->data.coords[i].y;
touch_scale(&x[i],&y[i]);
if (strength) {
strength[i] = tp->data.coords[i].strength;
}
}
/* Invalidate */
tp->data.points = 0;
portEXIT_CRITICAL(&tp->data.lock);
return (*point_num > 0);
}
static esp_err_t esp_lcd_touch_gt911_del(esp_lcd_touch_handle_t tp)
{
assert(tp != NULL);
/* Reset GPIO pin settings */
if (tp->config.int_gpio_num != GPIO_NUM_NC) {
gpio_reset_pin(tp->config.int_gpio_num);
}
/* Reset GPIO pin settings */
if (tp->config.rst_gpio_num != GPIO_NUM_NC) {
gpio_reset_pin(tp->config.rst_gpio_num);
}
free(tp);
return ESP_OK;
}
/*******************************************************************************
* Private API function
*******************************************************************************/
/* Reset controller */
static esp_err_t touch_gt911_reset(esp_lcd_touch_handle_t tp)
{
assert(tp != NULL);
if (tp->config.rst_gpio_num != GPIO_NUM_NC) {
ESP_RETURN_ON_ERROR(gpio_set_level(tp->config.rst_gpio_num, tp->config.levels.reset), TAG, "GPIO set level error!");
vTaskDelay(pdMS_TO_TICKS(10));
ESP_RETURN_ON_ERROR(gpio_set_level(tp->config.rst_gpio_num, !tp->config.levels.reset), TAG, "GPIO set level error!");
vTaskDelay(pdMS_TO_TICKS(10));
}
return ESP_OK;
}
static esp_err_t touch_gt911_read_cfg(esp_lcd_touch_handle_t tp)
{
uint8_t buf[4];
assert(tp != NULL);
ESP_RETURN_ON_ERROR(touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_PRODUCT_ID_REG, (uint8_t *)&buf[0], 3), TAG, "GT911 read error!");
ESP_RETURN_ON_ERROR(touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_CONFIG_REG, (uint8_t *)&buf[3], 1), TAG, "GT911 read error!");
ESP_LOGI(TAG, "TouchPad_ID:0x%02x,0x%02x,0x%02x", buf[0], buf[1], buf[2]);
ESP_LOGI(TAG, "TouchPad_Config_Version:%d", buf[3]);
return ESP_OK;
}
static esp_err_t touch_gt911_i2c_read(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t *data, uint8_t len)
{
assert(tp != NULL);
assert(data != NULL);
/* Read data */
return esp_lcd_panel_io_rx_param(tp->io, reg, data, len);
}
static esp_err_t touch_gt911_i2c_write(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t data)
{
assert(tp != NULL);
// *INDENT-OFF*
/* Write data */
return esp_lcd_panel_io_tx_param(tp->io, reg, (uint8_t[]){data}, 1);
// *INDENT-ON*
}
static void touch_scale(uint16_t *x_coordinate,uint16_t *y_coordinate)
{
uint32_t temp_x;
uint32_t temp_y;
temp_x = (*x_coordinate) * 1024 *10 / 800 / 10;
temp_y = (*y_coordinate) * 600 * 10 / 480 / 10;
*x_coordinate = (uint16_t)temp_x;
*y_coordinate = (uint16_t)temp_y;
}
@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file
* @brief ESP LCD touch: GT911
*/
#pragma once
#include "esp_lcd_touch.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Create a new GT911 touch driver
*
* @note The I2C communication should be initialized before use this function.
*
* @param io LCD/Touch panel IO handle
* @param config: Touch configuration
* @param out_touch: Touch instance handle
* @return
* - ESP_OK on success
* - ESP_ERR_NO_MEM if there is no memory for allocating main structure
*/
esp_err_t esp_lcd_touch_new_i2c_gt911(const esp_lcd_panel_io_handle_t io, const esp_lcd_touch_config_t *config, esp_lcd_touch_handle_t *out_touch);
/**
* @brief I2C address of the GT911 controller
*
*/
#define ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS (0x5D)
/**
* @brief Touch IO configuration structure
*
*/
#define ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG() \
{ \
.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS, \
.control_phase_bytes = 1, \
.dc_bit_offset = 0, \
.lcd_cmd_bits = 16, \
.flags = \
{ \
.disable_control_phase = 1, \
} \
}
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,99 @@
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_log.h"
#include "driver/i2c.h"
#include "esp_lcd_touch_gt911.h"
#include "gt911_touch.h"
#define CONFIG_LCD_HRES 600
#define CONFIG_LCD_VRES 1024
static const char *TAG = "example";
esp_lcd_touch_handle_t tp;
esp_lcd_panel_io_handle_t tp_io_handle;
uint16_t touch_strength[1];
uint8_t touch_cnt = 0;
gt911_touch::gt911_touch(int8_t sda_pin, int8_t scl_pin, int8_t rst_pin, int8_t int_pin)
{
_sda = sda_pin;
_scl = scl_pin;
_rst = rst_pin;
_int = int_pin;
}
void gt911_touch::begin()
{
i2c_config_t i2c_conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = (gpio_num_t)_sda,
.scl_io_num = (gpio_num_t)_scl,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
};
i2c_conf.master.clk_speed = 400000; // 400kHz
ESP_ERROR_CHECK(i2c_param_config(I2C_NUM_0, &i2c_conf));
ESP_ERROR_CHECK(i2c_driver_install(I2C_NUM_0, i2c_conf.mode, 0, 0, 0));
esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG();
ESP_LOGI(TAG, "Initialize touch IO (I2C)");
esp_lcd_new_panel_io_i2c((esp_lcd_i2c_bus_handle_t)I2C_NUM_0, &tp_io_config, &tp_io_handle);
esp_lcd_touch_config_t tp_cfg = {
.x_max = CONFIG_LCD_HRES,
.y_max = CONFIG_LCD_VRES,
.rst_gpio_num = (gpio_num_t)_rst,
.int_gpio_num = (gpio_num_t)_int,
.levels = {
.reset = 0,
.interrupt = 0,
},
.flags = {
.swap_xy = 0,
.mirror_x = 0,
.mirror_y = 0,
},
};
ESP_LOGI(TAG, "Initialize touch controller gt911");
ESP_ERROR_CHECK(esp_lcd_touch_new_i2c_gt911(tp_io_handle, &tp_cfg, &tp));
}
bool gt911_touch::getTouch(uint16_t *x, uint16_t *y)
{
esp_lcd_touch_read_data(tp);
bool touchpad_pressed = esp_lcd_touch_get_coordinates(tp, x, y, touch_strength, &touch_cnt, 1);
return touchpad_pressed;
}
void gt911_touch::set_rotation(uint8_t r){
switch(r){
case 0:
esp_lcd_touch_set_swap_xy(tp, false);
esp_lcd_touch_set_mirror_x(tp, false);
esp_lcd_touch_set_mirror_y(tp, false);
break;
case 1:
esp_lcd_touch_set_swap_xy(tp, false);
esp_lcd_touch_set_mirror_x(tp, true);
esp_lcd_touch_set_mirror_y(tp, true);
break;
case 2:
esp_lcd_touch_set_swap_xy(tp, false);
esp_lcd_touch_set_mirror_x(tp, false);
esp_lcd_touch_set_mirror_y(tp, false);
break;
case 3:
esp_lcd_touch_set_swap_xy(tp, false);
esp_lcd_touch_set_mirror_x(tp, true);
esp_lcd_touch_set_mirror_y(tp, true);
break;
}
}
@@ -0,0 +1,18 @@
#ifndef _GT911_TOUCH_H
#define _GT911_TOUCH_H
#include <stdio.h>
class gt911_touch
{
public:
gt911_touch(int8_t sda_pin, int8_t scl_pin, int8_t rst_pin = -1, int8_t int_pin = -1);
void begin();
bool getTouch(uint16_t *x, uint16_t *y);
void set_rotation(uint8_t r);
private:
int8_t _sda, _scl, _rst, _int;
};
#endif
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

@@ -0,0 +1,78 @@
JDEVB_RSOX(1024);
JDEVB_RSOY(600);
JDEVB_VS(2);
JDEVB_VBP(21);
JDEVB_VFP(12); //12->18(HKC),CKV_OFF(41->4B)_20230228
JDEVB_HS(24);
JDEVB_HBP(136);
JDEVB_HFP(160);
JDEVB_DOTCLK(51.2);
//------------------------------------------------//
//VCC=1.8V/3.3V, AVDD=11V, VGH=20V, VGL=-7, VCOM=3.6(~3.8)V
//1+2Dot inversion
//------------------------------------------------//
{0x30, 1,{0x00}},
{0xF7, 4,{0x49,0x61,0x02,0x00}},
{0x30, 1,{0x01}},
{0x04, 1,{0x0C}},
{0x05, 1,{0x00}}, //05=06(xhs)
{0x06, 1,{0x00}}, //06=80(xhs)
{0x0B, 1,{0x11}}, //0x13=4lanes0x12=3lanes0x11=2lanes0x10=1 lanes
{0x17, 1,{0x00}},
{0x20, 1,{0x04}}, //add //r_lansel_sel_reg=1, software charge lane must open
{0x1F, 1,{0x05}}, //add hs_settle time
{0x23, 1,{0x00}}, //add //close gas
{0x25, 1,{0x19}},
{0x28, 1,{0x18}},
{0x29, 1,{0x04}}, //revcom
{0x2A, 1,{0x01}}, //revcom
{0x2B, 1,{0x04}}, //vcom
{0x2C, 1,{0x01}}, //vcom
{0x30, 1,{0x02}},
{0x01, 1,{0x22}},
{0x03, 1,{0x12}},
{0x04, 1,{0x00}},
{0x05, 1,{0x64}},
{0x0A, 1,{0x08}},
{0x0B, 11,{0x0A,0x1A,0x0B,0x0D,0x0D,0x11,0x10,0x06,0x08,0x1F,0x1D}},
{0x0C, 11,{0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}},
{0x0D, 11,{0x16,0x1B,0x0B,0x0D,0x0D,0x11,0x10,0x07,0x09,0x1E,0x1C}},
{0x0E, 11,{0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}},
{0x0F, 11,{0x16,0x1B,0x0D,0x0B,0x0D,0x11,0x10,0x1C,0x1E,0x09,0x07}},
{0x10, 11,{0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}},
{0x11, 11,{0x0A,0x1A,0x0D,0x0B,0x0D,0x11,0x10,0x1D,0x1F,0x08,0x06}},
{0x12, 11,{0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D}},
{0x14, 4,{0x00,0x00,0x11,0x11}}, //CKV_OFF
{0x18, 1,{0x99}},
{0x30, 1,{0x06}},
{0x12, 14,{0x36,0x2C,0x2E,0x3C,0x38,0x35,0x35,0x32,0x2E,0x1D,0x2B,0x21,0x16,0x29}},
{0x13, 14,{0x36,0x2C,0x2E,0x3C,0x38,0x35,0x35,0x32,0x2E,0x1D,0x2B,0x21,0x16,0x29}},
//{0x30, 1,{0x08}}, //RGB&LVDS add
//{0x05, 1,{0x01}}, //RGB&LVDS add
//{0x0C, 1,{0x1A}}, //RGB&LVDS add
//{0x0D, 1,{0x0E}}, //RGB&LVDS add
//{0x30, 1,{0x07}},
//{0x01, 1,{0x04}}, //P7_r01=0x04(0x06)_r_hster_delay
{0x30, 1,{0x0A}},
{0x02, 1,{0x4F}},
{0x0B, 1,{0x40}},
{0x12, 1,{0x3E}},
{0x13, 1,{0x78}},
{0x30, 1,{0x0D}},
{0x0D, 1,{0x04}}, //0x0C, 0x04
{0x10, 1,{0x0C}},
{0x11, 1,{0x0C}},
{0x12, 1,{0x0C}},
{0x13, 1,{0x0C}},
{0x30, 1,{0x00}},
{0x11,0,{}},
{REGFLAG_DELAY, 120, {}},
{0x29,0,{}},
{REGFLAG_DELAY, 20, {}},
{REGFLAG_END_OF_TABLE, 0x00, {}}
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,3 @@
1、电脑需要安装CH340驱动程序:The computer needs to install the CH340 driver program
2、保证供电充足,开发板电流600ma以上:Ensure sufficient power supply, with a development board current of over 600mA
3、烧录选择USB1口:Choose USB1 port for burning