touch: beta_47 — Discover app (find/list/map nearby nodes + wardriving), Attaky release wiring, P4 LCD

- New Discover app: active NODE_DISCOVER sweep -> signal-ranked nearby list, tap-to-add-contact, GPS wardriving log (SD CSV) + signal-coloured coverage overlay on the map. Board-agnostic.
- T-Display P4 TFT-LCD (HI8561) variant support (WADA_P4_LCD build; AMOLED bin untouched).
- Wire the Attaky Core board (#158, @attakygit) into the release matrix + web flasher.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kaj Schittecat
2026-07-19 17:45:05 +02:00
co-authored by Claude Opus 4.8
parent cab0c81208
commit 99423e5bf8
16 changed files with 1442 additions and 30 deletions
+5
View File
@@ -391,6 +391,11 @@
<esp-web-install-button id="inst-v4r8" manifest="https://firmware.wadamesh.com/latest-beta/manifest-heltec-v4-r8-tft.json"></esp-web-install-button>
<a class="dl2" data-dl-board="heltec-v4-r8-tft" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-heltec-v4-r8-tft-merged.bin"><span class="dl2-row"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</span><span class="dlver" data-dl-ver></span></a>
<p data-wm-betaonly style="color:var(--mut);font-size:.78rem;margin:6px 0 0">New board, not yet hardware-tested &mdash; beta only. Please flash with care and report back.</p></div>
<div class="board"><h4>Attaky Core <span style="font-weight:600;color:var(--mut)">(experimental)</span></h4><p>ESP32-S3 &middot; 16&nbsp;MB &middot; 8&nbsp;MB PSRAM.</p>
<span class="sup part" tabindex="0"><span class="dot"></span>Partially supported<span class="pop"><b>Partially supported</b>The core works: LoRa mesh communication, chat, contacts, channels and the phone-app link. This community-contributed board (PR #158) isn't hardware-verified by us yet — flash with care and report back.</span></span>
<esp-web-install-button id="inst-attaky" manifest="https://firmware.wadamesh.com/latest-beta/manifest-attaky.json"></esp-web-install-button>
<a class="dl2" data-dl-board="attaky" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-attaky-merged.bin"><span class="dl2-row"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</span><span class="dlver" data-dl-ver></span></a>
<p data-wm-betaonly style="color:var(--mut);font-size:.78rem;margin:6px 0 0">New board, not yet hardware-tested &mdash; beta only. Please flash with care and report back.</p></div>
<div class="board"><h4>Tanmatsu</h4><p>ESP32-P4 keyboard handheld.</p>
<span class="sup part" tabindex="0"><span class="dot"></span>Partially supported<span class="pop"><b>Partially supported</b>The core works: LoRa mesh communication, chat, contacts, channels and the phone-app link over Wi-Fi/USB. Some features are still settling on this board (Bluetooth pairing has known gaps).</span></span>
<p style="color:var(--mut);font-size:.84rem;margin:0;line-height:1.5">Already have one? No USB flashing needed — install WADAMESH straight from the <b>Tanmatsu app store</b> on the device.</p>
+17
View File
@@ -0,0 +1,17 @@
# beta_47 — Discover app (find + map nearby nodes, wardriving), a new Attaky board, pager + P4 additions
# One user-facing note per non-blank, non-# line; # lines are section comments.
# New
New Discover app: actively sweep for nearby nodes and list everything that answers — repeaters, companions and room servers — ranked by signal, with live RSSI/SNR, a "direct" badge for anything in RF range, and a last-heard age. It's in the app drawer (the ⟳ tile, next to Map).
Tap any node in Discover to add it straight to your contacts — it keeps the right type (repeater, companion or room).
Wardriving: with a GPS fix, Discover logs every node it hears against your position to a CSV on the SD card as you move, and plots your RF coverage on the map as a signal-coloured track (green = strong, red = weak) — a standalone, on-device coverage mapper, no phone needed.
# New boards
Attaky Core (ESP32-S3, 16 MB flash / 8 MB PSRAM): a new board, contributed by @attakygit (#158). Experimental — on the beta channel while it finishes hardware verification.
LilyGo T-Display P4 (TFT-LCD screen variant): early support for the LCD screen model (the AMOLED variant was already supported). Experimental — if you have the LCD screen, grab the wadamesh-tdisplay-p4-lcd build from the release assets.
# Fixed
LilyGo T-LoRa Pager: the rotary encoder now keeps its focus inside the chat message list, and the composer navigation keys are remapped so moving around and typing a message no longer fight each other. (community contribution by @codemonkeybr, #162)
# Boards
T-Deck · Heltec V4 TFT · Heltec V4-R8 · ThinkNode M9 · RAK TAP v2 · T-LoRa Pager (LR1121 / SX1262) · Attaky Core · T-Display P4.
+1
View File
@@ -42,6 +42,7 @@ BOARDS = {
"manifest-heltec-v4-r8-tft.json": ("wadamesh — Heltec V4-R8 (experimental)", "wadamesh-heltec-v4-r8-tft-merged.bin", "ESP32-S3"),
"manifest-tlora-pager-lr1121.json": ("wadamesh — LilyGo T-LoRa Pager (LR1121)", "wadamesh-tlora-pager-lr1121-merged.bin", "ESP32-S3"),
"manifest-tlora-pager-sx1262.json": ("wadamesh — LilyGo T-LoRa Pager (SX1262)", "wadamesh-tlora-pager-sx1262-merged.bin", "ESP32-S3"),
"manifest-attaky.json": ("wadamesh — Attaky Core (experimental)", "wadamesh-attaky-merged.bin", "ESP32-S3"),
"manifest-tdisplay-p4.json": ("wadamesh — LilyGo T-Display P4", "wadamesh-tdisplay-p4-merged.bin", "ESP32-P4"),
}
for fn, (name, binf, chip) in BOARDS.items():
+1 -1
View File
@@ -29,7 +29,7 @@ DEST="${WADAMESH_VPS:-}"; DEST_PATH="${WADAMESH_VPS_PATH:-/srv/wadamesh/firmware
# env:binname pairs — plain string form (works on macOS's bash 3.2; no associative arrays).
# All S3/PIO boards. The T-Display P4 is an ESP32-P4 IDF build (tdisplay_p4/) handled OUT of band
# — see the release skill / the P4 build+merge step — because this loop is PlatformIO-only.
ENVS="heltec_v4_tft_companion_radio_usb_tcp_touch:wadamesh-heltec-v4-tft LilyGo_TDeck_companion_radio_touch:wadamesh-tdeck ThinkNode_M9_companion_radio_touch:wadamesh-thinknode-m9 rak_tap_v2_companion_radio_touch:wadamesh-rak-tap-v2 heltec_v4_r8_tft_companion_radio_usb_tcp_touch:wadamesh-heltec-v4-r8-tft tlora_pager_lr1121_companion_radio_touch:wadamesh-tlora-pager-lr1121 tlora_pager_sx1262_companion_radio_touch:wadamesh-tlora-pager-sx1262"
ENVS="heltec_v4_tft_companion_radio_usb_tcp_touch:wadamesh-heltec-v4-tft LilyGo_TDeck_companion_radio_touch:wadamesh-tdeck ThinkNode_M9_companion_radio_touch:wadamesh-thinknode-m9 rak_tap_v2_companion_radio_touch:wadamesh-rak-tap-v2 heltec_v4_r8_tft_companion_radio_usb_tcp_touch:wadamesh-heltec-v4-r8-tft tlora_pager_lr1121_companion_radio_touch:wadamesh-tlora-pager-lr1121 tlora_pager_sx1262_companion_radio_touch:wadamesh-tlora-pager-sx1262 attaky_mesh_series_companion_radio_touch:wadamesh-attaky"
# Per-channel destination paths.
if [ "$MODE" = "stable" ]; then
+48
View File
@@ -2401,6 +2401,22 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) {
_ui_sig_ms = millis();
}
}
// Discover scan (Discover app): collect EVERY NODE_DISCOVER_RESP matching the active discover
// sweep tag into _discover[], keyed by responder pubkey. Additive to the single-scalar probe
// capture above (they use different tags). RESP payload (MeshCore docs/payloads.md):
// [0]=0x9<<4|node_type [1]=their SNR*4 (reverse link) [2..5]=tag [6..]=pubkey (8 or 32 bytes).
if (packet->payload_len >= 6 + 8 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP
&& _discover_tag != 0) {
uint32_t rtag; memcpy(&rtag, &packet->payload[2], 4);
if (rtag == _discover_tag) {
uint8_t node_type = packet->payload[0] & 0x0F;
int8_t their_snr_q4 = (int8_t)packet->payload[1];
uint8_t pklen = (packet->payload_len >= 6 + 32) ? 32 : 8;
int8_t our_snr_q4 = (int8_t)(packet->getSNR() * 4.0f);
int8_t our_rssi = (int8_t)_radio->getLastRSSI();
discoverUpsert(&packet->payload[6], pklen, node_type, our_snr_q4, our_rssi, their_snr_q4, packet->path_len);
}
}
if (packet->payload_len + 4 > sizeof(out_frame)) {
MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len);
return;
@@ -2420,6 +2436,38 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) {
}
}
// Upsert a discover responder into _discover[] (keyed by the 8-byte pubkey prefix). New nodes
// append; a full table evicts the least-recently-heard. Both link directions + hop count are
// refreshed on every reply. See DiscoverHit / uiStartDiscoverScan in MyMesh.h.
void MyMesh::discoverUpsert(const uint8_t* pk, uint8_t pklen, uint8_t node_type,
int8_t our_snr_q4, int8_t our_rssi, int8_t their_snr_q4, uint8_t path_len) {
uint32_t now = millis();
int slot = -1;
for (uint8_t i = 0; i < _discover_cnt; i++) {
if (memcmp(_discover[i].pubkey, pk, 8) == 0) { slot = i; break; }
}
if (slot < 0) {
if (_discover_cnt < DISCOVER_MAX) {
slot = _discover_cnt++;
} else { // table full -> evict the least-recently-heard
uint32_t oldest = 0xFFFFFFFFu; slot = 0;
for (uint8_t i = 0; i < _discover_cnt; i++)
if (_discover[i].last_ms < oldest) { oldest = _discover[i].last_ms; slot = i; }
}
memset(&_discover[slot], 0, sizeof(DiscoverHit));
memcpy(_discover[slot].pubkey, pk, pklen > 32 ? 32 : pklen);
_discover[slot].first_ms = now;
}
DiscoverHit& h = _discover[slot];
h.node_type = node_type;
h.our_snr_q4 = our_snr_q4;
h.our_rssi = our_rssi;
h.their_snr_q4 = their_snr_q4;
h.path_len = path_len;
h.last_ms = now;
if (h.heard < 0xFFFF) h.heard++;
}
void MyMesh::onRawDataRecv(mesh::Packet *packet) {
if (packet->payload_len + 4 > sizeof(out_frame)) {
MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len);
+74
View File
@@ -481,6 +481,36 @@ public:
int8_t uiSignalRssi() const { return _ui_sig_rssi; }
uint32_t uiSignalMs() const { return _ui_sig_ms; }
// ---- Discover scan (the Discover app: active node-discovery, ALL node types) ----
// uiStartDiscoverScan() broadcasts a zero-hop NODE_DISCOVER_REQ with a type filter + a fresh
// random tag; EVERY neighbour that answers with a NODE_DISCOVER_RESP is upserted here (keyed by
// pubkey prefix) from onControlDataRecv. Unlike the single-scalar signal probe, every responder
// is kept, with BOTH link directions (our RX of them + their RX of us, from RESP payload[1]).
struct DiscoverHit {
uint8_t pubkey[32]; // responder identity (full key — the REQ sets prefix_only=0)
uint8_t node_type; // ADV_TYPE_* (RESP payload[0] low nibble): repeater/chat/room/sensor
int8_t our_snr_q4; // our RX SNR*4 of their reply (forward link)
int8_t our_rssi; // our RX RSSI dBm of their reply
int8_t their_snr_q4; // their RX SNR*4 of our request (reverse link — RESP payload[1])
uint8_t path_len; // hops the reply travelled (0 = heard directly, i.e. in RF range)
uint32_t first_ms; // millis() first heard this session
uint32_t last_ms; // millis() last heard
uint16_t heard; // reply count
};
static const int DISCOVER_MAX = 64;
DiscoverHit _discover[DISCOVER_MAX];
uint8_t _discover_cnt = 0;
uint32_t _discover_tag = 0; // active scan tag (matches RESPs; 0 = no scan yet)
uint32_t _discover_scan_ms = 0; // millis() the current scan sweep was fired
uint8_t discoverCount() const { return _discover_cnt; }
bool discoverGet(uint8_t i, DiscoverHit& out) const {
if (i >= _discover_cnt) return false; out = _discover[i]; return true;
}
void discoverClear() { _discover_cnt = 0; }
uint32_t discoverScanMs() const { return _discover_scan_ms; }
void discoverUpsert(const uint8_t* pk, uint8_t pklen, uint8_t node_type,
int8_t our_snr_q4, int8_t our_rssi, int8_t their_snr_q4, uint8_t path_len);
// ---- Recent-RX ring (RF Monitor app) ----
// One record per received frame, captured in logRxRaw(): payload type / route
// / hop count / length + signal, so the Monitor page can show a live "what am
@@ -711,6 +741,29 @@ public:
return _ui_sig_probe_tag;
}
/** DISCOVER SCAN (Discover app): like uiSendSignalProbe, but asks ALL node types and KEEPS
* every responder (see _discover[] + discoverUpsert, populated in onControlDataRecv). Broadcasts
* one zero-hop NODE_DISCOVER_REQ; neighbours reply DIRECTLY (never floods). type_filter = OR of
* (1<<ADV_TYPE_*); pass 0 for "all types". Returns the scan tag (0 = failed). The caller should
* airtime-gate repeated sweeps (Dispatcher::getRemainingTxBudget). */
uint32_t uiStartDiscoverScan(uint8_t type_filter = 0) {
uint8_t data[10];
data[0] = CTL_TYPE_NODE_DISCOVER_REQ; // 0x80; low bit prefix_only=0 -> full 32-byte pubkeys
data[1] = type_filter ? type_filter
: (uint8_t)((1 << ADV_TYPE_CHAT) | (1 << ADV_TYPE_REPEATER) |
(1 << ADV_TYPE_ROOM) | (1 << ADV_TYPE_SENSOR)); // all types
getRNG()->random(&data[2], 4); // fresh random tag to match this sweep's replies
memcpy(&_discover_tag, &data[2], 4);
if (_discover_tag == 0) { _discover_tag = 1; memcpy(&data[2], &_discover_tag, 4); }
uint32_t since = 0; // 0 = answer regardless of freshness
memcpy(&data[6], &since, 4);
mesh::Packet* pkt = createControlData(data, sizeof(data));
if (!pkt) { _discover_tag = 0; return 0; }
sendZeroHop(pkt);
_discover_scan_ms = millis();
return _discover_tag;
}
/** Request CayenneLPP telemetry from a remote contact. Reply is delivered
* via AbstractUITask::onTelemetryReply with the raw LPP payload after the
* 4-byte timestamp header. Falls back to onPingReply if the UI didn't
@@ -831,6 +884,27 @@ public:
return true;
}
/** Add a node found by the Discover app (active NODE_DISCOVER sweep) to contacts, from its full
* 32-byte pubkey + node TYPE + a display name. Like uiAddManualContact but preserves the type
* (repeater/room/sensor/chat) so the contact lands with the right role/icon the discovery
* response carries the type + full pubkey but no advert, so name is a placeholder until one
* arrives. Returns false if it's already a contact, the name is empty, or the table is full. */
bool uiAddDiscoveredContact(const uint8_t pub_key[32], uint8_t type, const char* name) {
if (!name || !name[0]) return false;
if (lookupContactByPubKey(pub_key, PUB_KEY_SIZE) != nullptr) return false;
ContactInfo ci{};
memcpy(ci.id.pub_key, pub_key, PUB_KEY_SIZE);
ci.type = type;
ci.out_path_len = OUT_PATH_UNKNOWN;
ci.last_advert_timestamp = 0; // unknown — we only heard a discovery reply
ci.lastmod = getRTCClock()->getCurrentTime();
StrHelper::strncpy(ci.name, name, sizeof(ci.name));
if (!addContact(ci)) return false;
saveContacts();
if (_ui) _ui->onThreadsChanged();
return true;
}
/** Persist the in-RAM contact table to flash (/contacts3). Public wrapper so
* UI paths that insert via the base addContact() e.g. the Discovered-list
* "Add to contacts" button can persist; otherwise that contact is RAM-only
+390 -7
View File
@@ -134,7 +134,11 @@
#elif defined(HAS_RAK_TAP_V2)
#include <LGFXDisplay.h> // LovyanGFX FSPI on RAK Tap V2
#elif defined(HAS_TDISPLAY_P4)
#include <RM69A10Display.h> // RM69A10 MIPI-DSI on the T-Display P4
#if defined(HAS_TDP4_LCD)
#include <HI8561Display.h> // HI8561 TFT-LCD (LCD SKU) on the T-Display P4
#else
#include <RM69A10Display.h> // RM69A10 MIPI-DSI AMOLED (default SKU) on the T-Display P4
#endif
#else
#include <helpers/ui/ST7789LCDDisplay.h>
#endif
@@ -182,7 +186,7 @@
#elif defined(HAS_RAK_TAP_V2) || defined(HELTEC_LORA_V4_R8)
extern LGFXDisplay display;
#elif defined(HAS_TDISPLAY_P4)
extern RM69A10Display display;
extern DISPLAY_CLASS display; // RM69A10Display (AMOLED) or HI8561Display (LCD) — set in CMakeLists
#else
extern ST7789LCDDisplay display;
#endif
@@ -20353,6 +20357,316 @@ static void openReaderPage() {
}
#endif // Reader
// ===== Discover app (active node-discovery + live signal-ranked list) =====================
// Fires a zero-hop NODE_DISCOVER_REQ sweep (ALL node types) every few seconds while open and lists
// every node that answers, ranked by signal. The engine (MyMesh::_discover[] + uiStartDiscoverScan
// + discoverUpsert) collects the replies; this page renders them + drives the cadence, airtime-gated
// so a repeating scan respects the duty-cycle budget. Rendered as ONE recolored label (like the RF
// Monitor feed) — light + scroll-safe, no per-row object churn. Map + wardriving build on top later.
static lv_obj_t* s_discover_root = nullptr;
static lv_obj_t* s_discover_feed = nullptr; // single recolored multi-line label
static lv_obj_t* s_discover_status = nullptr; // header: "Scanning… · N nearby"
static lv_obj_t* s_discover_btn_lbl = nullptr; // Scan/Stop button label
static lv_timer_t* s_discover_timer = nullptr;
static bool s_discover_scanning = true;
static uint32_t s_discover_last_scan_ms = 0;
static void closeDiscoverPage();
static void discoverJumpToMapHere(); // "Show on map" — defined with the map code (uses map statics)
// node_type -> row colour / short label (mirrors the map marker colours).
static uint32_t discTypeColor(uint8_t t) {
switch (t) {
case ADV_TYPE_REPEATER: return 0x4DA8FF; // blue
case ADV_TYPE_CHAT: return 0x53C06B; // green
case ADV_TYPE_ROOM: return 0xF0A020; // amber
case ADV_TYPE_SENSOR: return 0x35C9C9; // cyan
default: return 0x9AA0A6; // grey
}
}
static const char* discTypeName(uint8_t t) {
switch (t) {
case ADV_TYPE_REPEATER: return "repeater";
case ADV_TYPE_CHAT: return "companion";
case ADV_TYPE_ROOM: return "room";
case ADV_TYPE_SENSOR: return "sensor";
default: return "node";
}
}
// ---- Wardriving: log GPS-tagged sightings as we scan (a standalone, on-device MeshMapper) ----
// While the Discover app scans, sample our GPS position; at each sample, log every currently-heard
// node (our position + its type + signal) to an SD CSV, and keep an in-RAM coverage ring for the map
// overlay. A 0-hop hit means "reachable from HERE", so the track of samples IS a personal RF-coverage
// map. All UI-side (needs GPS, which the engine doesn't have). Map rendering reads s_disc_track[].
struct DiscTrackPt { int32_t lat_e6, lon_e6; int8_t best_rssi; uint8_t node_count; };
static const int k_disc_track_max = 160;
static DiscTrackPt* s_disc_track = (DiscTrackPt*)psAlloc(sizeof(DiscTrackPt) * k_disc_track_max); // PSRAM (~1KB off .bss)
static int s_disc_track_n = 0; // total samples ever (ring slot = n % max)
static int32_t s_disc_last_lat = 0, s_disc_last_lon = 0;
static uint32_t s_disc_last_sample_ms = 0;
static uint32_t s_disc_log_count = 0; // sightings written to SD this session
static lv_obj_t* s_disc_footer = nullptr; // bottom status line: GPS / wardrive state
static void discoverLogSighting(int32_t lat_e6, int32_t lon_e6, const MyMesh::DiscoverHit& h) {
#if CAP_SD
if (SD.cardType() == CARD_NONE) return;
markSdIo();
SD.mkdir("/meshcomod");
SD.mkdir("/meshcomod/discover");
File f = SD.open("/meshcomod/discover/wardrive.csv", FILE_APPEND);
if (!f) return;
if (f.size() == 0) f.print("epoch,lat,lon,type,pubkey,rssi,snr,hops\n");
uint32_t epoch = (uint32_t)rtc_clock.getCurrentTime();
f.printf("%lu,%.6f,%.6f,%s,%02X%02X%02X%02X,%d,%.1f,%u\n",
(unsigned long)epoch, (double)lat_e6 / 1e6, (double)lon_e6 / 1e6, discTypeName(h.node_type),
h.pubkey[0], h.pubkey[1], h.pubkey[2], h.pubkey[3],
(int)h.our_rssi, (double)h.our_snr_q4 / 4.0, (unsigned)h.path_len);
f.close();
s_disc_log_count++;
#else
(void)lat_e6; (void)lon_e6; (void)h;
#endif
}
// Called every scan tick: refresh the footer + (once we've moved ~15 m) log a coverage sample.
static void discoverWardriveTick() {
UITask* task = g_lv.task;
const bool fix = task && task->getGpsFix();
const double latd = fix ? task->getNodeLat() : 0.0, lond = fix ? task->getNodeLon() : 0.0;
if (fix && !(latd == 0.0 && lond == 0.0)) {
const int32_t lat_e6 = (int32_t)lround(latd * 1e6), lon_e6 = (int32_t)lround(lond * 1e6);
const uint32_t now = millis();
int32_t dlat = lat_e6 - s_disc_last_lat; if (dlat < 0) dlat = -dlat;
int32_t dlon = lon_e6 - s_disc_last_lon; if (dlon < 0) dlon = -dlon;
if (s_disc_last_sample_ms == 0 || dlat > 150 || dlon > 150 || (now - s_disc_last_sample_ms) > 20000) {
int nodes = 0; int8_t best = -128;
uint8_t n = the_mesh.discoverCount();
for (uint8_t i = 0; i < n; i++) {
MyMesh::DiscoverHit h;
if (!the_mesh.discoverGet(i, h) || (now - h.last_ms) > 8000) continue; // only currently-heard
nodes++; if (h.our_rssi > best) best = h.our_rssi;
discoverLogSighting(lat_e6, lon_e6, h);
}
if (nodes > 0) {
DiscTrackPt& p = s_disc_track[s_disc_track_n % k_disc_track_max];
p.lat_e6 = lat_e6; p.lon_e6 = lon_e6; p.best_rssi = best;
p.node_count = (uint8_t)(nodes > 255 ? 255 : nodes);
s_disc_track_n++;
}
s_disc_last_lat = lat_e6; s_disc_last_lon = lon_e6; s_disc_last_sample_ms = now;
}
}
if (s_disc_footer) {
if (!fix) lv_label_set_text(s_disc_footer, "#7A7F87 Wardrive: waiting for GPS fix\xE2\x80\xA6#");
else lv_label_set_text_fmt(s_disc_footer,
"#7A7F87 Wardrive: %d coverage pts \xC2\xB7 %lu logged to SD#",
s_disc_track_n > k_disc_track_max ? k_disc_track_max : s_disc_track_n,
(unsigned long)s_disc_log_count);
}
}
// Per-row snapshot for tap-to-add (populated by discoverBuildFeed in the displayed sort order):
// full pubkey + type + name, so a tap on the feed resolves to the exact node under it.
static const int DISC_MAXROWS = 40;
static uint8_t s_disc_row_key[DISC_MAXROWS][32];
static uint8_t s_disc_row_type[DISC_MAXROWS];
static char s_disc_row_name[DISC_MAXROWS][26];
static int s_disc_row_n = 0;
// Rebuild the feed label from the engine's _discover[] table, strongest-signal first.
static void discoverBuildFeed() {
if (!s_discover_feed) return;
const uint32_t now = millis();
static MyMesh::DiscoverHit hits[MyMesh::DISCOVER_MAX];
static uint8_t idx[MyMesh::DISCOVER_MAX];
uint8_t m = 0;
uint8_t n = the_mesh.discoverCount();
for (uint8_t i = 0; i < n && m < MyMesh::DISCOVER_MAX; i++) {
if (the_mesh.discoverGet(i, hits[m])) { idx[m] = m; m++; }
}
for (uint8_t a = 0; a < m; a++) // insertion sort by our_rssi desc
for (uint8_t b = (uint8_t)(a + 1); b < m; b++)
if (hits[idx[b]].our_rssi > hits[idx[a]].our_rssi) { uint8_t t = idx[a]; idx[a] = idx[b]; idx[b] = t; }
static char buf[3200];
int q = 0, rpt = 0, comp = 0;
const int MAXROWS = DISC_MAXROWS;
s_disc_row_n = 0;
for (uint8_t k = 0; k < m && k < MAXROWS && q < (int)sizeof(buf) - 112; k++) {
const MyMesh::DiscoverHit& h = hits[idx[k]];
if (h.node_type == ADV_TYPE_REPEATER) rpt++;
else if (h.node_type == ADV_TYPE_CHAT) comp++;
char name[26];
ContactInfo* c = the_mesh.lookupContactByPubKey((uint8_t*)h.pubkey, 6);
if (c && c->name[0]) snprintf(name, sizeof name, "%s", c->name);
else snprintf(name, sizeof name, "Node \xC2\xB7%02X%02X", h.pubkey[0], h.pubkey[1]);
memcpy(s_disc_row_key[k], h.pubkey, 32); // snapshot this row for tap-to-add
s_disc_row_type[k] = h.node_type;
snprintf(s_disc_row_name[k], sizeof s_disc_row_name[k], "%s", name);
s_disc_row_n = k + 1;
uint32_t age = (now - h.last_ms) / 1000;
char ago[10];
if (age < 60) snprintf(ago, sizeof ago, "%us", (unsigned)age);
else snprintf(ago, sizeof ago, "%um", (unsigned)(age / 60));
const char* direct = (h.path_len == 0) ? " #53C06B direct#" : "";
q += snprintf(buf + q, sizeof(buf) - q,
"#%06X %s# %s %ddBm %.1f%s \xC2\xB7 %s\n",
(unsigned)discTypeColor(h.node_type), name, discTypeName(h.node_type),
(int)h.our_rssi, (double)h.our_snr_q4 / 4.0, direct, ago);
}
if (q == 0) snprintf(buf, sizeof buf, "#7A7F87 Scanning\xE2\x80\xA6 nothing has answered yet#");
else if (buf[q - 1] == '\n') buf[q - 1] = '\0';
lv_label_set_text(s_discover_feed, buf);
if (s_discover_status)
lv_label_set_text_fmt(s_discover_status, "%s \xC2\xB7 %d nearby (%d rpt, %d comp)",
s_discover_scanning ? "Scanning\xE2\x80\xA6" : "Paused", (int)m, rpt, comp);
}
static void discoverTimerCb(lv_timer_t* t) {
(void)t;
if (!s_discover_root) return;
const uint32_t now = millis();
if (s_discover_scanning && (s_discover_last_scan_ms == 0 || (now - s_discover_last_scan_ms) >= 4000)) {
if (the_mesh.getRemainingTxBudget() >= 300) { // airtime gate: keep a duty-cycle cushion
the_mesh.uiStartDiscoverScan(0); // 0 = all node types
s_discover_last_scan_ms = now;
}
}
discoverBuildFeed();
discoverWardriveTick();
}
static void discoverScanToggleCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
s_discover_scanning = !s_discover_scanning;
if (s_discover_scanning) s_discover_last_scan_ms = 0; // resume -> sweep immediately
if (s_discover_btn_lbl) lv_label_set_text(s_discover_btn_lbl, s_discover_scanning ? "Stop" : "Scan");
discoverBuildFeed();
}
static void discoverShowMapCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
discoverJumpToMapHere(); // centre the map on us + open the Map tab (coverage overlay draws there)
}
// Tap a row in the feed -> add that node to contacts (using the full pubkey the discovery reply
// carried). Resolves the row from the tap's Y (uniform one-line rows: LONG_CLIP + a fixed line_space).
static void discoverFeedTapCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
if (!s_discover_feed || s_disc_row_n == 0) return;
lv_indev_t* indev = lv_indev_get_act();
if (!indev) return;
lv_point_t p; lv_indev_get_point(indev, &p);
lv_area_t a; lv_obj_get_coords(s_discover_feed, &a);
const int local_y = (int)p.y - (int)a.y1;
if (local_y < 0) return;
const int row_h = (int)lv_font_get_line_height(&g_font_12) + 3; // == the label's line_space
const int row = local_y / row_h;
if (row < 0 || row >= s_disc_row_n) return; // tapped empty space below the list
const uint8_t* pk = s_disc_row_key[row];
if (the_mesh.lookupContactByPubKey((uint8_t*)pk, PUB_KEY_SIZE) != nullptr) {
if (g_lv.task) g_lv.task->showAlert(TR("Already in contacts"), 1100);
return;
}
if (the_mesh.uiAddDiscoveredContact(pk, s_disc_row_type[row], s_disc_row_name[row])) {
if (g_lv.task) { char msg[44]; snprintf(msg, sizeof msg, "%s %s", TR("Added"), s_disc_row_name[row]); g_lv.task->showAlert(msg, 1400); }
} else if (g_lv.task) {
g_lv.task->showAlert(TR("Could not add"), 1100);
}
}
static void closeDiscoverPage() {
if (s_discover_timer) { lv_timer_del(s_discover_timer); s_discover_timer = nullptr; }
if (s_discover_root) { popupClose(&s_discover_root); }
s_discover_feed = s_discover_status = s_discover_btn_lbl = s_disc_footer = nullptr;
s_disc_row_n = 0;
s_discover_scanning = true;
if (s_apppage_close == closeDiscoverPage) {
s_apppage_title = nullptr; s_apppage_close = nullptr;
statusBarSetTall(false); updateGlobalStatusBar();
}
}
static void openDiscoverPage() {
closeDiscoverPage();
const lv_coord_t sw = lv_disp_get_hor_res(nullptr);
const lv_coord_t sh = lv_disp_get_ver_res(nullptr);
const int H = sh - STATUSBAR_H;
s_discover_root = lv_obj_create(lv_layer_top());
lv_obj_remove_style_all(s_discover_root);
lv_obj_set_size(s_discover_root, sw, H);
lv_obj_set_pos(s_discover_root, 0, STATUSBAR_H);
lv_obj_set_style_bg_color(s_discover_root, lv_color_hex(COLOR_BG), LV_PART_MAIN);
lv_obj_set_style_bg_opa(s_discover_root, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_clear_flag(s_discover_root, LV_OBJ_FLAG_SCROLLABLE);
s_apppage_title = "Discover";
s_apppage_close = closeDiscoverPage;
statusBarSetTall(true);
updateGlobalStatusBar();
const int top = STATUSBAR_H + 8;
s_discover_status = lv_label_create(s_discover_root);
lv_label_set_text(s_discover_status, "Scanning\xE2\x80\xA6");
lv_obj_set_style_text_font(s_discover_status, &g_font_12, LV_PART_MAIN);
lv_obj_set_style_text_color(s_discover_status, lv_color_hex(COLOR_SUB), LV_PART_MAIN);
lv_obj_set_pos(s_discover_status, 10, top);
lv_obj_set_width(s_discover_status, sw - 130); // keep clear of the two header buttons
lv_label_set_long_mode(s_discover_status, LV_LABEL_LONG_DOT);
lv_obj_t* btn = lv_btn_create(s_discover_root); // Stop / Scan
lv_obj_set_size(btn, 56, 30);
lv_obj_set_pos(btn, sw - 62, top - 4);
styleButton(btn);
lv_obj_add_event_cb(btn, discoverScanToggleCb, LV_EVENT_CLICKED, nullptr);
s_discover_btn_lbl = lv_label_create(btn);
lv_label_set_text(s_discover_btn_lbl, "Stop");
lv_obj_center(s_discover_btn_lbl);
lv_obj_t* mbtn = lv_btn_create(s_discover_root); // Show on map
lv_obj_set_size(mbtn, 56, 30);
lv_obj_set_pos(mbtn, sw - 122, top - 4);
styleButton(mbtn);
lv_obj_add_event_cb(mbtn, discoverShowMapCb, LV_EVENT_CLICKED, nullptr);
lv_obj_t* mlbl = lv_label_create(mbtn);
lv_label_set_text(mlbl, "Map");
lv_obj_center(mlbl);
lv_obj_t* sc = lv_obj_create(s_discover_root);
lv_obj_remove_style_all(sc);
lv_obj_set_size(sc, sw, H - (top - STATUSBAR_H) - 34 - 20); // leave a row for the wardrive footer
lv_obj_set_pos(sc, 0, top + 30);
lv_obj_set_style_bg_opa(sc, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_pad_all(sc, 8, LV_PART_MAIN);
lv_obj_set_scroll_dir(sc, LV_DIR_VER);
s_discover_feed = lv_label_create(sc);
lv_label_set_recolor(s_discover_feed, true);
lv_obj_set_width(s_discover_feed, sw - 20);
lv_label_set_long_mode(s_discover_feed, LV_LABEL_LONG_CLIP); // one line per row -> tap-to-add maps Y->row
lv_obj_set_style_text_line_space(s_discover_feed, 3, LV_PART_MAIN); // fixed row pitch (font line-height + 3)
lv_obj_set_style_text_font(s_discover_feed, &g_font_12, LV_PART_MAIN);
lv_obj_set_style_text_color(s_discover_feed, lv_color_hex(0xC8CDD2), LV_PART_MAIN);
lv_obj_add_flag(s_discover_feed, LV_OBJ_FLAG_CLICKABLE); // tap a node row -> add to contacts
lv_obj_add_event_cb(s_discover_feed, discoverFeedTapCb, LV_EVENT_CLICKED, nullptr);
lv_label_set_text(s_discover_feed, "");
// wardriving footer (GPS + logged-count status), pinned at the bottom
s_disc_footer = lv_label_create(s_discover_root);
lv_label_set_recolor(s_disc_footer, true);
lv_obj_set_style_text_font(s_disc_footer, &g_font_12, LV_PART_MAIN);
lv_obj_set_pos(s_disc_footer, 10, STATUSBAR_H + H - 18);
lv_label_set_text(s_disc_footer, "#7A7F87 Wardrive: \xE2\x80\xA6#");
the_mesh.discoverClear();
s_discover_scanning = true;
s_discover_last_scan_ms = 0; // fire the first sweep on the first tick
discoverBuildFeed();
s_discover_timer = lv_timer_create(discoverTimerCb, 1000, nullptr);
if (g_statusbar.root) lv_obj_move_foreground(g_statusbar.root);
}
static void openMonitorPage() {
closeMonitorPage();
const lv_coord_t sw = lv_disp_get_hor_res(nullptr);
@@ -24632,6 +24946,56 @@ static void routeHudUpdate();
static void showRouteHud();
static void hideRouteHud();
// ---- Discover wardriving coverage overlay (drawn on the map pan layer) ----
// Plots the Discover app's logged coverage samples (s_disc_track[], populated in discoverWardriveTick)
// as signal-coloured dots — a personal RF-coverage map: a 0-hop hit means "reachable from that point",
// so green = strong contact, red = weak. Object pool freed alongside the other map markers.
static lv_obj_t* s_disc_map_objs[k_disc_track_max] = {nullptr};
static int s_disc_map_obj_n = 0;
static void discoverFreeMapObjs() {
for (int i = 0; i < s_disc_map_obj_n; ++i)
if (s_disc_map_objs[i]) { lv_obj_del(s_disc_map_objs[i]); s_disc_map_objs[i] = nullptr; }
s_disc_map_obj_n = 0;
}
static void discoverDrawCoverage(lv_obj_t* parent, double cwx, double cwy) {
const int cap = (int)(sizeof(s_disc_map_objs) / sizeof(s_disc_map_objs[0]));
const int total = s_disc_track_n < k_disc_track_max ? s_disc_track_n : k_disc_track_max;
for (int k = 0; k < total && s_disc_map_obj_n < cap; ++k) {
const DiscTrackPt& p = s_disc_track[k];
double mwx, mwy;
latLonToWorldPx((double)p.lat_e6 / 1e6, (double)p.lon_e6 / 1e6, s_map_zoom, &mwx, &mwy);
const int sx = (int)(mwx - cwx + k_map_canvas_w / 2);
const int sy = (int)(mwy - cwy + k_map_canvas_h / 2);
if (sx < -4 || sx >= k_map_canvas_w + 4 || sy < -4 || sy >= k_map_canvas_h + 4) continue;
int r = p.best_rssi; if (r > -50) r = -50; if (r < -110) r = -110; // clamp to the colour range
int g = (r + 110) * 255 / 60; // 0 (weak) .. 255 (strong)
uint32_t col = ((uint32_t)(255 - g) << 16) | ((uint32_t)g << 8); // red -> green
lv_obj_t* d = lv_obj_create(parent);
lv_obj_remove_style_all(d);
lv_obj_set_size(d, 8, 8);
lv_obj_set_pos(d, sx - 4, sy - 4);
lv_obj_set_style_bg_color(d, lv_color_hex(col), LV_PART_MAIN);
lv_obj_set_style_bg_opa(d, LV_OPA_70, LV_PART_MAIN);
lv_obj_set_style_border_color(d, lv_color_hex(0x101010), LV_PART_MAIN);
lv_obj_set_style_border_width(d, 1, LV_PART_MAIN);
lv_obj_set_style_radius(d, 4, LV_PART_MAIN);
lv_obj_clear_flag(d, LV_OBJ_FLAG_CLICKABLE);
s_disc_map_objs[s_disc_map_obj_n++] = d;
}
}
// Discover page's "Show on map": centre the map on our current position + open the Map tab, where
// the coverage overlay draws. Keeps the centre (s_map_view_inited) so onMapTabActivated won't snap back.
static void discoverJumpToMapHere() {
UITask* task = g_lv.task;
if (task && task->getGpsFix()) {
s_map_center_lat = task->getNodeLat();
s_map_center_lon = task->getNodeLon();
s_map_view_inited = true;
}
closeDiscoverPage();
goToTab(MAP_TAB_INDEX);
}
static void freeMapMarkers() {
for (auto& m : s_map_markers) {
if (m.obj) { lv_obj_del(m.obj); m.obj = nullptr; }
@@ -24644,6 +25008,7 @@ static void freeMapMarkers() {
if (s_route_objs[i]) { lv_obj_del(s_route_objs[i]); s_route_objs[i] = nullptr; }
}
s_route_obj_n = 0;
discoverFreeMapObjs();
}
static void openMarkerPopupForContact(int mesh_idx);
@@ -24783,6 +25148,9 @@ static void renderMapMarkers() {
++slot;
}
// Discover wardriving coverage dots (my logged signal samples), under the route overlay.
if (s_disc_track_n > 0) discoverDrawCoverage(parent, cwx, cwy);
// Route-replay overlay — drawn last so the path + nodes sit above tiles,
// link lines and contact markers.
if (s_route_active) drawRouteOverlay(parent, cwx, cwy);
@@ -35111,6 +35479,7 @@ enum AppDrawerAction {
APPACT_CHATS, APPACT_CONTACTS, APPACT_MAP, APPACT_SETTINGS,
APPACT_ADVERT, APPACT_POWER, APPACT_MENTIONS, APPACT_CMDCENTER, APPACT_SIGNAL,
APPACT_TERMINAL, APPACT_FILES, APPACT_MONITOR, APPACT_SPECTRUM, APPACT_SNAKE, APPACT_VNC, APPACT_REMOTE, APPACT_READER,
APPACT_DISCOVER,
};
static void closeAppDrawer() {
@@ -35355,6 +35724,7 @@ static void appTileCb(lv_event_t* e) {
case APPACT_SIGNAL: openSignalInfoPopup(); return; // signal/traffic + auto-discover settings
case APPACT_MONITOR: openMonitorPage(); return; // RF activity graph + repeater-style radio stats
case APPACT_SPECTRUM: openSpectrumPage(); return; // swept RF spectrum analyzer (borrows the radio)
case APPACT_DISCOVER: openDiscoverPage(); return; // active node-discovery sweep + nearby list
#if !defined(HAS_TANMATSU)
case APPACT_VNC: openVncPage(); return; // screen mirror + remote control from a browser
case APPACT_REMOTE: openRemotePage(); return; // reboot into the web-resolution headless UI
@@ -35702,7 +36072,7 @@ static void openAppDrawer() {
{ LV_SYMBOL_ENVELOPE, "Chats", APPACT_CHATS, unread, 0x4F9DF7 }, // messaging blue
{ TOUCH_SYM_PERSON, "Contacts", APPACT_CONTACTS, 0, 0xA784E0 }, // people violet
{ LV_SYMBOL_GPS, "Map", APPACT_MAP, 0, 0x53C06B }, // location green
{ "@", "Mentions", APPACT_MENTIONS, mentions, 0xF2A33C }, // mention amber
{ LV_SYMBOL_REFRESH, "Discover", APPACT_DISCOVER, 0, 0x9B59FF }, // active node-discovery sweep (purple)
{ LV_SYMBOL_UPLOAD, "Advertise", APPACT_ADVERT, 0, 0xE072B0 }, // broadcast magenta
#if !defined(HAS_TANMATSU)
{ LV_SYMBOL_IMAGE, "VNC", APPACT_VNC, 0, 0x6C7CF0 }, // browser screen-mirror indigo
@@ -35714,6 +36084,7 @@ static void openAppDrawer() {
{ nullptr, "Signal", APPACT_SIGNAL, 0, COLOR_ACCENT }, // theme (drawn signal bars)
{ TOUCH_SYM_ANTENNA, "Monitor", APPACT_MONITOR, 0, 0x35C9C9 }, // RF monitor cyan
{ TOUCH_SYM_ANTENNA, "Spectrum", APPACT_SPECTRUM, 0, 0xE8A33D }, // RF spectrum analyzer amber
{ "@", "Mentions", APPACT_MENTIONS, mentions, 0xF2A33C }, // mention amber
{ LV_SYMBOL_SETTINGS, "Settings", APPACT_SETTINGS, 0, 0x9AA3AD }, // neutral gear grey
#if defined(HAS_TOUCH_UI)
{ ">_", "Terminal", APPACT_TERMINAL, 0, 0x3DD27A }, // console green
@@ -41794,8 +42165,13 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
g_draw_buffer = (lv_color_t*)heap_caps_malloc(buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!g_draw_buffer) g_draw_buffer = (lv_color_t*)malloc(buf_bytes);
#elif defined(HAS_TDISPLAY_P4)
// RM69A10: LVGL renders at 284-wide (half res, upscaled 2x on flush) — full-width band in the abundant 32MB PSRAM.
// P4: LVGL renders at half res (upscaled 2x on flush) — full-width band in the abundant 32MB PSRAM.
// AMOLED (RM69A10) = 284-wide; TFT-LCD (HI8561) = 270-wide.
#if defined(HAS_TDP4_LCD)
g_draw_buf_px = 270 * LV_DRAW_BUF_LINES;
#else
g_draw_buf_px = 284 * LV_DRAW_BUF_LINES;
#endif
const size_t buf_bytes = sizeof(lv_color_t) * g_draw_buf_px;
g_draw_buffer = (lv_color_t*)heap_caps_malloc(buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!g_draw_buffer) g_draw_buffer = (lv_color_t*)malloc(buf_bytes);
@@ -41936,11 +42312,17 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
g_lv.disp_drv.hor_res = TAN_PANEL_PW; // 480
g_lv.disp_drv.ver_res = TAN_PANEL_PH; // 800
#elif defined(HAS_TDISPLAY_P4)
// Render at HALF the 568x1232 panel; RM69A10Display upscales RM_UI_SCALE(2)x on flush, so the whole
// UI is uniformly 2x bigger on the high-DPI AMOLED (simpler than per-element scaling). The GT9895
// touch reports in this same 284x616 logical space.
// Render at HALF the native panel; the P4 DisplayDriver upscales 2x on flush, so the whole UI is
// uniformly 2x bigger on the high-DPI panel (simpler than per-element scaling). The touch driver
// reports in this same logical space. Two SKUs (build-time): AMOLED 568x1232 -> 284x616 (GT9895
// touch); TFT-LCD 540x1168 -> 270x584 (HI8561 integrated touch).
#if defined(HAS_TDP4_LCD)
g_lv.disp_drv.hor_res = 270;
g_lv.disp_drv.ver_res = 584;
#else
g_lv.disp_drv.hor_res = 284;
g_lv.disp_drv.ver_res = 616;
#endif
#elif defined(TLORA_PAGER)
// Fixed landscape via hardware MADCTL rotation (like T-Deck/Heltec below),
// just a different native panel size — no ui_landscape ternary needed since
@@ -44312,6 +44694,7 @@ static const PopupEnt k_popup_registry[] = {
{ P_OPEN(s_urlmenu_root), []{ closeUrlMenu(); }, PF_COUNT }, // chat URL -> action menu
{ P_OPEN(s_meminfo_root), []{ closeMemInfo(); }, PF_COUNT },
{ P_OPEN(s_monitor_root), []{ closeMonitorPage(); }, PF_COUNT },
{ P_OPEN(s_discover_root), []{ closeDiscoverPage(); }, PF_COUNT },
{ P_OPEN(s_spec_root), []{ closeSpectrumPage(); }, PF_COUNT },
{ P_OPEN(s_advert_root), []{ closeAdvertPage(); }, PF_COUNT }, // was dismissable but never counted
#if defined(HAS_EXPANSION_KIT)
@@ -6,7 +6,10 @@
# drivers (the P4's touch is HI8561, in variants/tdisplay_p4/).
set(REPO "${CMAKE_CURRENT_LIST_DIR}/../../..")
set(APP "${REPO}/src")
file(GLOB_RECURSE APP_SRCS "${APP}/*.cpp" "${APP}/*.c" "${REPO}/variants/tdisplay_p4/*.cpp")
# Also glob variants/tdisplay_p4/*.c the vendored esp_lcd_hi8561.c (Espressif Apache-2.0, verbatim)
# is kept as C so it compiles without the C++ strictness patches esp_lcd_rm69a10.cpp needed; its header
# has extern "C", so HI8561Display.cpp (C++, LCD SKU) links against it fine.
file(GLOB_RECURSE APP_SRCS "${APP}/*.cpp" "${APP}/*.c" "${REPO}/variants/tdisplay_p4/*.cpp" "${REPO}/variants/tdisplay_p4/*.c")
list(FILTER APP_SRCS EXCLUDE REGEX "/main\\.cpp$")
list(FILTER APP_SRCS EXCLUDE REGEX "/helpers/input/(HeltecV4CapTouch|TDeckKeyboard|TDeckTouch|TDeckTrackball|RakTapV2Touch)\\.cpp$")
+18 -3
View File
@@ -52,9 +52,8 @@ set(WADA_DEFS
XL9535_SX1262_RST_IO=16 XL9535_SX1262_DIO1_IO=17 # SX1262 reset + IRQ behind the expander
XL9535_RF_SWITCH_IO=1 # SKY13453 VCTL (TX/RX path)
# --- Display: RM69A10 AMOLED, MIPI-DSI, 568x1232 portrait (self-emissive; no backlight pin) ---
DISPLAY_CLASS=RM69A10Display
SCREEN_WIDTH=568 SCREEN_HEIGHT=1232
# --- Display: panel SKU (DISPLAY_CLASS / SCREEN_WIDTH / SCREEN_HEIGHT) is selected in the
# WADA_P4_LCD block below the RESET line (XL9535 IO2) is shared by both SKUs. ---
XL9535_SCREEN_RST_IO=2
# --- Touch: HI8561 (I2C_1); RST/INT on the expander ---
@@ -81,6 +80,22 @@ set(WADA_DEFS
foreach(_d ${WADA_DEFS})
idf_build_set_property(COMPILE_OPTIONS "-D${_d}" APPEND)
endforeach()
# --- Panel SKU: the LilyGo T-Display P4 ships as AMOLED (RM69A10, self-emissive, default) or
# TFT-LCD (HI8561 TDDI, 540x1168 + integrated touch + DCS-0x51 backlight). LilyGo themselves branch
# per-panel ([rm69a10] vs [hi8561] firmware), so we build a SEPARATE bin per SKU the proven AMOLED
# build stays byte-for-byte untouched. Select the LCD with `WADA_P4_LCD=1 ./build.sh build`
# (compiles HI8561Display + the HI8561 integrated-touch path in Hi8561Touch.cpp). ---
if(DEFINED ENV{WADA_P4_LCD})
set(P4_DISPLAY_DEFS DISPLAY_CLASS=HI8561Display SCREEN_WIDTH=540 SCREEN_HEIGHT=1168 HAS_TDP4_LCD=1)
message(STATUS "wadamesh: T-Display P4 panel = HI8561 TFT-LCD (540x1168)")
else()
set(P4_DISPLAY_DEFS DISPLAY_CLASS=RM69A10Display SCREEN_WIDTH=568 SCREEN_HEIGHT=1232)
message(STATUS "wadamesh: T-Display P4 panel = RM69A10 AMOLED (568x1232)")
endif()
foreach(_d ${P4_DISPLAY_DEFS})
idf_build_set_property(COMPILE_OPTIONS "-D${_d}" APPEND)
endforeach()
# One-off C6-reflash helper: build with `C6_FLASH_HELPER=1 ./build.sh build` to boot a P4 image that
# never touches the C6 (no C6_EN reset pulse, no AT worker), so the C6 can be held in ROM download mode
# and reflashed over its own USB. Harmless when the env var is unset (normal builds are unaffected).
+192
View File
@@ -0,0 +1,192 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// HI8561 TFT-LCD DisplayDriver for the LilyGo T-Display P4 (LCD SKU). Mirrors RM69A10Display.cpp
// almost line-for-line — the ESP32-P4 MIPI-DSI bring-up is identical; only the vendor panel driver,
// the 540x1168 resolution and the DPI timing differ. The vendor panel driver lives in the vendored
// esp_lcd_hi8561.{h,cpp} (Espressif's Apache-2.0 esp_lcd_hi8561, from Waveshare-ESP32-components,
// unmodified except the .c->.cpp rename for our variants/ glob). The 540x1168 @ 48 MHz DPI timing is
// the vendor's HI8561_540_1168_PANEL_60HZ_DPI_CONFIG macro (hand-inlined here so num_fbs / pixel
// format stay explicit and this file diffs cleanly against RM69A10Display.cpp). The DSI-PHY LDO is
// kept at the tested-on-the-board 1.83 V (a deprecated LilyGo P4 bin notes "changed MIPI voltage
// domain to 1.8V" — same rail for both panels), NOT the 2.5 V a generic HI8561 eval board uses.
#if defined(HAS_TDISPLAY_P4)
#include "HI8561Display.h"
#include <Arduino.h>
#include "esp_heap_caps.h"
#include "esp_ldo_regulator.h"
#include "Xl9535.h" // SCREEN_RST lives on the expander (reset_gpio_num = -1)
#include "esp_lcd_mipi_dsi.h"
#include "esp_lcd_panel_io.h" // esp_lcd_panel_io_tx_param (runtime brightness 0x51)
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_hi8561.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
// The DPI panel copies each draw_bitmap band into the internal frame buffer ASYNCHRONOUSLY
// (DMA2D) and fires on_color_trans_done when the copy is complete. writePixelsRGB565 reuses a
// single upscale scratch buffer (and LVGL reuses its draw buffer) on the very next flush, so
// without waiting for that copy the next band overwrites the source mid-transfer — corrupting
// the frame buffer as horizontal black/garbage bands. This binary semaphore makes each flush
// synchronous: draw_bitmap -> wait for done. (Same contract as RM69A10Display.)
static SemaphoreHandle_t s_flush_sem = nullptr;
static bool IRAM_ATTR hi8561TransDone(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data_t*, void*) {
BaseType_t hpw = pdFALSE;
if (s_flush_sem) xSemaphoreGiveFromISR(s_flush_sem, &hpw);
return hpw == pdTRUE;
}
// HI8561 TFT-LCD, 540x1168 portrait. DPI timing = vendor HI8561_540_1168_PANEL_60HZ_DPI_CONFIG.
#define HI_W 540
#define HI_H 1168
// UI scale: the panel is high-DPI, so LVGL renders at 1/HI_UI_SCALE and writePixelsRGB565
// nearest-neighbour-upscales each flush to fill the native panel — whole UI HI_UI_SCALE x bigger,
// uniformly. UITask's HAS_TDP4_LCD LVGL res + the HI8561-touch scale must match (HI_W/HI_UI_SCALE).
#ifndef HI_UI_SCALE
#define HI_UI_SCALE 2
#endif
#define HI_DPI_MHZ 48 // vendor HI8561_540_1168 DPI clock
#define HI_LANES 2
#define HI_BITRATE 1000 // Mbps per lane (vendor HI8561_PANEL_BUS_DSI_2CH_CONFIG)
#define HI_HSYNC 20 // hsync_pulse_width
#define HI_HBP 40 // hsync_back_porch
#define HI_HFP 20 // hsync_front_porch
#define HI_VSYNC 2 // vsync_pulse_width
#define HI_VBP 12 // vsync_back_porch
#define HI_VFP 200 // vsync_front_porch
// ESP32-P4 MIPI-DSI PHY internal LDO: channel 3 @ 1.83V — the tested LilyGo T-Display P4 value
// (same rail the RM69A10 SKU uses; the board's MIPI voltage domain is 1.8V, per LilyGo).
#define DSI_LDO_CHAN 3
#define DSI_LDO_MV 1830
static esp_ldo_channel_handle_t s_ldo = nullptr;
bool HI8561Display::begin() {
// 1. Power the DSI PHY via the P4 internal LDO (BEFORE creating the DSI bus).
esp_ldo_channel_config_t ldo_cfg = { .chan_id = DSI_LDO_CHAN, .voltage_mv = DSI_LDO_MV };
if (esp_ldo_acquire_channel(&ldo_cfg, &s_ldo) != ESP_OK) {
Serial.println("[HI8561] LDO acquire fail"); return false;
}
delay(100);
// 1b. Panel hardware reset via the XL9535 (reset_gpio_num = -1 so esp_lcd won't do it). Same
// HIGH -> LOW -> HIGH / 200 ms order as the RM69A10 SKU, done AFTER the LDO and BEFORE the DSI bus.
xl9535.write(Xl9535::IO_SCREEN_RST, true); delay(200);
xl9535.write(Xl9535::IO_SCREEN_RST, false); delay(200);
xl9535.write(Xl9535::IO_SCREEN_RST, true); delay(200);
// 2. DSI bus (initialises the DSI PHY).
esp_lcd_dsi_bus_handle_t dsi_bus = nullptr;
esp_lcd_dsi_bus_config_t bus_cfg = {
.bus_id = 0, .num_data_lanes = HI_LANES,
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, .lane_bit_rate_mbps = HI_BITRATE,
};
if (esp_lcd_new_dsi_bus(&bus_cfg, &dsi_bus) != ESP_OK) { Serial.println("[HI8561] dsi_bus fail"); return false; }
// 3. DBI IO (LCD command channel).
esp_lcd_panel_io_handle_t dbi_io = nullptr;
esp_lcd_dbi_io_config_t dbi_cfg = { .virtual_channel = 0, .lcd_cmd_bits = 8, .lcd_param_bits = 8 };
if (esp_lcd_new_panel_io_dbi(dsi_bus, &dbi_cfg, &dbi_io) != ESP_OK) { Serial.println("[HI8561] dbi_io fail"); return false; }
// 4. DPI (video) config — vendor HI8561_540_1168_PANEL_60HZ_DPI_CONFIG values, inlined.
esp_lcd_dpi_panel_config_t dpi_cfg = {
.virtual_channel = 0,
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
.dpi_clock_freq_mhz = HI_DPI_MHZ,
.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565,
.num_fbs = 1,
.video_timing = {
.h_size = HI_W, .v_size = HI_H,
.hsync_pulse_width = HI_HSYNC, .hsync_back_porch = HI_HBP, .hsync_front_porch = HI_HFP,
.vsync_pulse_width = HI_VSYNC, .vsync_back_porch = HI_VBP, .vsync_front_porch = HI_VFP,
},
.flags = { .use_dma2d = true },
};
// 5. HI8561 vendor panel (reset via the XL9535 -> reset_gpio_num=-1; use the driver's default init).
hi8561_vendor_config_t vendor = {
.init_cmds = nullptr, .init_cmds_size = 0,
.mipi_config = { .dsi_bus = dsi_bus, .dpi_config = &dpi_cfg },
};
esp_lcd_panel_dev_config_t dev = {
.reset_gpio_num = -1,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = 16,
.vendor_config = &vendor,
};
if (esp_lcd_new_panel_hi8561(dbi_io, &dev, &_panel) != ESP_OK) { Serial.println("[HI8561] new_panel fail"); return false; }
esp_lcd_panel_reset(_panel);
if (esp_lcd_panel_init(_panel) != ESP_OK) { Serial.println("[HI8561] panel_init fail"); return false; }
esp_lcd_panel_disp_on_off(_panel, true);
// Brightness: the HI8561's default init does SLPOUT/DISPON but leaves brightness unset; drive it at
// runtime via DCS 0x51 (the TDDI's integrated backlight — no GPIO). Keep the DBI IO handle so
// setBrightness() can drive it live (CC slider); start at max so the LCD actually lights.
_dbi_io = dbi_io;
setBrightness(255);
_on = true;
// Signal each draw_bitmap's frame-buffer copy completion so writePixelsRGB565 can wait for it.
s_flush_sem = xSemaphoreCreateBinary();
esp_lcd_dpi_panel_event_callbacks_t cbs = {};
cbs.on_color_trans_done = hi8561TransDone;
esp_lcd_dpi_panel_register_event_callbacks(_panel, &cbs, nullptr);
// Clear the panel framebuffer to black once, so nothing stale shows before LVGL's first flush.
{
const int SH = 32;
uint16_t* strip = (uint16_t*)heap_caps_calloc((size_t)HI_W * SH, 2, MALLOC_CAP_SPIRAM);
if (strip) {
for (int y0 = 0; y0 < HI_H; y0 += SH) {
int hh = (y0 + SH <= HI_H) ? SH : (HI_H - y0);
esp_lcd_panel_draw_bitmap(_panel, 0, y0, HI_W, y0 + hh, strip);
if (s_flush_sem) xSemaphoreTake(s_flush_sem, pdMS_TO_TICKS(100)); // let the copy finish before reusing/freeing strip
}
free(strip);
}
}
Serial.printf("[HI8561] up %dx%d\n", HI_W, HI_H);
return true;
}
void HI8561Display::writePixelsRGB565(int x, int y, int w, int h, const uint16_t* pixels) {
if (!_panel || !pixels || w <= 0 || h <= 0) return;
#if HI_UI_SCALE > 1
// Nearest-neighbour upscale the (half-res) LVGL band to the native panel: each source pixel becomes
// an HI_UI_SCALE x HI_UI_SCALE block. One expanded band -> one draw_bitmap (exclusive end coords).
const int S = HI_UI_SCALE, dw = w * S, dh = h * S;
static uint16_t* s_up = nullptr; static size_t s_up_px = 0;
size_t need = (size_t)dw * dh;
if (need > s_up_px) {
if (s_up) heap_caps_free(s_up);
s_up = (uint16_t*)heap_caps_malloc(need * sizeof(uint16_t), MALLOC_CAP_SPIRAM);
s_up_px = s_up ? need : 0;
}
if (s_up) {
for (int row = 0; row < h; row++) {
const uint16_t* src = pixels + (size_t)row * w;
uint16_t* d0 = s_up + (size_t)(row * S) * dw; // first of S output rows
for (int i = 0; i < w; i++) { uint16_t c = src[i]; uint16_t* p = d0 + i * S; for (int sx = 0; sx < S; sx++) p[sx] = c; }
for (int sy = 1; sy < S; sy++) memcpy(d0 + (size_t)sy * dw, d0, (size_t)dw * sizeof(uint16_t));
}
esp_lcd_panel_draw_bitmap(_panel, x * S, y * S, x * S + dw, y * S + dh, s_up);
if (s_flush_sem) xSemaphoreTake(s_flush_sem, pdMS_TO_TICKS(100)); // wait for the FB copy before the next flush reuses s_up
return;
}
// fall through to an unscaled draw if the scratch alloc failed (tiny UI, but visible)
#endif
// exclusive end coords (esp_lcd_panel_draw_bitmap contract).
esp_lcd_panel_draw_bitmap(_panel, x, y, x + w, y + h, pixels);
if (s_flush_sem) xSemaphoreTake(s_flush_sem, pdMS_TO_TICKS(100)); // wait for the FB copy before LVGL reuses this buffer
}
void HI8561Display::setBrightness(uint8_t b) {
if (!_dbi_io) return;
esp_lcd_panel_io_tx_param(_dbi_io, 0x51, &b, 1); // DCS SET_DISPLAY_BRIGHTNESS
}
void HI8561Display::turnOn() { if (_panel) esp_lcd_panel_disp_on_off(_panel, true); _on = true; }
void HI8561Display::turnOff() { if (_panel) esp_lcd_panel_disp_on_off(_panel, false); _on = false; }
#endif // HAS_TDISPLAY_P4
+48
View File
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
// HI8561 TFT-LCD (MIPI-DSI, 540x1168 portrait) — wadamesh DisplayDriver for the LilyGo T-Display P4
// *LCD* SKU. The T-Display P4 ships in two panel variants (LilyGo's own firmware branches
// [hi8561] vs [rm69a10]): the AMOLED SKU uses the RM69A10 (see RM69A10Display), the TFT-LCD SKU
// uses the HI8561 TDDI (integrated display+touch driver). This class is the LCD-SKU display; it is
// a near line-for-line mirror of RM69A10Display — the P4 DSI bring-up is identical (internal LDO ->
// DSI bus -> DBI IO -> DPI panel -> vendor init -> reset/init/on) — only the vendor panel driver
// (esp_lcd_new_panel_hi8561, in the vendored Apache-2.0 esp_lcd_hi8561.*), the resolution
// (540x1168), and the DPI timing (48 MHz, from the vendor's HI8561_540_1168 macro) differ. The
// panel RESET line is on the XL9535 (released in RM/HI begin), so the vendor reset_gpio_num = -1.
// Backlight is HI8561-internal (no GPIO): brightness is DCS 0x51, like the RM69A10.
// Selected at build time by HAS_TDP4_LCD (DISPLAY_CLASS=HI8561Display) — see tdisplay_p4/main/CMakeLists.txt.
#include <stdint.h>
#include <helpers/ui/DisplayDriver.h>
#include "esp_lcd_types.h"
class HI8561Display : public DisplayDriver {
bool _on = true;
esp_lcd_panel_handle_t _panel = nullptr;
esp_lcd_panel_io_handle_t _dbi_io = nullptr; // DCS command channel (runtime brightness 0x51)
public:
HI8561Display() : DisplayDriver(540, 1168) {}
bool begin(); // full DSI bring-up; returns false on failure (logs the stage)
// LVGL flush hot path — exclusive end coords (see RM69A10Display note).
void writePixelsRGB565(int x, int y, int w, int h, const uint16_t* pixels);
void setDisplayRotation(int rot) { (void)rot; } // TODO(device): panel rotation / SW-rotate
void startFrame() {}
void endFrame() {}
// --- DisplayDriver contract (minimal; LVGL does the real drawing) ---
bool isOn() override { return _on; }
void turnOn() override;
void turnOff() override;
void setBrightness(uint8_t b); // HI8561 cmd 0x51 (integrated backlight)
void clear() override {}
void startFrame(Color) override {}
void setTextSize(int) override {}
void setColor(Color) override {}
void setCursor(int, int) override {}
void print(const char*) override {}
void fillRect(int, int, int, int) override {}
void drawRect(int, int, int, int) override {}
void drawXbm(int, int, const uint8_t*, int, int) override {}
uint16_t getTextWidth(const char*) override { return 0; }
};
+110 -15
View File
@@ -43,24 +43,49 @@
// LVGL renders at 284x616 (half the 568x1232 panel; RM69A10Display upscales 2x on flush), so touch
// coordinates must be reported in that SAME 284x616 logical space — the GT9895's native 1060x2400
// grid is scaled straight to 284x616 here.
// Logical (LVGL) space = native panel / UI-scale(2). The LilyGo T-Display P4 has TWO panel SKUs,
// each with its OWN touch controller (a build-time choice — HAS_TDP4_LCD):
// AMOLED (RM69A10, 568x1232) -> 284x616 logical, touch = Goodix GT9895 (0x5D).
// TFT-LCD (HI8561, 540x1168) -> 270x584 logical, touch = HI8561 integrated TDDI touch (0x68).
#if defined(HAS_TDP4_LCD)
static const int SCR_W = 270;
static const int SCR_H = 584;
static const int RAW_W = 270;
static const int RAW_H = 584;
// The HI8561 is a TDDI (touch+display in one). Its integrated touch is at I2C 0x68 and reads via an
// indirect ERAM-pointer protocol (ported from LilyGo cpp_bus_driver hi8561_touch.cpp, GPL-3.0):
// once at init, read a touch-info start-address out of the ERAM section table (InitAddressInfo);
// then each poll writes {0xF3, addr(BE32), 0x03} and reads the point back (x/y big-endian, 0xFFFF =
// no finger). It reports in panel-native 540x1168; we scale /2 (SCR_W/HI8561_NATIVE_W) to logical.
// TODO(device): confirm the native grid + axis orientation against on-screen taps (the touch-debug
// overlay shows raw coords, via heltecV4CapTouchGetRaw).
static const uint8_t HI8561_ADDR = 0x68;
static const uint32_t HI8561_ERAM_BASE = 0x20011000;
static const uint16_t HI8561_ERAM_SIZE = 4096;
// kEsramSectionInfoStartAddress = base + 4 + 25*8 + 4 = 0x200110D0 (see cpp_bus_driver hi8561_touch.h).
static const uint32_t HI8561_ESRAM_SECTION_INFO = HI8561_ERAM_BASE + 4 + 25 * 8 + 4;
static const uint8_t HI8561_POINT_OFFSET = 3; // kTouchPointAddressOffset
static const int HI8561_NATIVE_W = 540;
static const int HI8561_NATIVE_H = 1168;
static uint32_t s_hi8561_info_addr = 0; // touch_info_start_address_ (learned at init)
#else
static const int SCR_W = 284;
static const int SCR_H = 616;
static const int RAW_W = 284;
static const int RAW_H = 616;
// NOTE: despite this file's name, the LilyGo T-Display P4 (RM69A10 variant) actually ships a Goodix
// GT9895 touch controller at 0x5D — NOT the HI8561 (that's a different display variant). An I2C bus
// scan on the real board found 0x5D. This driver talks to the GT9895. Ported from LilyGo
// NOTE: despite this file's name, the LilyGo T-Display P4 AMOLED (RM69A10) SKU actually ships a
// Goodix GT9895 touch controller at 0x5D — NOT the HI8561 (that's the LCD SKU, gated above). An I2C
// bus scan on the real board found 0x5D. This path talks to the GT9895. Ported from LilyGo
// cpp_bus_driver gt9895.cpp: the touch report is at register 0x00010308; each read = write that
// 32-bit address big-endian (4 bytes), then read the report. Scale factors default to 1.0 (the
// controller reports directly in panel coordinates), so no rescale is needed.
// 32-bit address big-endian (4 bytes), then read the report.
static const uint8_t GT9895_ADDR = 0x5D;
static const uint8_t GT9895_TOUCH_CMD[4] = { 0x00, 0x01, 0x03, 0x08 }; // register 0x00010308, big-endian
// The GT9895 reports in its native 1060x2400 digitiser grid; scale to the 568x1232 panel (LilyGo's
// GT9895_MAX_X_SIZE/MAX_Y_SIZE + RM69A10_SCREEN_W/H scale factors). Without this the raw coords
// over-run the panel (touch clamps past ~54% X / ~51% Y).
// The GT9895 reports in its native 1060x2400 digitiser grid; scale to the 284x616 logical space.
static const int GT9895_NATIVE_W = 1060;
static const int GT9895_NATIVE_H = 2400;
#endif
static bool s_init_ok = false;
static bool s_given_up = false;
@@ -97,9 +122,21 @@ static uint8_t s_point_rotation = 0;
#define P4_TOUCH_LONG_MS 1000
#endif
// ---- GT9895 low-level access ----
// ---- Touch-controller low-level access ----
// Write an N-byte command (repeated start), then read M bytes.
// Write an N-byte command (repeated start, no stop), then read M bytes. One helper per SKU (only
// the compiled one is used); the transaction shape is identical, just the I2C address differs.
#if defined(HAS_TDP4_LCD)
static bool hi8561WriteRead(const uint8_t* cmd, uint8_t clen, uint8_t* out, uint8_t olen) {
Wire.beginTransmission(HI8561_ADDR);
Wire.write(cmd, clen);
if (Wire.endTransmission(false) != 0) return false; // repeated start, no stop
uint8_t got = Wire.requestFrom((int)HI8561_ADDR, (int)olen);
if (got != olen) return false;
for (uint8_t i = 0; i < olen; i++) out[i] = Wire.read();
return true;
}
#else
static bool gt9895WriteRead(const uint8_t* cmd, uint8_t clen, uint8_t* out, uint8_t olen) {
Wire.beginTransmission(GT9895_ADDR);
Wire.write(cmd, clen);
@@ -109,6 +146,7 @@ static bool gt9895WriteRead(const uint8_t* cmd, uint8_t clen, uint8_t* out, uint
for (uint8_t i = 0; i < olen; i++) out[i] = Wire.read();
return true;
}
#endif
// ---- Init (called by UITask loop every tick until success or give-up) ----
@@ -140,6 +178,41 @@ bool heltecV4CapTouchBegin() {
Serial.println(sc);
}
#if defined(HAS_TDP4_LCD)
// HI8561 integrated TDDI touch at 0x68 — probe, then learn the ERAM touch-info pointer once.
Wire.beginTransmission(HI8561_ADDR);
if (Wire.endTransmission() != 0) {
snprintf(s_scan_str, sizeof s_scan_str, "i2c7/8: no ACK at 0x%02X", HI8561_ADDR);
Serial.printf("[TOUCH] HI8561 not found (retry %d/3)\n", s_retries);
return false;
}
// InitAddressInfo: read touch_info_start_address_ out of the ERAM section table (offset 8).
{
const uint32_t sa = HI8561_ESRAM_SECTION_INFO;
uint8_t cmd[6] = { 0xF3, (uint8_t)(sa >> 24), (uint8_t)(sa >> 16),
(uint8_t)(sa >> 8), (uint8_t)sa, 0x03 };
uint8_t buf[48] = {0};
if (!hi8561WriteRead(cmd, sizeof cmd, buf, sizeof buf)) {
snprintf(s_scan_str, sizeof s_scan_str, "0x%02X hi8561 info read fail", HI8561_ADDR);
Serial.printf("[TOUCH] %s (retry %d/3)\n", s_scan_str, s_retries);
return false;
}
uint32_t info = (uint32_t)buf[8] | ((uint32_t)buf[9] << 8) |
((uint32_t)buf[10] << 16) | ((uint32_t)buf[11] << 24);
if (info < HI8561_ERAM_BASE || info >= (HI8561_ERAM_BASE + HI8561_ERAM_SIZE)) {
snprintf(s_scan_str, sizeof s_scan_str, "0x%02X hi8561 info bad 0x%08lX",
HI8561_ADDR, (unsigned long)info);
Serial.printf("[TOUCH] %s (retry %d/3)\n", s_scan_str, s_retries);
return false;
}
s_hi8561_info_addr = info;
}
s_init_ok = true;
snprintf(s_scan_str, sizeof s_scan_str, "0x%02X hi8561 OK (info 0x%08lX)",
HI8561_ADDR, (unsigned long)s_hi8561_info_addr);
Serial.printf("[TOUCH] %s\n", s_scan_str);
return true;
#else
Wire.beginTransmission(GT9895_ADDR);
if (Wire.endTransmission() != 0) {
snprintf(s_scan_str, sizeof s_scan_str, "i2c7/8: no ACK at 0x%02X", GT9895_ADDR);
@@ -151,6 +224,7 @@ bool heltecV4CapTouchBegin() {
snprintf(s_scan_str, sizeof s_scan_str, "0x%02X gt9895 OK (retry %d)", GT9895_ADDR, s_retries - 1);
Serial.printf("[TOUCH] %s\n", s_scan_str);
return true;
#endif
}
// ---- Coordinate mapping (identity in portrait; rotation kept for parity) ----
@@ -169,12 +243,32 @@ static void mapRaw(uint16_t rx, uint16_t ry, uint16_t* ox, uint16_t* oy) {
}
// ---- Physical report read ------------------------------------------------
// Returns 1 = finger down (rx/ry set to raw panel coords), 0 = no touch, -1 = I2C error.
//
// Returns 1 = finger down (rx/ry set to logical coords), 0 = no touch, -1 = I2C error.
// TODO(device): verify X/Y axis orientation + origin against on-screen taps (may need flip/swap in mapRaw).
#if defined(HAS_TDP4_LCD)
// HI8561 integrated touch: read finger 1's point at (info_addr + kTouchPointAddressOffset). The
// report is 5 bytes: [x_hi][x_lo][y_hi][y_lo][pressure] (x/y big-endian); 0xFFFF/0xFFFF = no touch.
static int hi8561ReadPoint(uint16_t* rx, uint16_t* ry) {
if (!s_hi8561_info_addr) return -1;
const uint32_t addr = s_hi8561_info_addr + HI8561_POINT_OFFSET; // finger 1 (stride handled at init)
uint8_t cmd[6] = { 0xF3, (uint8_t)(addr >> 24), (uint8_t)(addr >> 16),
(uint8_t)(addr >> 8), (uint8_t)addr, 0x03 };
uint8_t buf[5] = {0};
if (!hi8561WriteRead(cmd, sizeof cmd, buf, sizeof buf)) return -1;
uint16_t x = ((uint16_t)buf[0] << 8) | buf[1]; // big-endian
uint16_t y = ((uint16_t)buf[2] << 8) | buf[3];
if (x == 0xFFFF && y == 0xFFFF) return 0; // no finger down
if (x >= HI8561_NATIVE_W) x = HI8561_NATIVE_W - 1; // clamp a stray out-of-range report
if (y >= HI8561_NATIVE_H) y = HI8561_NATIVE_H - 1;
// Scale the native 540x1168 grid down to the 270x584 logical space.
*rx = (uint16_t)((uint32_t)x * SCR_W / HI8561_NATIVE_W);
*ry = (uint16_t)((uint32_t)y * SCR_H / HI8561_NATIVE_H);
return 1;
}
#else
// GT9895 single-touch read: 16 bytes = TOUCH_POINT_ADDRESS_OFFSET(8) + 1*SINGLE_TOUCH_POINT_DATA_SIZE(8).
// buf[2] = finger count; point data at offset 8: [id/status][?][x_lo][x_hi][y_lo][y_hi][pressure][?]
// (x/y little-endian; scale factor 1.0 => already panel coords).
// TODO(device): verify X/Y axis orientation + origin against on-screen taps (may need flip/swap in mapRaw).
// (x/y little-endian).
static int hi8561ReadPoint(uint16_t* rx, uint16_t* ry) {
uint8_t buf[16] = {0};
if (!gt9895WriteRead(GT9895_TOUCH_CMD, 4, buf, sizeof buf)) return -1;
@@ -182,11 +276,12 @@ static int hi8561ReadPoint(uint16_t* rx, uint16_t* ry) {
if (fingers < 1 || fingers > 10) return 0; // no finger down
uint32_t x = (uint32_t)buf[10] | ((uint32_t)buf[11] << 8);
uint32_t y = (uint32_t)buf[12] | ((uint32_t)buf[13] << 8);
// Scale the native 1060x2400 grid down to the 568x1232 panel.
// Scale the native 1060x2400 grid down to the 284x616 logical space.
*rx = (uint16_t)(x * (uint32_t)SCR_W / GT9895_NATIVE_W);
*ry = (uint16_t)(y * (uint32_t)SCR_H / GT9895_NATIVE_H);
return 1;
}
#endif
static void hi8561Poll() {
uint16_t rx = 0, ry = 0;
+25 -2
View File
@@ -10,8 +10,10 @@ flashes over the right-side USB (USB-Serial-JTAG). LoRa region = EU 868.
- **SX1262** SPI SCLK=2/MOSI=3/MISO=4, CS=24, BUSY=6 (raw GPIO); **RST=XL9535-IO16, DIO1=XL9535-IO17**
- **XL9535 I2C expander** on I2C_1 (SDA=7/SCL=8, INT=GPIO5): power rails, C6-EN(IO14), SD-EN(IO15),
SCREEN_RST(IO2), TOUCH_RST(IO3)/INT(IO4), SX1262 RST(IO16)/DIO1(IO17), RF-switch VCTL(IO1), GPS-WAKE
- **Display** RM69A10 AMOLED MIPI-DSI 568×1232 (portrait); reset via XL9535
- **Touch** HI8561 (or GT9895) on I2C_1 (7/8); RST/INT via XL9535
- **Display** two SKUs (LilyGo branches [rm69a10]/[hi8561]): RM69A10 AMOLED MIPI-DSI 568×1232, or
HI8561 TFT-LCD MIPI-DSI 540×1168 — both portrait, reset via XL9535 IO2. Build-time select (below).
- **Touch** per-SKU on I2C_1 (7/8), RST/INT via XL9535: AMOLED = Goodix GT9895 (0x5D); LCD = HI8561
integrated TDDI touch (0x68)
- **C6** esp-hosted over SDIO_2 (CLK18/CMD19/D0-3=14-17); **SD** SD_MMC on SDIO_1 slot 0 (43/44/39-42)
- **GPS** L76K UART: ESP RX=22 / TX=23. **RTC** PCF8563, **gauge** BQ27220 on I2C_1
@@ -110,3 +112,24 @@ rm69a10_driver.cpp,t_display_p4_driver.cpp}`.
## Build
`cd tdisplay_p4 && ./build.sh build` · flash: `./build.sh flash -p /dev/cu.usbmodem<P4>`
## TFT-LCD SKU (HI8561) — second panel variant (DONE, needs on-device verify)
The T-Display P4 ships in two panels; LilyGo branches its own firmware `[rm69a10]` (AMOLED) vs
`[hi8561]` (TFT-LCD). We build a **separate bin per SKU** (vendor-style) so the proven AMOLED build
is never touched. **Select the LCD at build time:** `WADA_P4_LCD=1 ./build.sh build`.
- **Display** `HI8561Display.{h,cpp}` — mirrors `RM69A10Display` line-for-line; only the vendor panel
driver (`esp_lcd_new_panel_hi8561`), the resolution (**540×1168**) and the DPI timing (**48 MHz**,
`hbp40/hpw20/hfp20 · vbp12/vpw2/vfp200`) differ. Same P4 DSI bring-up, same **1.83 V** DSI-PHY LDO
(a deprecated LilyGo P4 bin notes "changed MIPI voltage domain to 1.8V"), same XL9535 IO2 reset.
Backlight is HI8561-internal (no GPIO) — brightness = DCS **0x51**, like the AMOLED.
- **Touch** the HI8561 is a TDDI (touch+display in one): its **integrated touch at I2C 0x68** (NOT the
AMOLED's GT9895) — added as the `HAS_TDP4_LCD` path in `Hi8561Touch.cpp`. Indirect ERAM-pointer
protocol ported from LilyGo `cpp_bus_driver/hi8561_touch.cpp` (GPL-3.0): read the touch-info start
address out of ERAM once, then poll finger 1 (`{0xF3,addr(BE32),0x03}` → x/y big-endian).
- **Vendored** `esp_lcd_hi8561.{h,cpp}` = Espressif's Apache-2.0 driver (from Waveshare-ESP32-components),
copied verbatim (`.c``.cpp` for our `variants/*.cpp` glob) — same provenance/pattern as `esp_lcd_rm69a10`.
- **Wiring** `main/CMakeLists.txt` `WADA_P4_LCD` block sets `DISPLAY_CLASS=HI8561Display`,
`SCREEN_WIDTH/HEIGHT=540/1168`, `HAS_TDP4_LCD=1`; UITask picks the include/extern + LVGL res (270×584).
- **On-device verify (no LCD device in-house):** (1) screen lights + renders; (2) touch registers and
is aligned — if offset, the HI8561 native touch grid ≠ 540×1168; read raw coords via the Map "Tile
debug"/touch-debug overlay (`heltecV4CapTouchGetRaw`) and adjust `HI8561_NATIVE_W/H` + the /2 scale.
+336
View File
@@ -0,0 +1,336 @@
/*
* 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_hi8561.h"
#define HI8561_CMD_SHLR_BIT (1 << 0)
#define HI8561_CMD_UPDN_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 hi8561_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);
} hi8561_panel_t;
static const char *TAG = "hi8561";
static esp_err_t panel_hi8561_del(esp_lcd_panel_t *panel);
static esp_err_t panel_hi8561_init(esp_lcd_panel_t *panel);
static esp_err_t panel_hi8561_reset(esp_lcd_panel_t *panel);
static esp_err_t panel_hi8561_invert_color(esp_lcd_panel_t *panel, bool invert_color_data);
static esp_err_t panel_hi8561_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y);
static esp_err_t panel_hi8561_disp_on_off(esp_lcd_panel_t *panel, bool on_off);
esp_err_t esp_lcd_new_panel_hi8561(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)
{
// wadamesh: version-log commented out — the ESP_LCD_HI8561_VER_* macros come from the component's
// cmake_utilities dependency, which we don't vendor (same as esp_lcd_rm69a10.cpp). Sole edit to this
// otherwise-verbatim Espressif Apache-2.0 driver.
// ESP_LOGI(TAG, "version: %d.%d.%d", ESP_LCD_HI8561_VER_MAJOR, ESP_LCD_HI8561_VER_MINOR,
// ESP_LCD_HI8561_VER_PATCH);
ESP_RETURN_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, TAG, "invalid arguments");
hi8561_vendor_config_t *vendor_config = (hi8561_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;
hi8561_panel_t *hi8561 = (hi8561_panel_t *)calloc(1, sizeof(hi8561_panel_t));
ESP_RETURN_ON_FALSE(hi8561, ESP_ERR_NO_MEM, TAG, "no mem for hi8561 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->rgb_ele_order) {
case LCD_RGB_ELEMENT_ORDER_RGB:
hi8561->madctl_val = 0;
break;
case LCD_RGB_ELEMENT_ORDER_BGR:
hi8561->madctl_val |= LCD_CMD_BGR_BIT;
break;
default:
ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space");
break;
}
hi8561->io = io;
hi8561->init_cmds = vendor_config->init_cmds;
hi8561->init_cmds_size = vendor_config->init_cmds_size;
hi8561->reset_gpio_num = panel_dev_config->reset_gpio_num;
hi8561->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
hi8561->del = panel_handle->del;
hi8561->init = panel_handle->init;
// Overwrite the functions of MIPI DPI panel
panel_handle->del = panel_hi8561_del;
panel_handle->init = panel_hi8561_init;
panel_handle->reset = panel_hi8561_reset;
panel_handle->mirror = panel_hi8561_mirror;
panel_handle->invert_color = panel_hi8561_invert_color;
panel_handle->disp_on_off = panel_hi8561_disp_on_off;
panel_handle->user_data = hi8561;
*ret_panel = panel_handle;
ESP_LOGD(TAG, "new hi8561 panel @%p", hi8561);
return ESP_OK;
err:
if (hi8561) {
if (panel_dev_config->reset_gpio_num >= 0) {
gpio_reset_pin(panel_dev_config->reset_gpio_num);
}
free(hi8561);
}
return ret;
}
static const hi8561_lcd_init_cmd_t vendor_specific_init_default[] = {
// {cmd, { data }, data_size, delay_ms}
/**** CMD_Page 3 ****/
{0xDF, (uint8_t[]){0x90, 0x69, 0xF9}, 3, 0},
{0xDE, (uint8_t[]){0x00}, 1, 0},
{0xBB, (uint8_t[]){0x0F, 0x10, 0x43, 0x50, 0x32, 0x44, 0x44}, 7, 0},
{0xBF, (uint8_t[]){0x46, 0x32}, 2, 0},
{0xC0, (uint8_t[]){0x01, 0xAD, 0x01, 0xAD}, 4, 0},
{0xBD, (uint8_t[]){0x00, 0xB4}, 2, 0},
{0xC6, (uint8_t[]){0x00, 0x7D, 0x00, 0xC8, 0x00, 0x17, 0x1A, 0x82, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}, 23, 0},
{0xC8, (uint8_t[]){0x23, 0x48, 0x87}, 3, 0},
// {0xCC, (uint8_t[]){0x33}, 1, 0},//4lane
{0xCC, (uint8_t[]){0x31}, 1, 0}, // 2lane
// {0xCC, (uint8_t[]){0x30}, 1, 0}, // 1lane
{0xBC, (uint8_t[]){0x2E, 0x80, 0x84}, 3, 0},
{0xC3, (uint8_t[]){0x3B, 0x01, 0x02, 0x05, 0x0C, 0x0C, 0x75, 0x0A, 0x79, 0x0A, 0x79, 0x02, 0x6E, 0x02, 0x6E, 0x02, 0x6E, 0x0A, 0x0D, 0x0A, 0x0F, 0x0A, 0x0F, 0x0A, 0x0F}, 25, 0},
{0xC4, (uint8_t[]){0x01, 0x02, 0x05, 0x0C, 0x0C, 0x75, 0x0A, 0x79, 0x0A, 0x79, 0x02, 0x6E, 0x02, 0x6E, 0x02, 0x6E, 0x0A, 0x0D, 0x0A, 0x0F, 0x0A, 0x0F, 0x0A, 0x0F}, 24, 0},
{0xC5, (uint8_t[]){0x03, 0x05, 0x0C, 0x0C, 0x75, 0x0A, 0x79, 0x0A, 0x79, 0x02, 0x6E, 0x02, 0x6E, 0x02, 0x6E, 0x0A, 0x0D, 0x0A, 0x0F, 0x0A, 0x0F, 0x0A, 0x0F}, 23, 0},
{0xD7, (uint8_t[]){0x00, 0x0A, 0x63, 0x0A, 0x63, 0x0A, 0x63, 0x0A, 0x63, 0x0A, 0x63, 0x0A, 0x63, 0x0A, 0x63, 0x0A, 0x63}, 17, 0},
{0xCB, (uint8_t[]){0x7F, 0x78, 0x71, 0x64, 0x5A, 0x58, 0x4B, 0x51, 0x3A, 0x53, 0x51, 0x4F, 0x6A, 0x54, 0x57, 0x46, 0x3F, 0x2F, 0x1B, 0x0F, 0x08, 0x7F, 0x78, 0x71, 0x64, 0x5A, 0x58, 0x4B, 0x51, 0x3A, 0x53, 0x51, 0x4F, 0x6A, 0x54, 0x57, 0x46, 0x3F, 0x2F, 0x1B, 0x0F, 0x08, 0x00}, 43, 0},
{0xCE, (uint8_t[]){0x00, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C}, 23, 0},
{0xCF, (uint8_t[]){0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 45, 0},
{0xD0, (uint8_t[]){0x00, 0x1F, 0x1F, 0x11, 0x1E, 0x1F, 0x0F, 0x0F, 0x0D, 0x0D, 0x0B, 0x0B, 0x09, 0x09, 0x07, 0x07, 0x05, 0x05, 0x01, 0x1F, 0x1F, 0x1F, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 29, 0},
{0xD1, (uint8_t[]){0x00, 0x1F, 0x1F, 0x10, 0x1E, 0x1F, 0x0E, 0x0E, 0x0C, 0x0C, 0x0A, 0x0A, 0x08, 0x08, 0x06, 0x06, 0x04, 0x04, 0x00, 0x1F, 0x1F, 0x1F, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 29, 0},
{0xD2, (uint8_t[]){0x00, 0x5F, 0x1F, 0x10, 0x1F, 0x1E, 0x08, 0x08, 0x4A, 0x0A, 0x0C, 0x0C, 0x0E, 0x0E, 0x04, 0x04, 0x06, 0x06, 0x00, 0x1F, 0x1F, 0x1F, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 29, 0},
{0xD3, (uint8_t[]){0x00, 0x1F, 0x1F, 0x11, 0x1F, 0x1E, 0x09, 0x09, 0x0B, 0x0B, 0x0D, 0x0D, 0x0F, 0x0F, 0x05, 0x05, 0x07, 0x07, 0x01, 0x1F, 0x1F, 0x1F, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 29, 0},
{0xD4, (uint8_t[]){0x00, 0x20, 0x0B, 0x00, 0x0D, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x03, 0x00, 0x81, 0x04, 0xAE, 0x04, 0xB0, 0x04, 0xB2, 0x04, 0xB4, 0x04, 0xB6, 0x04, 0xB8, 0x00, 0x00, 0x00, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0x06, 0x44, 0x06, 0x46, 0x03, 0x03, 0x00, 0x00, 0x07, 0x00, 0x06, 0x04, 0xA7, 0x04, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x20, 0x00}, 87, 0},
{0xD5, (uint8_t[]){0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x32, 0x5A, 0x00, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x1E, 0xB3, 0x00, 0x0F, 0x06, 0x0C, 0x00, 0x71, 0x20, 0x04, 0x10, 0x04, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, 61, 0},
{0xCD, (uint8_t[]){0x00, 0x00}, 2, 0},
{0xDE, (uint8_t[]){0x01}, 1, 0},
{0xB9, (uint8_t[]){0x00, 0xFF, 0xFF, 0x04}, 4, 0},
{0xC7, (uint8_t[]){0x1F, 0x14, 0x0E}, 3, 0},
{0xDE, (uint8_t[]){0x02}, 1, 0},
{0xE5, (uint8_t[]){0x00, 0x60, 0x60, 0x02, 0x18, 0x60, 0x18, 0x60, 0x09, 0x04, 0x00, 0xC5, 0x01, 0x2C, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04}, 24, 0},
{0xE6, (uint8_t[]){0x10, 0x10, 0x82}, 3, 0},
{0xC4, (uint8_t[]){0x00, 0x11, 0x07, 0x00, 0x11, 0x01, 0x08}, 7, 0},
{0xC3, (uint8_t[]){0x20, 0xFF}, 2, 0},
{0xBD, (uint8_t[]){0x1B}, 1, 0},
{0xC6, (uint8_t[]){0x4A, 0x00}, 2, 0},
{0xCD, (uint8_t[]){0x14, 0x64, 0x11, 0x40}, 4, 0},
{0xC1, (uint8_t[]){0x00, 0x40, 0x00, 0x02, 0x02, 0x02, 0x02, 0x7F, 0x00, 0x00}, 10, 0},
{0xB3, (uint8_t[]){0x00, 0xA8}, 2, 0},
{0xBB, (uint8_t[]){0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x40, 0x43, 0x04}, 11, 0},
{0xC2, (uint8_t[]){0x02, 0x42, 0x50, 0x00, 0x02, 0xE4, 0x61, 0x73, 0xF9, 0x08}, 10, 0},
{0xEC, (uint8_t[]){0x07, 0x07, 0x40, 0x00, 0x22, 0x02, 0x00, 0xFF, 0x08, 0x7C, 0x00, 0x00, 0x00, 0x00}, 14, 0},
{0xDE, (uint8_t[]){0x03}, 1, 0},
{0xD1, (uint8_t[]){0x00, 0x00, 0x21, 0xFF, 0x00}, 5, 0},
{0xDE, (uint8_t[]){0x00}, 1, 0},
{0x35, (uint8_t[]){0x00}, 0, 30},
{0x11, (uint8_t[]){0x00}, 0, 120},
{0x29, (uint8_t[]){0x00}, 0, 50},
};
static esp_err_t panel_hi8561_del(esp_lcd_panel_t *panel)
{
hi8561_panel_t *hi8561 = (hi8561_panel_t *)panel->user_data;
// Delete MIPI DPI panel
ESP_RETURN_ON_ERROR(hi8561->del(panel), TAG, "del hi8561 panel failed");
if (hi8561->reset_gpio_num >= 0) {
gpio_reset_pin(hi8561->reset_gpio_num);
}
ESP_LOGD(TAG, "del hi8561 panel @%p", hi8561);
free(hi8561);
return ESP_OK;
}
static esp_err_t panel_hi8561_init(esp_lcd_panel_t *panel)
{
hi8561_panel_t *hi8561 = (hi8561_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = hi8561->io;
const hi8561_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_LOGI(TAG, "LCD ID: %02X %02X %02X", ID[0], ID[1], ID[2]);
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) {
hi8561->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 (hi8561->init_cmds) {
init_cmds = hi8561->init_cmds;
init_cmds_size = hi8561->init_cmds_size;
} else {
init_cmds = vendor_specific_init_default;
init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(hi8561_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;
hi8561->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(hi8561->init(panel), TAG, "init MIPI DPI panel failed");
return ESP_OK;
}
static esp_err_t panel_hi8561_reset(esp_lcd_panel_t *panel)
{
hi8561_panel_t *hi8561 = (hi8561_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = hi8561->io;
// Perform hardware reset
if (hi8561->reset_gpio_num >= 0) {
gpio_set_level(hi8561->reset_gpio_num, !hi8561->flags.reset_level);
vTaskDelay(pdMS_TO_TICKS(5));
gpio_set_level(hi8561->reset_gpio_num, hi8561->flags.reset_level);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(hi8561->reset_gpio_num, !hi8561->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_hi8561_invert_color(esp_lcd_panel_t *panel, bool invert_color_data)
{
hi8561_panel_t *hi8561 = (hi8561_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = hi8561->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_hi8561_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y)
{
hi8561_panel_t *hi8561 = (hi8561_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = hi8561->io;
uint8_t madctl_val = hi8561->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 |= HI8561_CMD_SHLR_BIT;
} else {
madctl_val &= ~HI8561_CMD_SHLR_BIT;
}
if (mirror_y) {
madctl_val |= HI8561_CMD_UPDN_BIT;
} else {
madctl_val &= ~HI8561_CMD_UPDN_BIT;
}
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t []) {
madctl_val
}, 1), TAG, "send command failed");
hi8561->madctl_val = madctl_val;
return ESP_OK;
}
static esp_err_t panel_hi8561_disp_on_off(esp_lcd_panel_t *panel, bool on_off)
{
hi8561_panel_t *hi8561 = (hi8561_panel_t *)panel->user_data;
esp_lcd_panel_io_handle_t io = hi8561->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
+168
View File
@@ -0,0 +1,168 @@
/*
* SPDX-FileCopyrightText: 2024-2025 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"
#include "esp_idf_version.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 */
} hi8561_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 hi8561_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;
} hi8561_vendor_config_t;
/**
* @brief Create LCD panel for model hi8561
*
* @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_hi8561(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 HI8561_PANEL_BUS_DSI_2CH_CONFIG() \
{ \
.bus_id = 0, \
.num_data_lanes = 2, \
.phy_clk_src = 0, \
.lane_bit_rate_mbps = 1000, \
}
/**
* @brief MIPI-DBI panel IO configuration structure
*
*/
#define HI8561_PANEL_IO_DBI_CONFIG() \
{ \
.virtual_channel = 0, \
.lcd_cmd_bits = 8, \
.lcd_param_bits = 8, \
}
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0)
/**
* @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 HI8561_540_1168_PANEL_60HZ_DPI_CONFIG(px_format) \
{ \
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, \
.dpi_clock_freq_mhz = 48, \
.virtual_channel = 0, \
.pixel_format = px_format, \
.num_fbs = 1, \
.video_timing = { \
.h_size = 540, \
.v_size = 1168, \
.hsync_back_porch = 40, \
.hsync_pulse_width = 20, \
.hsync_front_porch = 20, \
.vsync_back_porch = 12, \
.vsync_pulse_width = 2, \
.vsync_front_porch = 200, \
}, \
.flags.use_dma2d = true, \
}
#endif
/**
* @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] color_format Input color format of the panel
*
*/
#define HI8561_540_1168_PANEL_60HZ_DPI_CONFIG_CF(color_format) \
{ \
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, \
.dpi_clock_freq_mhz = 48, \
.virtual_channel = 0, \
.in_color_format = color_format, \
.num_fbs = 1, \
.video_timing = { \
.h_size = 540, \
.v_size = 1168, \
.hsync_back_porch = 40, \
.hsync_pulse_width = 20, \
.hsync_front_porch = 20, \
.vsync_back_porch = 12, \
.vsync_pulse_width = 2, \
.vsync_front_porch = 200, \
}, \
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#ifndef LCD_COLOR_PIXEL_FORMAT_RGB565
#define LCD_COLOR_PIXEL_FORMAT_RGB565 LCD_COLOR_FMT_RGB565
#endif
#ifndef LCD_COLOR_PIXEL_FORMAT_RGB666
#define LCD_COLOR_PIXEL_FORMAT_RGB666 LCD_COLOR_FMT_RGB888
#endif
#ifndef LCD_COLOR_PIXEL_FORMAT_RGB888
#define LCD_COLOR_PIXEL_FORMAT_RGB888 LCD_COLOR_FMT_RGB888
#endif
#define HI8561_540_1168_PANEL_60HZ_DPI_CONFIG(color_format) HI8561_540_1168_PANEL_60HZ_DPI_CONFIG_CF(color_format)
#endif
#ifdef __cplusplus
}
#endif
#endif
+5 -1
View File
@@ -15,7 +15,11 @@ extern "C" void set_boot_phase(int phase);
#include "../../src/helpers/ClockFloorRTC.h" // monotonic send-timestamp floor (issue #89)
#include <helpers/SensorManager.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#include "RM69A10Display.h"
#if defined(HAS_TDP4_LCD)
#include "HI8561Display.h" // HI8561 TFT-LCD (LCD SKU) — DISPLAY_CLASS=HI8561Display
#else
#include "RM69A10Display.h" // RM69A10 AMOLED (default SKU) — DISPLAY_CLASS=RM69A10Display
#endif
#include "Xl9535.h"
class TDisplayP4Board : public ESP32Board {