Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b554940af | ||
|
|
3779724000 | ||
|
|
71dff36904 | ||
|
|
35d0daf203 | ||
|
|
4df3dce481 | ||
|
|
e7c7df65fa | ||
|
|
4e6a9e0108 | ||
|
|
2d6a88bd1f | ||
|
|
b1ad449147 | ||
|
|
b1b689c48f | ||
|
|
b525f75335 | ||
|
|
3d6e6f0963 | ||
|
|
5e4f9a221e | ||
|
|
57f206220d | ||
|
|
756bb9f821 | ||
|
|
99423e5bf8 | ||
|
|
cab0c81208 | ||
|
|
b67f559d15 | ||
|
|
85e8681fef | ||
|
|
3fbc3e3582 | ||
|
|
22a08c6df6 | ||
|
|
041dd60b8d | ||
|
|
36e49d4d9e |
No files matched your search
@@ -33,11 +33,75 @@ normally and these combos step out of the way.
|
||||
| Turn, with a dropdown open | Scroll through the dropdown's options | none — encoder only |
|
||||
| Turn, with the accent picker open | Cycle through the accent variants (see below) | none — encoder only (Fn+Space to enter is shared) |
|
||||
| Turn, with the @-mention list open | Cycle through matching contacts (see below) | none — encoder only |
|
||||
| Turn, at the oldest/newest **loaded** message in an open chat | Loads more history in that direction and keeps going — see [Chat screen](#chat-screen) | none — encoder only |
|
||||
|
||||
The knob doubles as your only way to reach the bottom tab bar — since
|
||||
there's no touch to tap an icon, **Fn+turn** while on any main tab is the
|
||||
fastest way to switch sections.
|
||||
|
||||
## Chat screen
|
||||
|
||||
An open chat has no touch-only jump-to-oldest/jump-to-latest buttons on this
|
||||
board — everything is reachable by turning and clicking, and the encoder
|
||||
behaves a bit differently here than anywhere else in the app.
|
||||
|
||||
### Turning through messages
|
||||
|
||||
Message history is loaded lazily: only the messages currently near the
|
||||
viewport actually exist as on-screen items, and older/newer ones are pulled in
|
||||
as you approach them. Turning the encoder normally moves the focus highlight
|
||||
one message at a time, and **at the edge of what's currently loaded, it keeps
|
||||
going** — the turn instead scrolls the list to pull in the next batch of
|
||||
history, then continues moving the highlight onto it. You'll only ever land
|
||||
outside the message list (see below) once you've genuinely reached the very
|
||||
oldest or very newest message in the whole conversation — never mid-history.
|
||||
|
||||
### Leaving the message list
|
||||
|
||||
Turning **past the oldest loaded message** (top edge), once there's truly
|
||||
nothing more to load, wraps around to whatever the last focusable item on the
|
||||
screen is — the channel-settings gear in an open **channel**, or the Send
|
||||
button in a **DM** (a DM has no gear to reach). This is plain focus wrap, the
|
||||
same as reaching the end of any other list in the app; there's no chat-specific
|
||||
handling for this edge.
|
||||
|
||||
Turning **past the newest loaded message** (bottom edge) moves into the reply
|
||||
row below the messages: quick-reply (△) → emoji → text box → Send. Turning
|
||||
further past Send *is* chat-specific: in a channel it goes to the
|
||||
channel-settings gear; in a DM (nothing to reach there) it returns you to the
|
||||
newest message instead.
|
||||
|
||||
### The reply row
|
||||
|
||||
Below the messages, turning steps through: **quick-reply (△) → emoji →
|
||||
text box → Send**, in that order, in both directions — nothing is skipped.
|
||||
Turning past the △ chip (toward the messages) always jumps straight to the
|
||||
newest message, regardless of channel or DM.
|
||||
|
||||
### Backspace and Fn (Alt) — leaving vs. catching up
|
||||
|
||||
These two keys are your shortcuts in and out of "reading mode" on this
|
||||
screen, and they do different things depending on whether you've got unread
|
||||
messages:
|
||||
|
||||
- **Fn (Alt) tapped alone** — jumps straight to the **newest** message and
|
||||
moves focus into the text box, ready to type a reply. This is the
|
||||
deliberate "I'm done reading, let me respond" gesture — it works regardless
|
||||
of where your focus currently is in the chat.
|
||||
- **Backspace (tap)** — jumps to whichever message needs your attention:
|
||||
- If there are unread messages, it jumps to the first one — right below the
|
||||
**"NEW ----"** divider — and selects it, so you can then turn forward
|
||||
through your unread messages in order, oldest-of-the-unread first.
|
||||
- If nothing is unread, it jumps to the newest message instead (same
|
||||
destination as Fn+Alt, but leaves focus on the message itself rather than
|
||||
the text box).
|
||||
|
||||
Backspace's override above only applies while you're inside an open chat and
|
||||
*not* actively editing the text box — the usual rule from the top of this
|
||||
guide; with a field focused, Backspace deletes a character as normal.
|
||||
Fn (Alt) tap's override fires the same way whether or not the text box
|
||||
already has focus, since landing there is the whole point of the gesture.
|
||||
|
||||
## Keyboard layout
|
||||
|
||||
Base layer (no modifier):
|
||||
@@ -77,13 +141,13 @@ Hold **Fn (Alt)** for numbers/symbols instead:
|
||||
| Key | While editing a text field | Otherwise |
|
||||
|---|---|---|
|
||||
| **Enter** | Send / submit / newline | Select the focused item — or, if a chat message bubble is focused, opens its action menu (Ack / Mention / Copy / Info / Block) |
|
||||
| **Backspace** (tap) | Delete a character | Jump straight to the newest message if you've scrolled up in an open chat |
|
||||
| **Backspace** (tap) | Delete a character | In an open chat: jump to your first unread message, or the newest message if nothing's unread — see [Chat screen](#chat-screen) |
|
||||
| **Backspace** (hold ~1 s) | — | Same as the encoder's long-press: **Back** |
|
||||
| **Backspace** (hold ~1 s) *while the screen is locked* | — | Unlock the screen |
|
||||
| **Space** (tap) | Types a space | — |
|
||||
| **Space** (double-tap, within 250 ms) | Switches between English and your configured secondary keyboard layout | — |
|
||||
| **Space** (hold ~1 s) | — | Locks the screen (shows a "Locking…" progress bar; tapping any key cancels) |
|
||||
| **Fn (Alt)** tapped alone (press+release, nothing else) | — | Moves focus to the next field/item — a keyboard-only substitute for a turn of the encoder |
|
||||
| **Fn (Alt)** tapped alone (press+release, nothing else) | — | Moves focus to the next field/item — a keyboard-only substitute for a turn of the encoder. In an open chat: jumps to the newest message and focuses the text box instead — see [Chat screen](#chat-screen) |
|
||||
|
||||
The **BOOT** button (top of the device) instantly wakes the screen from
|
||||
idle-dim. It does *not* unlock a screen you've manually locked with the
|
||||
@@ -153,11 +217,12 @@ dimmable brightness curve.
|
||||
| Input | Action |
|
||||
|---|---|
|
||||
| Turn encoder (or Fn tap = NEXT only) | Move focus |
|
||||
| Turn, at the loaded edge of a chat | Load more history, keep moving — only exits the list at the true oldest/newest message |
|
||||
| Click encoder (or Enter) | Select / confirm |
|
||||
| Hold encoder ~1s (or hold Backspace ~1s) | Back |
|
||||
| Fn + turn (main tab) | Switch tabs |
|
||||
| Fn + turn (page/chat) | Scroll |
|
||||
| Fn tap alone | Next field |
|
||||
| Fn tap alone | Next field (in a chat: jump to newest message + focus the text box) |
|
||||
| Hold Shift + letter | Momentary uppercase |
|
||||
| Fn + Shift (editing a field) | Toggle Caps Lock |
|
||||
| Fn + Shift (not editing a field) | Nothing |
|
||||
@@ -165,7 +230,7 @@ dimmable brightness curve.
|
||||
| Fn + Space | Enter accent picker |
|
||||
| @ + letters (composer) | Auto-focuses the mention list — no Fn+Space needed |
|
||||
| Enter | Select / send / message action menu |
|
||||
| Backspace (tap) | Delete / jump to latest message |
|
||||
| Backspace (tap) | Delete / in a chat: jump to first unread message, or newest if none |
|
||||
| Backspace (hold 1s) | Back, or unlock if locked |
|
||||
| Space (tap) | Space |
|
||||
| Space (double-tap) | Switch keyboard language |
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_16MB.csv",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=0",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "opi",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "attaky_mesh_series"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "Attaky Core (ESP32-S3-WROOM-1U-N16R8, 16 MB FLASH, 8 MB OCTAL PSRAM)",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 8388608,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 115200
|
||||
},
|
||||
"url": "https://attaky.com/",
|
||||
"vendor": "attaky"
|
||||
}
|
||||
@@ -42,40 +42,54 @@ if os.path.exists(nf):
|
||||
notes = "\n".join(l.strip() for l in open(nf, encoding="utf-8")
|
||||
if l.strip() and not l.strip().startswith("#"))
|
||||
|
||||
# Both touch boards are ESP32-S3, single touch-UI ("gui") role. ota_0 sits at
|
||||
# 0x10000 on both (partitions_t*_touch.csv), so flash-update@0x10000 is correct.
|
||||
# Every wadamesh tile here is ESP32-S3, touch-UI ("gui") role. ota_0 sits at
|
||||
# 0x10000 on every touch env (partitions_t*_touch.csv), so flash-update@0x10000 is correct.
|
||||
# `name` MUST match the official MeshCore device name character-for-character, or Mesh America lists it
|
||||
# as a NEW device instead of folding our firmware into the existing tile (TJ Downes, 2026-06). Canonical
|
||||
# names verified from apps.meshamerica.com/proxy/flasher/config.json. The V4 entry is the Expansion Kit
|
||||
# (Touch) variant only, per Kaj. `slug` still maps to our bin filenames on firmware.wadamesh.com.
|
||||
#
|
||||
# A board may carry `variants` instead of a single `slug`: one "gui" firmware entry is emitted per
|
||||
# variant, all under the one tile. The T-Lora Pager ships with either radio, and Mesh America folds both
|
||||
# under a single "LilyGo T-Lora Pager" tile distinguished by a [RADIO] title prefix — matching how their
|
||||
# own stock pager entries are labelled ([SX1262]/[LR1121]), so ours sit naturally alongside them.
|
||||
BOARDS = [
|
||||
{"name": "Heltec v4 + Expansion Kit (Touch)", "slug": "heltec-v4-tft"},
|
||||
{"name": "LilyGo T-Deck", "slug": "tdeck"},
|
||||
{"name": "LilyGo T-Lora Pager", "variants": [
|
||||
{"title": "[LR1121] Touch UI", "slug": "tlora-pager-lr1121"},
|
||||
{"title": "[SX1262] Touch UI", "slug": "tlora-pager-sx1262"},
|
||||
]},
|
||||
]
|
||||
|
||||
def _firmware(title, slug):
|
||||
merged, appbin = "wadamesh-%s-merged.bin" % slug, "wadamesh-%s.bin" % slug
|
||||
return {
|
||||
"role": "gui",
|
||||
"title": title,
|
||||
"version": {TAG: {
|
||||
"notes": notes,
|
||||
"files": [
|
||||
{"type": "flash-wipe", "name": merged,
|
||||
"url": "%s/%s" % (BASE, merged),
|
||||
"title": "Full install (bootloader + firmware)"},
|
||||
{"type": "flash-update", "name": appbin,
|
||||
"url": "%s/%s" % (BASE, appbin),
|
||||
"title": "Update (app only)"},
|
||||
],
|
||||
}},
|
||||
}
|
||||
|
||||
def device(b):
|
||||
merged, appbin = "wadamesh-%s-merged.bin" % b["slug"], "wadamesh-%s.bin" % b["slug"]
|
||||
# `variants` (per-radio builds) fold under one tile; a plain board is the single default variant.
|
||||
variants = b.get("variants") or [{"title": "Touch UI", "slug": b["slug"]}]
|
||||
return {
|
||||
"maker": "wadamesh",
|
||||
"class": "wadamesh",
|
||||
"name": b["name"],
|
||||
"type": "esp32",
|
||||
"tooltip": "<img class='device' src='%s'>" % ICON,
|
||||
"firmware": [{
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {TAG: {
|
||||
"notes": notes,
|
||||
"files": [
|
||||
{"type": "flash-wipe", "name": merged,
|
||||
"url": "%s/%s" % (BASE, merged),
|
||||
"title": "Full install (bootloader + firmware)"},
|
||||
{"type": "flash-update", "name": appbin,
|
||||
"url": "%s/%s" % (BASE, appbin),
|
||||
"title": "Update (app only)"},
|
||||
],
|
||||
}},
|
||||
}],
|
||||
"firmware": [_firmware(v["title"], v["slug"]) for v in variants],
|
||||
}
|
||||
|
||||
# Optional "what's this?" blurb Mesh America shows on a hover tooltip next to our provider badge
|
||||
|
||||
@@ -17,19 +17,19 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_40": {
|
||||
"notes": "Big translation pass: hundreds of previously-English strings across menus, settings, dialogs and the setup flow are now translated into all 11 non-English languages (Dutch, German, French, Spanish, Italian, Russian, Ukrainian, Bulgarian, Serbian, Greek, Brazilian Portuguese). Menu rows that carry an icon translate too. Switch language under Settings.\nChat scrolling is fully virtualized (contributed by scratchdiver, PR #130): a chat now scrolls through its ENTIRE stored history, up to 5000 messages on SD-card devices, smoothly in both bubble and compact mode, instead of only the most recent rows.\nLong-press any message for a Delete option that removes just that message; the message menu lays its buttons two per row so it fits on one screen.\nThe chat and channel settings sheet gains \"Delete history\": wipes the whole conversation after a confirmation, while the channel membership or contact stays.\nThe in-chat jump buttons are slim arrows tucked against the right edge: fully visible while you scroll, fading to half opacity a second after you stop.\nThinkNode M9 hardware support is complete (contributed by ded, PR #138): GPS, microSD, buzzer, backlight control, lock screen, long-press context menus, and a d-pad-driven setup wizard. It can be installed straight from the web flasher.\nNew board: RAK WisMesh Tap V2 (contributed by Ethac.chen, PR #136), with a 30+ fps display driver, also installable from the web flasher.\nThe Wi-Fi network screens (join a network, hidden network, saved-network details) are full-screen pages now instead of small popups, matching the other settings pages and scrolling properly with the keyboard open.\nThe lock screen shows an envelope with the number of unread messages under the clock, so you can check for news without unlocking (idea from issue #93).\nPasswords with special characters can be entered everywhere: long-press any settings field, including the Wi-Fi password, for a Cut / Copy / Paste / All / Sym menu. Sym opens a picker with the characters the on-screen keyboard lacks (issue #122). Double-tap selects a word.\nFor Launcher installs: a \"Save update bin to SD\" button on the About page downloads the latest firmware of your selected channel into a BINS folder on the SD card, ready to flash from the Launcher. The test-builds toggle is visible on Launcher installs too.\nNew region preset \"Australia (Mid)\": 915.075 MHz, 125 kHz, SF9, CR5 (requested in issue #121).\nYour GPS position is now the first row in the quick-reply picker, so you can send your current coordinates with one tap (contributed by Yazutsu, PR #125).\nTanmatsu: hold ALT and press a letter to open the accent picker; keep ALT held and press again to cycle, release to insert (requested by jochentj, issue #129). Double-tap the space bar to switch between your enabled keyboard languages, same as the T-Deck.\nWhile in a chat, the top bar shows the active keyboard language next to the clock when more than one layout is enabled.\nThe \"Store data on SD\" toggle is now actually honored after a reboot (fix contributed by scratchdiver, PR #123). On Launcher installs it silently reverted to internal storage every boot, which also kept the slow internal-flash writes. Turning it off works reliably now too.\nTurning on \"Store data on SD\" migrates your existing profile, settings and identity to the card FIRST, so upgrading no longer risks adopting an empty card and booting as a brand-new node. If you were already hit by that, Settings -> General -> \"Copy internal data to SD\" restores the profile from internal flash.\nCard-less devices (Heltec V4, and any board without an SD card) no longer freeze for several seconds at a time. A busy mesh re-writes the whole contacts file to internal flash on every advert refresh, and that flash garbage-collection stalled the whole device; contact saves are now coalesced so only real changes (a new or removed node) are written promptly.\nDeleting a message, or clearing a whole chat, now sticks: it survives a reboot or a hardware reset instead of coming back (reported by Leon P and MrWeavis). Deleting is also much faster - chat history is written compactly, only the messages you actually have, so a delete, a history wipe, or a reboot no longer pauses the screen while it saves.\nMessages no longer vanish when you reboot the device from the phone app (reported by Leon P): the app's reboot command now saves chat history first, like every on-device reboot path already did.\nChat history saves to the SD card are more robust: files are written atomically with a fallback save path, so a badly timed reboot can no longer truncate your message history (contributed by scratchdiver, PR #135).\nLonger translated labels (French, German, etc.) that used to clip or wrap now shrink to fit: the message action menu, confirmation dialogs, and the app-drawer tiles all keep their text on one line. The signal card's \"tap for details\" hint no longer overlaps in other languages.\Line truncated
|
||||
"beta_46": {
|
||||
"notes": "Redesigned control center: round quick-toggle buttons that spread to fill the panel, a frosted-glass look, real lock/bell icons, and a confirmation popup on every toggle.\nDo Not Disturb: set quiet hours in Settings > Sound to silence the incoming-message chime during a daily window, with a bell in the status bar while it's active. (community contribution by Tesso Codemonkey)\nDo Not Disturb quick-toggle in the control center — a bell that gains a slash while quiet hours are on.\nContacts no longer go missing between reboots on microSD devices: saves are now atomic (written then swapped in) and the card is mounted far more reliably at boot.\nHeltec V4-R8: the microSD card now mounts, and the freeze when waking the device from sleep is fixed.\nLilyGo T-Pager: the map no longer shows a \"storage error\" under the launcher — it caches tiles to the microSD card, like the T-Deck.\nHeltec V4 in portrait: the Bluetooth icon no longer overlaps the clock in the status bar, and the control-center buttons no longer clip when tapped.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_40/wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_46/wadamesh-heltec-v4-tft-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-heltec-v4-tft.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_40/wadamesh-heltec-v4-tft.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_46/wadamesh-heltec-v4-tft.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
@@ -49,19 +49,74 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_40": {
|
||||
"notes": "Big translation pass: hundreds of previously-English strings across menus, settings, dialogs and the setup flow are now translated into all 11 non-English languages (Dutch, German, French, Spanish, Italian, Russian, Ukrainian, Bulgarian, Serbian, Greek, Brazilian Portuguese). Menu rows that carry an icon translate too. Switch language under Settings.\nChat scrolling is fully virtualized (contributed by scratchdiver, PR #130): a chat now scrolls through its ENTIRE stored history, up to 5000 messages on SD-card devices, smoothly in both bubble and compact mode, instead of only the most recent rows.\nLong-press any message for a Delete option that removes just that message; the message menu lays its buttons two per row so it fits on one screen.\nThe chat and channel settings sheet gains \"Delete history\": wipes the whole conversation after a confirmation, while the channel membership or contact stays.\nThe in-chat jump buttons are slim arrows tucked against the right edge: fully visible while you scroll, fading to half opacity a second after you stop.\nThinkNode M9 hardware support is complete (contributed by ded, PR #138): GPS, microSD, buzzer, backlight control, lock screen, long-press context menus, and a d-pad-driven setup wizard. It can be installed straight from the web flasher.\nNew board: RAK WisMesh Tap V2 (contributed by Ethac.chen, PR #136), with a 30+ fps display driver, also installable from the web flasher.\nThe Wi-Fi network screens (join a network, hidden network, saved-network details) are full-screen pages now instead of small popups, matching the other settings pages and scrolling properly with the keyboard open.\nThe lock screen shows an envelope with the number of unread messages under the clock, so you can check for news without unlocking (idea from issue #93).\nPasswords with special characters can be entered everywhere: long-press any settings field, including the Wi-Fi password, for a Cut / Copy / Paste / All / Sym menu. Sym opens a picker with the characters the on-screen keyboard lacks (issue #122). Double-tap selects a word.\nFor Launcher installs: a \"Save update bin to SD\" button on the About page downloads the latest firmware of your selected channel into a BINS folder on the SD card, ready to flash from the Launcher. The test-builds toggle is visible on Launcher installs too.\nNew region preset \"Australia (Mid)\": 915.075 MHz, 125 kHz, SF9, CR5 (requested in issue #121).\nYour GPS position is now the first row in the quick-reply picker, so you can send your current coordinates with one tap (contributed by Yazutsu, PR #125).\nTanmatsu: hold ALT and press a letter to open the accent picker; keep ALT held and press again to cycle, release to insert (requested by jochentj, issue #129). Double-tap the space bar to switch between your enabled keyboard languages, same as the T-Deck.\nWhile in a chat, the top bar shows the active keyboard language next to the clock when more than one layout is enabled.\nThe \"Store data on SD\" toggle is now actually honored after a reboot (fix contributed by scratchdiver, PR #123). On Launcher installs it silently reverted to internal storage every boot, which also kept the slow internal-flash writes. Turning it off works reliably now too.\nTurning on \"Store data on SD\" migrates your existing profile, settings and identity to the card FIRST, so upgrading no longer risks adopting an empty card and booting as a brand-new node. If you were already hit by that, Settings -> General -> \"Copy internal data to SD\" restores the profile from internal flash.\nCard-less devices (Heltec V4, and any board without an SD card) no longer freeze for several seconds at a time. A busy mesh re-writes the whole contacts file to internal flash on every advert refresh, and that flash garbage-collection stalled the whole device; contact saves are now coalesced so only real changes (a new or removed node) are written promptly.\nDeleting a message, or clearing a whole chat, now sticks: it survives a reboot or a hardware reset instead of coming back (reported by Leon P and MrWeavis). Deleting is also much faster - chat history is written compactly, only the messages you actually have, so a delete, a history wipe, or a reboot no longer pauses the screen while it saves.\nMessages no longer vanish when you reboot the device from the phone app (reported by Leon P): the app's reboot command now saves chat history first, like every on-device reboot path already did.\nChat history saves to the SD card are more robust: files are written atomically with a fallback save path, so a badly timed reboot can no longer truncate your message history (contributed by scratchdiver, PR #135).\nLonger translated labels (French, German, etc.) that used to clip or wrap now shrink to fit: the message action menu, confirmation dialogs, and the app-drawer tiles all keep their text on one line. The signal card's \"tap for details\" hint no longer overlaps in other languages.\Line truncated
|
||||
"beta_46": {
|
||||
"notes": "Redesigned control center: round quick-toggle buttons that spread to fill the panel, a frosted-glass look, real lock/bell icons, and a confirmation popup on every toggle.\nDo Not Disturb: set quiet hours in Settings > Sound to silence the incoming-message chime during a daily window, with a bell in the status bar while it's active. (community contribution by Tesso Codemonkey)\nDo Not Disturb quick-toggle in the control center — a bell that gains a slash while quiet hours are on.\nContacts no longer go missing between reboots on microSD devices: saves are now atomic (written then swapped in) and the card is mounted far more reliably at boot.\nHeltec V4-R8: the microSD card now mounts, and the freeze when waking the device from sleep is fixed.\nLilyGo T-Pager: the map no longer shows a \"storage error\" under the launcher — it caches tiles to the microSD card, like the T-Deck.\nHeltec V4 in portrait: the Bluetooth icon no longer overlaps the clock in the status bar, and the control-center buttons no longer clip when tapped.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_40/wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_46/wadamesh-tdeck-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_40/wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_46/wadamesh-tdeck.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"maker": "wadamesh",
|
||||
"class": "wadamesh",
|
||||
"name": "LilyGo T-Lora Pager",
|
||||
"type": "esp32",
|
||||
"tooltip": "<img class='device' src='https://wadamesh.com/wadamesh-icon-512.png'>",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "gui",
|
||||
"title": "[LR1121] Touch UI",
|
||||
"version": {
|
||||
"beta_46": {
|
||||
"notes": "Redesigned control center: round quick-toggle buttons that spread to fill the panel, a frosted-glass look, real lock/bell icons, and a confirmation popup on every toggle.\nDo Not Disturb: set quiet hours in Settings > Sound to silence the incoming-message chime during a daily window, with a bell in the status bar while it's active. (community contribution by Tesso Codemonkey)\nDo Not Disturb quick-toggle in the control center — a bell that gains a slash while quiet hours are on.\nContacts no longer go missing between reboots on microSD devices: saves are now atomic (written then swapped in) and the card is mounted far more reliably at boot.\nHeltec V4-R8: the microSD card now mounts, and the freeze when waking the device from sleep is fixed.\nLilyGo T-Pager: the map no longer shows a \"storage error\" under the launcher — it caches tiles to the microSD card, like the T-Deck.\nHeltec V4 in portrait: the Bluetooth icon no longer overlaps the clock in the status bar, and the control-center buttons no longer clip when tapped.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-tlora-pager-lr1121-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_46/wadamesh-tlora-pager-lr1121-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-tlora-pager-lr1121.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_46/wadamesh-tlora-pager-lr1121.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"role": "gui",
|
||||
"title": "[SX1262] Touch UI",
|
||||
"version": {
|
||||
"beta_46": {
|
||||
"notes": "Redesigned control center: round quick-toggle buttons that spread to fill the panel, a frosted-glass look, real lock/bell icons, and a confirmation popup on every toggle.\nDo Not Disturb: set quiet hours in Settings > Sound to silence the incoming-message chime during a daily window, with a bell in the status bar while it's active. (community contribution by Tesso Codemonkey)\nDo Not Disturb quick-toggle in the control center — a bell that gains a slash while quiet hours are on.\nContacts no longer go missing between reboots on microSD devices: saves are now atomic (written then swapped in) and the card is mounted far more reliably at boot.\nHeltec V4-R8: the microSD card now mounts, and the freeze when waking the device from sleep is fixed.\nLilyGo T-Pager: the map no longer shows a \"storage error\" under the launcher — it caches tiles to the microSD card, like the T-Deck.\nHeltec V4 in portrait: the Bluetooth icon no longer overlaps the clock in the status bar, and the control-center buttons no longer clip when tapped.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-tlora-pager-sx1262-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_46/wadamesh-tlora-pager-sx1262-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-tlora-pager-sx1262.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_46/wadamesh-tlora-pager-sx1262.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
|
||||
+162
-44
@@ -177,6 +177,28 @@
|
||||
.dl2 .dlver{display:block;font-size:.72rem;color:var(--mut);font-weight:600;margin-top:2px;padding-left:20px;letter-spacing:.03em}
|
||||
.dl2 .dlver:empty{display:none}
|
||||
.hint{max-width:760px;margin:18px auto 0;color:var(--mut);font-size:.86rem;text-align:center}
|
||||
/* device selector — grouped by manufacturer, collapsible, searchable */
|
||||
.devsel{max-width:840px; margin:20px auto 0}
|
||||
.devsearch{position:relative; max-width:460px; margin:0 auto 18px}
|
||||
.devsearch>svg{position:absolute; left:15px; top:50%; transform:translateY(-50%); width:17px; height:17px; color:var(--mut); pointer-events:none}
|
||||
.devsearch input{width:100%; box-sizing:border-box; font:inherit; font-size:.95rem; padding:11px 42px; border:1px solid var(--line); border-radius:11px; background:#fff; color:var(--ink)}
|
||||
.devsearch input::-webkit-search-cancel-button{display:none}
|
||||
.devsearch input:focus{outline:none; border-color:var(--teal); box-shadow:0 0 0 3px rgba(21,182,166,.15)}
|
||||
.devsearch-x{position:absolute; right:8px; top:50%; transform:translateY(-50%); width:26px; height:26px; border:0; border-radius:50%; background:var(--bg2); color:var(--mut); font-size:1.15rem; line-height:1; cursor:pointer; display:flex; align-items:center; justify-content:center}
|
||||
.devsearch-x:hover{color:var(--ink); background:var(--line)}
|
||||
.devsearch-empty{text-align:center; color:var(--mut); font-size:.9rem; margin:12px 0 0}
|
||||
.devsearch-empty button{font:inherit; font-size:.9rem; color:var(--teal-ink); background:none; border:0; padding:0; cursor:pointer; text-decoration:underline}
|
||||
.mfr{border:1px solid var(--line); border-radius:14px; background:var(--bg2); margin:0 auto 12px; overflow:hidden}
|
||||
.mfr[hidden]{display:none}
|
||||
.mfr>summary{list-style:none; cursor:pointer; user-select:none; display:flex; align-items:center; gap:11px; padding:15px 18px; font-weight:700; font-size:1.05rem; color:var(--ink)}
|
||||
.mfr>summary::-webkit-details-marker{display:none}
|
||||
.mfr>summary:hover{color:var(--teal-ink)}
|
||||
.mfr-name{flex:1 1 auto}
|
||||
.mfr-count{font-weight:600; font-size:.78rem; color:var(--mut); letter-spacing:.02em}
|
||||
.mfr .chev{width:18px; height:18px; color:var(--mut); flex:none; transition:transform .18s}
|
||||
.mfr[open]>summary .chev{transform:rotate(180deg)}
|
||||
.mfr-boards{max-width:none; margin:0; padding:2px 16px 18px}
|
||||
.board[hidden]{display:none}
|
||||
/* "install a previous version" picker */
|
||||
.verpick{max-width:760px; margin:16px auto 0; border:1px solid var(--line); border-radius:12px; background:var(--bg2); text-align:left}
|
||||
.verpick summary{cursor:pointer; padding:12px 16px; font-weight:600; font-size:.92rem; color:var(--ink); list-style:none; user-select:none}
|
||||
@@ -367,50 +389,102 @@
|
||||
<div class="step"><div class="num">2</div><div><h4>Pick your board & install</h4><p>Hit <b>Install</b> below, choose the serial port, and let it flash. It does a clean full install.</p></div></div>
|
||||
<div class="step"><div class="num">3</div><div><h4>You're on the mesh</h4><p>The device reboots into WADAMESH. Set your name + region in the first-run wizard.</p></div></div>
|
||||
</div>
|
||||
<div class="boards">
|
||||
<div class="board"><h4>Heltec V4 + TFT</h4><p>ESP32-S3 with CHSC6x touch.</p>
|
||||
<span class="sup full" tabindex="0"><span class="dot"></span>Fully supported<span class="pop"><b>Fully supported</b>Everything wadamesh offers works here — this is one of the two primary development boards, tested on every release.</span></span>
|
||||
<esp-web-install-button id="inst-heltec" manifest="https://firmware.wadamesh.com/latest/manifest-heltec-v4-tft.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="heltec-v4-tft" download href="https://firmware.wadamesh.com/latest/wadamesh-heltec-v4-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></div>
|
||||
<div class="board"><h4>LilyGo T-Deck / Plus</h4><p>ESP32-S3 keyboard handheld.</p>
|
||||
<span class="sup full" tabindex="0"><span class="dot"></span>Fully supported<span class="pop"><b>Fully supported</b>Everything wadamesh offers works here — this is one of the two primary development boards, tested on every release.</span></span>
|
||||
<esp-web-install-button id="inst-tdeck" manifest="https://firmware.wadamesh.com/latest/manifest-tdeck.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="tdeck" download href="https://firmware.wadamesh.com/latest/wadamesh-tdeck-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></div>
|
||||
<div class="board"><h4>ThinkNode M9</h4><p>ESP32-S3 QWERTY handheld (LR1110).</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. Not every feature has been ported or hardware-verified on this board yet.</span></span>
|
||||
<esp-web-install-button id="inst-m9" manifest="https://firmware.wadamesh.com/latest-beta/manifest-thinknode-m9.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="thinknode-m9" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-thinknode-m9-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: beta channel only until the next stable.</p></div>
|
||||
<div class="board"><h4>RAK WisMesh Tap V2</h4><p>ESP32-S3 touch node.</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. Not every feature has been ported or hardware-verified on this board yet.</span></span>
|
||||
<esp-web-install-button id="inst-rak" manifest="https://firmware.wadamesh.com/latest-beta/manifest-rak-tap-v2.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="rak-tap-v2" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-rak-tap-v2-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: beta channel only until the next stable.</p></div>
|
||||
<div class="board"><h4>Heltec V4-R8 <span style="font-weight:600;color:var(--mut)">(experimental)</span></h4><p>ESP32-S3 · 8 MB PSRAM · SD card.</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 revision is still being hardware-verified — flash with care and report back.</span></span>
|
||||
<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 — 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>
|
||||
<a class="dl" href="https://shop.nicolaielectronics.nl/" target="_blank" rel="noopener"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>Get a Tanmatsu</a></div>
|
||||
<div class="board"><h4>LilyGo T-Display P4</h4><p>ESP32-P4 AMOLED touch handheld.</p>
|
||||
<span class="sup part" tabindex="0"><span class="dot"></span>Partially supported<span class="pop"><b>Partially supported</b>Early port — it's known that not everything works yet, but the core does: LoRa mesh communication, chat, map, GPS and the phone-app link over Wi-Fi or USB. Bluetooth pairing isn't available on this board (a hardware limitation of its stock radio firmware).</span></span>
|
||||
<esp-web-install-button id="inst-tdp4" manifest="https://firmware.wadamesh.com/latest-beta/manifest-tdisplay-p4.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="tdisplay-p4" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-tdisplay-p4-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: beta channel only until the next stable.</p></div>
|
||||
<div class="board"><h4>LilyGo T-LoRa Pager</h4><p>ESP32-S3 QWERTY + rotary encoder.</p>
|
||||
<span class="sup part" tabindex="0"><span class="dot"></span>Partially supported<span class="pop"><b>Partially supported</b>New community port (thanks @codemonkeybr). The core works: LoRa mesh communication, chat, contacts, channels and the phone-app link, driven entirely by the keyboard and rotary encoder. Pick the build that matches your radio chip.</span></span>
|
||||
<p style="color:var(--mut);font-size:.8rem;margin:0">Radio <b>LR1121</b>:</p>
|
||||
<esp-web-install-button id="inst-pager-lr1121" manifest="https://firmware.wadamesh.com/latest-beta/manifest-tlora-pager-lr1121.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="tlora-pager-lr1121" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-tlora-pager-lr1121-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 style="color:var(--mut);font-size:.8rem;margin:8px 0 0">Radio <b>SX1262</b>:</p>
|
||||
<esp-web-install-button id="inst-pager-sx1262" manifest="https://firmware.wadamesh.com/latest-beta/manifest-tlora-pager-sx1262.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="tlora-pager-sx1262" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-tlora-pager-sx1262-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: beta channel only until the next stable.</p></div>
|
||||
<div class="devsel" id="devsel">
|
||||
<div class="devsearch">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-3.6-3.6"/></svg>
|
||||
<input type="search" id="devSearch" autocomplete="off" placeholder="Search by device or maker — T-Deck, Heltec, P4…" aria-label="Search devices">
|
||||
<button type="button" id="devClearX" class="devsearch-x" data-devclear hidden aria-label="Clear search">×</button>
|
||||
</div>
|
||||
<p class="devsearch-empty" data-devempty hidden>No devices match “<span data-devq></span>”. <button type="button" data-devclear>Clear search</button></p>
|
||||
|
||||
<details class="mfr" open data-mfr="LilyGo">
|
||||
<summary><span class="mfr-name">LilyGo</span><span class="mfr-count"></span><svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg></summary>
|
||||
<div class="boards mfr-boards">
|
||||
<div class="board" data-search="lilygo t-deck tdeck plus esp32-s3 keyboard handheld"><h4>LilyGo T-Deck / Plus</h4><p>ESP32-S3 keyboard handheld.</p>
|
||||
<span class="sup full" tabindex="0"><span class="dot"></span>Fully supported<span class="pop"><b>Fully supported</b>Everything wadamesh offers works here — this is one of the two primary development boards, tested on every release.</span></span>
|
||||
<esp-web-install-button id="inst-tdeck" manifest="https://firmware.wadamesh.com/latest/manifest-tdeck.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="tdeck" download href="https://firmware.wadamesh.com/latest/wadamesh-tdeck-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></div>
|
||||
<div class="board" data-search="lilygo t-display p4 tdisplay amoled rm69a10 esp32-p4 touch handheld"><h4>LilyGo T-Display P4 <span style="font-weight:600;color:var(--mut)">(AMOLED)</span></h4><p>ESP32-P4 · AMOLED touch handheld.</p>
|
||||
<span class="sup part" tabindex="0"><span class="dot"></span>Partially supported<span class="pop"><b>Partially supported</b>Early port — it's known that not everything works yet, but the core does: LoRa mesh communication, chat, map, GPS and the phone-app link over Wi-Fi or USB. Bluetooth pairing isn't available on this board (a hardware limitation of its stock radio firmware).</span></span>
|
||||
<esp-web-install-button id="inst-tdp4" manifest="https://firmware.wadamesh.com/latest-beta/manifest-tdisplay-p4.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="tdisplay-p4" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-tdisplay-p4-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: beta channel only until the next stable.</p></div>
|
||||
<div class="board" data-search="lilygo t-display p4 tdisplay lcd tft hi8561 esp32-p4 touch handheld"><h4>LilyGo T-Display P4 <span style="font-weight:600;color:var(--mut)">(LCD)</span></h4><p>ESP32-P4 · TFT-LCD touch handheld.</p>
|
||||
<span class="sup part" tabindex="0"><span class="dot"></span>Partially supported<span class="pop"><b>Partially supported</b>The LCD-screen model of the T-Display P4 (the AMOLED variant is listed above). Early port — the core works: LoRa mesh, chat, map, GPS and the phone-app link over Wi-Fi or USB. Bluetooth pairing isn't available on this board. Flash with care and report back.</span></span>
|
||||
<esp-web-install-button id="inst-tdp4-lcd"></esp-web-install-button>
|
||||
<a class="dl2" id="dl-tdp4-lcd" download><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" id="dl-tdp4-lcd-ver"></span></a>
|
||||
<p data-wm-betaonly style="color:var(--mut);font-size:.78rem;margin:6px 0 0">New screen variant, not yet hardware-tested — beta only. Flash with care and report back.</p></div>
|
||||
<div class="board" data-search="lilygo t-lora pager tlora esp32-s3 qwerty rotary encoder lr1121 sx1262"><h4>LilyGo T-LoRa Pager</h4><p>ESP32-S3 QWERTY + rotary encoder.</p>
|
||||
<span class="sup part" tabindex="0"><span class="dot"></span>Partially supported<span class="pop"><b>Partially supported</b>New community port (thanks @codemonkeybr). The core works: LoRa mesh communication, chat, contacts, channels and the phone-app link. Pick the build that matches your radio chip.</span></span>
|
||||
<p style="color:var(--mut);font-size:.8rem;margin:0">Radio <b>LR1121</b>:</p>
|
||||
<esp-web-install-button id="inst-pager-lr1121" manifest="https://firmware.wadamesh.com/latest-beta/manifest-tlora-pager-lr1121.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="tlora-pager-lr1121" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-tlora-pager-lr1121-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 style="color:var(--mut);font-size:.8rem;margin:8px 0 0">Radio <b>SX1262</b>:</p>
|
||||
<esp-web-install-button id="inst-pager-sx1262" manifest="https://firmware.wadamesh.com/latest-beta/manifest-tlora-pager-sx1262.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="tlora-pager-sx1262" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-tlora-pager-sx1262-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: beta channel only until the next stable.</p></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="mfr" open data-mfr="Heltec">
|
||||
<summary><span class="mfr-name">Heltec</span><span class="mfr-count"></span><svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg></summary>
|
||||
<div class="boards mfr-boards">
|
||||
<div class="board" data-search="heltec v4 tft esp32-s3 chsc6x touch"><h4>Heltec V4 + TFT</h4><p>ESP32-S3 with CHSC6x touch.</p>
|
||||
<span class="sup full" tabindex="0"><span class="dot"></span>Fully supported<span class="pop"><b>Fully supported</b>Everything wadamesh offers works here — this is one of the two primary development boards, tested on every release.</span></span>
|
||||
<esp-web-install-button id="inst-heltec" manifest="https://firmware.wadamesh.com/latest/manifest-heltec-v4-tft.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="heltec-v4-tft" download href="https://firmware.wadamesh.com/latest/wadamesh-heltec-v4-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></div>
|
||||
<div class="board" data-search="heltec v4-r8 v4 r8 esp32-s3 psram sd card experimental"><h4>Heltec V4-R8 <span style="font-weight:600;color:var(--mut)">(experimental)</span></h4><p>ESP32-S3 · 8 MB PSRAM · SD card.</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 revision is still being hardware-verified — flash with care and report back.</span></span>
|
||||
<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 — beta only. Please flash with care and report back.</p></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="mfr" data-mfr="Elecrow">
|
||||
<summary><span class="mfr-name">Elecrow</span><span class="mfr-count"></span><svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg></summary>
|
||||
<div class="boards mfr-boards">
|
||||
<div class="board" data-search="elecrow thinknode m9 esp32-s3 qwerty lr1110 handheld"><h4>ThinkNode M9</h4><p>ESP32-S3 QWERTY handheld (LR1110).</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. Not every feature has been ported or hardware-verified on this board yet.</span></span>
|
||||
<esp-web-install-button id="inst-m9" manifest="https://firmware.wadamesh.com/latest-beta/manifest-thinknode-m9.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="thinknode-m9" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-thinknode-m9-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: beta channel only until the next stable.</p></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="mfr" data-mfr="RAKwireless">
|
||||
<summary><span class="mfr-name">RAKwireless</span><span class="mfr-count"></span><svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg></summary>
|
||||
<div class="boards mfr-boards">
|
||||
<div class="board" data-search="rakwireless rak wismesh tap v2 esp32-s3 touch node"><h4>RAK WisMesh Tap V2</h4><p>ESP32-S3 touch node.</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. Not every feature has been ported or hardware-verified on this board yet.</span></span>
|
||||
<esp-web-install-button id="inst-rak" manifest="https://firmware.wadamesh.com/latest-beta/manifest-rak-tap-v2.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="rak-tap-v2" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-rak-tap-v2-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: beta channel only until the next stable.</p></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="mfr" data-mfr="Nicolai Electronics">
|
||||
<summary><span class="mfr-name">Nicolai Electronics</span><span class="mfr-count"></span><svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg></summary>
|
||||
<div class="boards mfr-boards">
|
||||
<div class="board" data-search="nicolai electronics tanmatsu esp32-p4 keyboard handheld app store"><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>
|
||||
<a class="dl" href="https://shop.nicolaielectronics.nl/" target="_blank" rel="noopener"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>Get a Tanmatsu</a></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="mfr" data-mfr="Attaky">
|
||||
<summary><span class="mfr-name">Attaky</span><span class="mfr-count"></span><svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg></summary>
|
||||
<div class="boards mfr-boards">
|
||||
<div class="board" data-search="attaky core esp32-s3 psram experimental"><h4>Attaky Core <span style="font-weight:600;color:var(--mut)">(experimental)</span></h4><p>ESP32-S3 · 16 MB · 8 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 — beta only. Please flash with care and report back.</p></div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<details class="verpick">
|
||||
<details class="verpick">
|
||||
<summary>Install a previous version</summary>
|
||||
<div class="verpick-body">
|
||||
<div class="verpick-row"><label for="verSel">Version</label><select id="verSel" aria-label="Firmware version"></select></div>
|
||||
@@ -614,6 +688,19 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
|
||||
// toggle (beta-only boards stay pinned to latest-beta) and show the exact version
|
||||
// each feed serves — both channel tags are fetched once so every link is accurate.
|
||||
const WM_VER = {};
|
||||
// The T-Display P4 LCD-screen variant ships only in the per-tag BETA archive (no
|
||||
// rolling-feed bin, no server manifest), so wire its Install button to a manifest
|
||||
// built client-side (same blob trick as the previous-version picker) and point its
|
||||
// download at the immutable archive bin. Called once the beta tag is known.
|
||||
window.wmWireP4Lcd = function(tag){
|
||||
const bin = 'https://firmware.wadamesh.com/releases/BETA/' + tag + '/wadamesh-tdisplay-p4-lcd-merged.bin';
|
||||
const btn = document.getElementById('inst-tdp4-lcd');
|
||||
if (btn){ const m = { name:'wadamesh ' + tag + ' \u2014 T-Display P4 (LCD)', version:tag, new_install_prompt_erase:true,
|
||||
builds:[{ chipFamily:'ESP32-P4', parts:[{ path:bin, offset:0 }] }] };
|
||||
btn.setAttribute('manifest', URL.createObjectURL(new Blob([JSON.stringify(m)], {type:'application/json'}))); }
|
||||
const dl = document.getElementById('dl-tdp4-lcd');
|
||||
if (dl){ dl.href = bin; const v = document.getElementById('dl-tdp4-lcd-ver'); if (v) v.textContent = tag; }
|
||||
};
|
||||
window.wmUpdateDownloads = function(){
|
||||
document.querySelectorAll('[data-dl-board]').forEach(a => {
|
||||
const feed = a.hasAttribute('data-dl-betaonly') ? 'latest-beta' : WM.feed;
|
||||
@@ -622,13 +709,44 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
|
||||
});
|
||||
};
|
||||
['latest','latest-beta'].forEach(feed => fetch('https://firmware.wadamesh.com/' + feed + '/version.json', {cache:'no-store'})
|
||||
.then(r => r.ok ? r.json() : null).then(v => { if (v && v.tag) { WM_VER[feed] = v.tag; wmUpdateDownloads(); } }).catch(()=>{}));
|
||||
.then(r => r.ok ? r.json() : null).then(v => { if (v && v.tag) { WM_VER[feed] = v.tag; wmUpdateDownloads(); if (feed === 'latest-beta' && window.wmWireP4Lcd) wmWireP4Lcd(v.tag); } }).catch(()=>{}));
|
||||
document.querySelectorAll('[data-wm-ch]').forEach(b => b.addEventListener('click', () => wmSetChannel(b.getAttribute('data-wm-ch'))));
|
||||
wmSetChannel('stable');
|
||||
|
||||
// ---- device selector: fill counts, remember default open state, live search ----
|
||||
(function(){
|
||||
const root = document.getElementById('devsel'); if (!root) return;
|
||||
const input = document.getElementById('devSearch');
|
||||
const clearX = document.getElementById('devClearX');
|
||||
const empty = root.querySelector('[data-devempty]'), qout = root.querySelector('[data-devq]');
|
||||
const groups = Array.from(root.querySelectorAll('.mfr'));
|
||||
groups.forEach(g => { g.dataset.defopen = g.open ? '1' : '0';
|
||||
const n = g.querySelectorAll('.board').length, c = g.querySelector('.mfr-count');
|
||||
if (c) c.textContent = n + (n === 1 ? ' device' : ' devices'); });
|
||||
const txt = b => (b.getAttribute('data-search') || b.textContent || '').toLowerCase();
|
||||
function run(q){
|
||||
const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
let anyVisible = false;
|
||||
groups.forEach(g => { let vis = 0;
|
||||
g.querySelectorAll('.board').forEach(b => { const ok = !terms.length || terms.every(t => txt(b).includes(t));
|
||||
b.hidden = !ok; if (ok) vis++; });
|
||||
g.hidden = vis === 0;
|
||||
if (!g.hidden){ anyVisible = true; g.open = terms.length ? true : (g.dataset.defopen === '1'); } });
|
||||
if (empty) empty.hidden = !(terms.length && !anyVisible);
|
||||
if (qout) qout.textContent = q;
|
||||
if (clearX) clearX.hidden = !q;
|
||||
}
|
||||
function clr(){ if (input){ input.value = ''; input.focus(); } run(''); }
|
||||
if (input){ input.addEventListener('input', () => run(input.value));
|
||||
input.addEventListener('keydown', e => { if (e.key === 'Escape') clr(); }); }
|
||||
root.querySelectorAll('[data-devclear]').forEach(b => b.addEventListener('click', clr));
|
||||
})();
|
||||
|
||||
// ---- community videos — add an entry here to add a video ----
|
||||
const VIDEOS = [
|
||||
// newest first
|
||||
{ id: 'e63uKp9r65U', title: 'LilyGo T-Deck Plus + WADAMESH: The Firmware That Finally Makes It Worth It', by: 'Signal Trek' },
|
||||
{ id: '1I7Oe1SqFlk', title: 'WadaMesh Update! Supercharge Your T-Deck', by: 'KDHD Stuff' },
|
||||
{ id: 's-8pRsC0TYA', title: 'WadaMesh: The Feature That Makes T-Deck Plus Stand Out', by: 'BFGNeil' },
|
||||
{ id: 'V19od8jbFRc', title: 'WadaMesh: The Best T-Deck Firmware So Far', by: 'KDHD Stuff' },
|
||||
// { id: '<youtube-id>', title: '<title>', by: '<channel>' },
|
||||
|
||||
+181
@@ -278,6 +278,187 @@ lib_deps =
|
||||
${env:heltec_v4_tft_companion_radio_usb_tcp_touch.lib_deps}
|
||||
lovyan03/LovyanGFX@1.2.21
|
||||
|
||||
; ======================================================================
|
||||
; Attaky Mesh Series: `pio run -e attaky_mesh_series_companion_radio_touch`.
|
||||
; ======================================================================
|
||||
[env:attaky_mesh_series_companion_radio_touch]
|
||||
platform = platformio/espressif32@6.11.0
|
||||
framework = arduino
|
||||
board = attaky_mesh_series
|
||||
monitor_speed = 115200
|
||||
board_build.partitions = partitions_tft_touch.csv
|
||||
extra_scripts = pre:scripts/inject_wifi_env.py, merge-bin.py
|
||||
|
||||
build_flags =
|
||||
-Wno-deprecated-declarations -Wno-unused-parameter -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
|
||||
-D MC_VENDORED_TOUCH_APP
|
||||
-D LORA_FREQ=910.525
|
||||
-D LORA_BW=250
|
||||
-D LORA_SF=7
|
||||
-D ENABLE_PRIVATE_KEY_IMPORT=1
|
||||
-D ENABLE_PRIVATE_KEY_EXPORT=1
|
||||
-D RADIOLIB_EXCLUDE_CC1101=1
|
||||
-D RADIOLIB_EXCLUDE_RF69=1
|
||||
-D RADIOLIB_EXCLUDE_SX1231=1
|
||||
-D RADIOLIB_EXCLUDE_SI443X=1
|
||||
-D RADIOLIB_EXCLUDE_RFM2X=1
|
||||
-D RADIOLIB_EXCLUDE_SX128X=1
|
||||
-D RADIOLIB_EXCLUDE_AFSK=1
|
||||
-D RADIOLIB_EXCLUDE_AX25=1
|
||||
-D RADIOLIB_EXCLUDE_HELLSCHREIBER=1
|
||||
-D RADIOLIB_EXCLUDE_MORSE=1
|
||||
-D RADIOLIB_EXCLUDE_APRS=1
|
||||
-D RADIOLIB_EXCLUDE_BELL=1
|
||||
-D RADIOLIB_EXCLUDE_RTTY=1
|
||||
-D RADIOLIB_EXCLUDE_SSTV=1
|
||||
-D ESP32_PLATFORM
|
||||
-D ENV_INCLUDE_GPS=1
|
||||
; 0x38 is the FT6636 touch controller, not an AHT10/20. The Adafruit probe
|
||||
; mis-detects it as a sensor and can hang boot on its register 0xBA, so keep off.
|
||||
-D ENV_INCLUDE_AHTX0=0
|
||||
-D ENV_INCLUDE_BME280=1
|
||||
-D ENV_INCLUDE_BMP280=1
|
||||
-D ENV_INCLUDE_SHTC3=1
|
||||
-D ENV_INCLUDE_SHT4X=1
|
||||
-D ENV_INCLUDE_LPS22HB=1
|
||||
-D ENV_INCLUDE_INA3221=1
|
||||
-D ENV_INCLUDE_INA219=1
|
||||
-D ENV_INCLUDE_INA226=1
|
||||
-D ENV_INCLUDE_INA260=1
|
||||
; 0x5A is the keyboard module's AW9523 (left half), not an MLX90614.
|
||||
-D ENV_INCLUDE_MLX90614=0
|
||||
-D ENV_INCLUDE_VL53L0X=1
|
||||
-D ENV_INCLUDE_BME680=1
|
||||
-D ENV_INCLUDE_BMP085=1
|
||||
-I variants/attaky_mesh_series
|
||||
-D ATTAKY_MESH_SERIES
|
||||
-D HAS_ATTAKY_MESH_KEYBOARD=1
|
||||
-D USE_SX1262
|
||||
-D ESP32_CPU_FREQ=80
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
-D P_LORA_DIO_1=21
|
||||
-D P_LORA_NSS=8
|
||||
-D P_LORA_RESET=9
|
||||
-D P_LORA_BUSY=16
|
||||
-D P_LORA_SCLK=5
|
||||
-D P_LORA_MISO=7
|
||||
-D P_LORA_MOSI=6
|
||||
-D PIN_USER_BTN=0
|
||||
-D LORA_TX_POWER=10
|
||||
-D MAX_LORA_TX_POWER=22
|
||||
-D SX126X_REGISTER_PATCH=1
|
||||
-D SX126X_DIO2_AS_RF_SWITCH=true
|
||||
; Ra-01SH clocks the SX1262 from a 32 MHz crystal, not a TCXO, so DIO3 is not a
|
||||
; TCXO supply. Must be 0 explicitly: std_init() defaults to 1.6 V otherwise.
|
||||
-D SX126X_DIO3_TCXO_VOLTAGE=0
|
||||
-D SX126X_CURRENT_LIMIT=140
|
||||
-D SX126X_RX_BOOSTED_GAIN=1
|
||||
-D PIN_GPS_RX=18
|
||||
-D PIN_GPS_TX=17
|
||||
; EnvironmentSensorManager reads GPS_BAUD_RATE, not GPS_BAUD.
|
||||
-D GPS_BAUD_RATE=9600
|
||||
; ATGM336H cold-start can exceed the 1 s probe window. Without this the probe
|
||||
; gives up and start_gps() never runs, so GPS reads on but nothing is parsed.
|
||||
-D ENV_SKIP_GPS_DETECT=1
|
||||
; Keep the base rotation portrait; UITask applies the landscape transform.
|
||||
-D PIN_BOARD_SDA=2
|
||||
-D PIN_BOARD_SCL=1
|
||||
-D DISPLAY_ROTATION=2
|
||||
-D DISPLAY_SCALE_X=1.0
|
||||
-D DISPLAY_SCALE_Y=1.0
|
||||
-D PIN_TFT_RST=10
|
||||
-D PIN_TFT_VDD_CTL=-1
|
||||
-D PIN_TFT_LEDA_CTL=14
|
||||
-D PIN_TFT_LEDA_CTL_ACTIVE=HIGH
|
||||
-D PIN_TFT_CS=45
|
||||
-D PIN_TFT_DC=12
|
||||
-D PIN_TFT_SCL=11
|
||||
-D PIN_TFT_SDA=13
|
||||
-I include
|
||||
-I src
|
||||
-I src/ui-touch
|
||||
-D UI_LVGL=1
|
||||
-D HAS_TOUCH_UI=1
|
||||
-D FIRMWARE_OTA_ENV='"attaky_mesh_series_companion_radio_touch"'
|
||||
-D ENABLE_ADVERT_ON_BOOT=0
|
||||
-D LV_CONF_PATH=lv_conf.h
|
||||
-D LV_CONF_INCLUDE_SIMPLE=1
|
||||
-D LV_INDEV_DEF_SCROLL_THROW=7
|
||||
-D USER_SETUP_LOADED=1
|
||||
-D ST7789_DRIVER=1
|
||||
-D TFT_WIDTH=240
|
||||
-D TFT_HEIGHT=320
|
||||
-D TFT_MOSI=13
|
||||
-D TFT_SCLK=11
|
||||
-D TFT_CS=45
|
||||
-D TFT_DC=12
|
||||
-D TFT_RST=10
|
||||
-D TFT_MISO=-1
|
||||
-D SPI_FREQUENCY=80000000
|
||||
-D TFT_RGB_ORDER=TFT_RGB
|
||||
-D TFT_INVERSION_ON=1
|
||||
-D TFT_BL=14
|
||||
-D TFT_BACKLIGHT_ON=1
|
||||
-D MAX_CONTACTS=2000
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=ST7789LCDDisplay
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MULTI_TRANSPORT_COMPANION=1
|
||||
-D WADAMESH_FORK_BUILD=1
|
||||
-D TCP_PORT=5000
|
||||
-D WS_PORT=8765
|
||||
-D BLE_PIN_CODE=123456
|
||||
-D CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=1
|
||||
-D CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384
|
||||
-D CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096
|
||||
|
||||
build_src_filter =
|
||||
+<*.cpp>
|
||||
+<*.c>
|
||||
+<helpers/*.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/input/*.cpp>
|
||||
; The Attaky driver provides the shared touch API.
|
||||
-<helpers/input/TDeckTouch.cpp>
|
||||
+<ui-touch/*.cpp>
|
||||
+<ui-touch/*.c>
|
||||
+<../variants/attaky_mesh_series/*.cpp>
|
||||
|
||||
lib_deps =
|
||||
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.5
|
||||
SPI
|
||||
Wire
|
||||
jgromes/RadioLib @ ^7.6.0
|
||||
rweather/Crypto @ ^0.4.0
|
||||
adafruit/RTClib @ ^2.1.3
|
||||
melopero/Melopero RV3028 @ ^1.1.0
|
||||
electroniccats/CayenneLPP @ 1.6.1
|
||||
h2zero/NimBLE-Arduino @ 1.4.3
|
||||
adafruit/Adafruit INA3221 Library @ ^1.0.1
|
||||
adafruit/Adafruit INA219 @ ^1.2.3
|
||||
robtillaart/INA226 @ ^0.6.4
|
||||
adafruit/Adafruit INA260 Library @ ^1.5.3
|
||||
adafruit/Adafruit AHTX0 @ ^2.0.5
|
||||
adafruit/Adafruit BME280 Library @ ^2.3.0
|
||||
adafruit/Adafruit BMP280 Library @ ^2.6.8
|
||||
adafruit/Adafruit SHTC3 Library @ ^1.0.1
|
||||
sensirion/Sensirion I2C SHT4x @ ^1.1.2
|
||||
arduino-libraries/Arduino_LPS22HB @ ^1.0.2
|
||||
adafruit/Adafruit MLX90614 Library @ ^2.1.5
|
||||
adafruit/Adafruit_VL53L0X @ ^1.2.4
|
||||
stevemarple/MicroNMEA @ ^2.0.6
|
||||
adafruit/Adafruit BME680 Library @ ^2.0.4
|
||||
adafruit/Adafruit BMP085 Library @ ^1.2.4
|
||||
adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
|
||||
https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip
|
||||
ESP32Async/ESPAsyncWebServer @ 3.10.3
|
||||
file://arch/esp32/AsyncElegantOTA
|
||||
lvgl/lvgl @ ^8.3.11
|
||||
bodmer/TFT_eSPI @ ^2.5.43
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
knolleary/PubSubClient @ ^2.8.0
|
||||
|
||||
; ======================================================================
|
||||
; LilyGo T-Deck / T-Deck Plus — flattened equivalent of meshcomod's
|
||||
; LilyGo_TDeck_companion_radio_touch. ST7789 via Adafruit (not TFT_eSPI);
|
||||
|
||||
+21
-16
@@ -1,24 +1,29 @@
|
||||
# beta_45 (TEST) - three new boards (LilyGo T-LoRa Pager, LilyGo T-Display P4, Heltec V4-R8), plus fixes.
|
||||
# beta_45 (STABLE) — everything since the last stable (beta_40): browser-based remote control, an on-device web browser, three new boards, and a batch of fixes.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- Community ---
|
||||
Big thanks to @codemonkeybr, who contributed the entire LilyGo T-LoRa Pager port this cycle.
|
||||
# --- New: control wadamesh from a browser (over Wi-Fi — no app, account or cloud) ---
|
||||
New web control panel: run the device from a phone or laptop browser over Wi-Fi, with full Chats, Contacts, Discovered, Terminal and Settings tabs — served by the device itself. Turn on the VNC app, then open http://<device-ip>:8765/ from any browser on the same network.
|
||||
VNC app: mirrors the live device screen into the browser and lets you drive it — taps and typing in the browser control the device, and anything that changes on the device shows up in the browser.
|
||||
REMOTE app: a headless mode that renders the whole UI at a larger, browser-friendly resolution over Wi-Fi while the device screen shows the address to open; a Rotate button flips between desktop (landscape) and phone (portrait) layouts.
|
||||
Web Contacts: search, sort and filter contacts, see each one's distance, coordinates and last-heard age, and get the same per-contact actions as on the device (favourite, block, reset path, remove). Discovered nodes appear too, each with a one-tap "Add".
|
||||
Web Chats: pop-up notifications (with an optional beep) when a message arrives, the open chat auto-refreshes as replies land, and acknowledgements + repeats-heard show for messages you send from the browser.
|
||||
|
||||
# --- New board: LilyGo T-LoRa Pager (by @codemonkeybr, PR #149) ---
|
||||
New board: the LilyGo T-LoRa Pager (ESP32-S3) - a QWERTY-keyboard handheld with a rotary encoder and no touchscreen. The whole UI is driven from the keyboard and the encoder, both radio variants are supported (LR1121 and SX1262), and everything was verified on real hardware against a live mesh.
|
||||
Pager controls: the rotary encoder scrolls and its click opens menus; a universal back gesture, screen lock, keyboard backlight modes, an accent-character picker and an @-mention picker are all keyboard/encoder driven. There is a full shortcut reference on the site.
|
||||
# --- New: on-device web browser + links in chat ---
|
||||
New "Web" app (8 MB boards — T-Deck, Tanmatsu): open a URL and read the page as plain readable text — no proxy, tracking, JavaScript or images — with tappable links and back/forward/refresh.
|
||||
Links in a chat are now tappable: "Open in web" loads the page in the on-device browser, and "Create QR" shows a scannable QR to open the link on your phone (works on every board, including the Heltec V4).
|
||||
|
||||
# --- New board: LilyGo T-Display P4 ---
|
||||
New board: the LilyGo T-Display P4 (ESP32-P4) - an AMOLED touchscreen handheld. Early port, marked "partially supported": LoRa mesh, chat, the map, GPS and the phone-app companion over Wi-Fi/USB all work. Bluetooth pairing is not available on this board (a limitation of its stock radio-coprocessor firmware); pair over Wi-Fi or USB instead.
|
||||
T-Display P4: screen brightness and notification sound now work and have Control-Center sliders, and the browser-based screen mirror / remote apps work over Wi-Fi.
|
||||
|
||||
# --- New board: Heltec V4-R8 ---
|
||||
New board: the Heltec WiFi LoRa 32 V4-R8 (8 MB PSRAM) with the Expansion Kit V2 (microSD). Marked "partially supported" while it finishes hardware testing.
|
||||
# --- New boards ---
|
||||
LilyGo T-LoRa Pager (ESP32-S3): a QWERTY-keyboard handheld with a rotary encoder and no touchscreen, fully driven from the keyboard + encoder; both radio variants (LR1121 and SX1262) supported. Contributed by @codemonkeybr (PR #149) — thank you!
|
||||
LilyGo T-Display P4 (ESP32-P4): an AMOLED touchscreen handheld. Early port ("partially supported") — LoRa mesh, chat, map, GPS and the phone-app companion over Wi-Fi/USB work; Bluetooth pairing isn't available on this board (stock radio-coprocessor limitation — pair over Wi-Fi or USB).
|
||||
Heltec WiFi LoRa 32 V4-R8 (8 MB PSRAM) with Expansion Kit V2 (microSD): "partially supported" while hardware testing finishes.
|
||||
|
||||
# --- Fixed ---
|
||||
Heltec V4-R8: fixed random hard-freezes that needed a reset - the touch controller was sharing I2C pins with the clock and sensors in a way that could wedge the bus. It no longer does.
|
||||
Tanmatsu (and other keyboard-only boards): you can now select and edit the Wi-Fi network name and password fields in Wi-Fi settings - the join screen's fields were previously unreachable with the keyboard.
|
||||
T-Display P4: the clock now sets itself over Wi-Fi even where NTP is blocked, and won't get stuck on a bad date reported by the hardware clock.
|
||||
Heltec V4: map tiles now fill the whole map instead of only the top — the 2 MB-PSRAM V4 was running out of contiguous memory for the tile grid, so tiles now reuse a small fixed pool and only load what's on screen.
|
||||
Heltec V4-R8: fixed random hard-freezes that needed a reset (the touch controller was wedging the shared I2C bus).
|
||||
Tanmatsu and other keyboard-only boards: you can now select and edit the Wi-Fi name and password fields in Wi-Fi settings.
|
||||
T-Display P4: the clock sets itself over Wi-Fi even where NTP is blocked, and won't get stuck on a bad date from the hardware clock.
|
||||
French (AZERTY) on-screen keyboard: the spacebar is back in the normal letter view.
|
||||
REMOTE mode now works on the Heltec V4 TFT (and other 2 MB-PSRAM boards); keyboard-less boards can leave REMOTE by holding the screen ~3 s, and the web page has an "Exit remote" button.
|
||||
|
||||
# --- Also ---
|
||||
The flasher site now shows a support badge on every board (fully vs partially supported); hover it for what works.
|
||||
The flasher site shows a support badge on every board (fully vs partially supported); hover it to see what works.
|
||||
@@ -0,0 +1,12 @@
|
||||
# beta_46 — control-center redesign, Do Not Disturb, contact safety + board fixes
|
||||
|
||||
# New
|
||||
Redesigned control center: round quick-toggle buttons that spread to fill the panel, a frosted-glass look, real lock/bell icons, and a confirmation popup on every toggle.
|
||||
Do Not Disturb: set quiet hours in Settings > Sound to silence the incoming-message chime during a daily window, with a bell in the status bar while it's active. (community contribution by Tesso Codemonkey)
|
||||
Do Not Disturb quick-toggle in the control center — a bell that gains a slash while quiet hours are on.
|
||||
|
||||
# Fixed
|
||||
Contacts no longer go missing between reboots on microSD devices: saves are now atomic (written then swapped in) and the card is mounted far more reliably at boot.
|
||||
Heltec V4-R8: the microSD card now mounts, and the freeze when waking the device from sleep is fixed.
|
||||
LilyGo T-Pager: the map no longer shows a "storage error" under the launcher — it caches tiles to the microSD card, like the T-Deck.
|
||||
Heltec V4 in portrait: the Bluetooth icon no longer overlaps the clock in the status bar, and the control-center buttons no longer clip when tapped.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,14 @@
|
||||
# beta_48 - stability and fixes: crash, boot-stuck, MQTT, message times, keyboard and more
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# Fixed
|
||||
Fixed a crash that could reboot the device while you were typing in a chat.
|
||||
Fixed devices getting stuck on the boot screen after an update when a microSD card is fitted. The boot-time copy of your data to the card could hang on a slow or damaged card; it is now guarded and the device always boots.
|
||||
MQTT bridge settings now save and survive a reboot. They were being written to the wrong storage and getting lost.
|
||||
Message times now match the clock at the top of the screen, instead of showing 1910 or other wrong dates.
|
||||
Dutch keyboard: the "1" and "!" keys were typing "ij" and "IJ". Fixed.
|
||||
The message box now stops you at the real send limit instead of quietly dropping the end of a long message when you send.
|
||||
Copy and paste no longer cuts off long text.
|
||||
The quick messages list now scrolls when there are more entries than fit on screen.
|
||||
Room server names that start with an unusual character no longer show an empty box on the Join screen.
|
||||
Tanmatsu: you can now reach every emoji in the picker, not just the first half.
|
||||
@@ -0,0 +1,13 @@
|
||||
# beta_49 - T-Display P4 only
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
This beta only has T-Display P4 fixes. If you run any other board there is nothing new for you here, stay on beta_48.
|
||||
|
||||
# Fixed (T-Display P4)
|
||||
The Snake game no longer traps you. Tap the top bar to leave it. Before, the only way out was a reset.
|
||||
The full-screen tools (Monitor, Spectrum, Discover, Web mirror, Advert) can now be left by tapping the top bar, so they no longer trap you either.
|
||||
The screen timeout now works. The display turns off after your idle time and wakes on touch. This also fixes standby and burn-in dimming.
|
||||
Text no longer overflows its boxes. The interface is pinned to the 100% size that fits the panel.
|
||||
GPS: the L76K module is now found reliably and read at 9600 baud. You still need the GPS antenna connected and a clear view of the sky to get a fix.
|
||||
The control center dropdown is taller so the GPS status line sits below the buttons instead of behind them.
|
||||
P4 LCD model only: fixed the black screen. The backlight now turns on. Please test and report.
|
||||
@@ -0,0 +1,13 @@
|
||||
# beta_50 - on-device updates for every board, plus pager and Attaky fixes
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# On-device updates
|
||||
Fixed the on-device updater downloading the wrong firmware on several boards. The V4-R8, ThinkNode M9, RAK Tap V2, T-LoRa Pager and Attaky now each pull their own build instead of the Heltec V4 TFT one.
|
||||
T-Display P4: the "Get test builds (beta)" toggle and the on-device update now appear. They were hidden before, so the P4 could only be updated over USB. Newly enabled, please test and report.
|
||||
|
||||
# T-LoRa Pager (codemonkeybr)
|
||||
Lock screen fixed: tap Backspace to peek the screen without unlocking, hold Backspace to unlock. The clock and text are repositioned to fit the wide screen and stay readable.
|
||||
|
||||
# Attaky Mesh Series (attakygit)
|
||||
On-screen keyboard now works with the detachable keyboard module: the keys stay down while the module is attached, and the module's # key brings them back up for symbols. The POWER button toggles the screen and wakes the board.
|
||||
Board fixes: correct LoRa clock (no false TCXO), GPS baud, and sensor probing that was colliding with the touch and keyboard chips.
|
||||
@@ -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
@@ -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
|
||||
|
||||
+52
-4
@@ -420,6 +420,23 @@ bool DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
}
|
||||
|
||||
void DataStore::loadContacts(DataStoreHost* host) {
|
||||
#if defined(ESP32)
|
||||
// Recover an atomic-save swap interrupted by power loss: if the live file is gone but the
|
||||
// fully-written temp survives, adopt it. A temp alongside an intact live file is a stale
|
||||
// leftover (save crashed after writing temp but before the swap) — keep live, drop temp.
|
||||
{
|
||||
FILESYSTEM* cfs = _getContactsChannelsFS();
|
||||
char live[80], tmp[80];
|
||||
if (_root[0]) { snprintf(live, sizeof live, "%s/contacts3", _root);
|
||||
snprintf(tmp, sizeof tmp, "%s/contacts3.tmp", _root); }
|
||||
else { strncpy(live, "/contacts3", sizeof live);
|
||||
strncpy(tmp, "/contacts3.tmp", sizeof tmp); }
|
||||
if (cfs->exists(tmp)) {
|
||||
if (!cfs->exists(live)) cfs->rename(tmp, live); // interrupted swap -> recover the temp
|
||||
else cfs->remove(tmp); // stale temp -> the live file wins
|
||||
}
|
||||
}
|
||||
#endif
|
||||
File file = openRead(_getContactsChannelsFS(), "/contacts3");
|
||||
if (file) {
|
||||
bool full = false;
|
||||
@@ -462,8 +479,21 @@ void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
|
||||
// (FAT has no such GC); it matters most for card-less SPIFFS devices.
|
||||
WdtHeavyGuard _wdt;
|
||||
#endif
|
||||
File file = openWrite(_getContactsChannelsFS(), "/contacts3");
|
||||
#if defined(ESP32)
|
||||
// Write to a TEMP file and swap it in only after it is FULLY written, so a partial
|
||||
// write — a multi-second SPIFFS GC stall or an SD hiccup mid-save — can never truncate
|
||||
// the operator's live contact list. Without this, openWrite("/contacts3") truncates the
|
||||
// real file up front, so any failed write permanently destroyed the list (the "lost all
|
||||
// my repeaters overnight" report). Other platforms keep the direct write (LittleFS, and
|
||||
// untested here). The swap + boot-recovery live after the write loop below.
|
||||
const char* kContactsWriteTarget = "/contacts3.tmp";
|
||||
#else
|
||||
const char* kContactsWriteTarget = "/contacts3";
|
||||
#endif
|
||||
File file = openWrite(_getContactsChannelsFS(), kContactsWriteTarget);
|
||||
bool wrote_ok = false; // true only if every record reached the file
|
||||
if (file) {
|
||||
bool ok = true; // cleared on any short write (partial file)
|
||||
uint32_t idx = 0;
|
||||
ContactInfo c;
|
||||
uint8_t unused = 0;
|
||||
@@ -478,7 +508,6 @@ void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
|
||||
uint8_t* buf = (uint8_t*)malloc(REC * CHUNK_RECS);
|
||||
if (buf) {
|
||||
size_t fill = 0;
|
||||
bool ok = true;
|
||||
while (ok && host->getContactForSave(idx, c)) {
|
||||
if (filter && !filter(c)) {
|
||||
idx++; // advance to next contact
|
||||
@@ -504,7 +533,7 @@ void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
|
||||
}
|
||||
idx++; // advance to next contact
|
||||
}
|
||||
if (ok && fill > 0) file.write(buf, fill);
|
||||
if (ok && fill > 0) ok = (file.write(buf, fill) == fill);
|
||||
free(buf);
|
||||
} else {
|
||||
while (host->getContactForSave(idx, c)) {
|
||||
@@ -525,13 +554,32 @@ void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
|
||||
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
|
||||
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);
|
||||
|
||||
if (!success) break; // write failed
|
||||
if (!success) { ok = false; break; } // write failed -> keep the live file
|
||||
|
||||
idx++; // advance to next contact
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
wrote_ok = ok;
|
||||
}
|
||||
#if defined(ESP32)
|
||||
// Commit the temp only if it was written IN FULL; otherwise keep the existing live list.
|
||||
// (_rp uses one shared buffer, so build both paths explicitly to avoid clobbering it.)
|
||||
{
|
||||
FILESYSTEM* cfs = _getContactsChannelsFS();
|
||||
char live[80], tmp[80];
|
||||
if (_root[0]) { snprintf(live, sizeof live, "%s/contacts3", _root);
|
||||
snprintf(tmp, sizeof tmp, "%s/contacts3.tmp", _root); }
|
||||
else { strncpy(live, "/contacts3", sizeof live);
|
||||
strncpy(tmp, "/contacts3.tmp", sizeof tmp); }
|
||||
if (wrote_ok) {
|
||||
cfs->remove(live); // FAT/SPIFFS rename won't overwrite an existing target
|
||||
cfs->rename(tmp, live); // if this rename fails, loadContacts recovers the temp at next boot
|
||||
} else {
|
||||
cfs->remove(tmp); // discard the partial temp; the live contact list is untouched
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void DataStore::loadChannels(DataStoreHost* host) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
MqttBridge mqtt_bridge;
|
||||
|
||||
#if !defined(HAS_TANMATSU) && !defined(HAS_TDISPLAY_P4) // ---- real PubSubClient/WiFiClient implementation; NOT built on the P4 boards ----
|
||||
#include <Preferences.h>
|
||||
#include "SdNvsPrefs.h" // NOT raw NVS Preferences: the touch firmware abandoned NVS (tiny, shared,
|
||||
// doesn't survive Launcher) for a file-backed store. MQTT was the last setting
|
||||
// still on NVS, so its writes silently failed / didn't persist (GH #128).
|
||||
#include <WiFi.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -42,7 +44,7 @@ void MqttBridge::loadConfig() {
|
||||
_host[0] = _user[0] = _pwd[0] = _psk[0] = '\0';
|
||||
_port = 1883;
|
||||
|
||||
Preferences p;
|
||||
SdNvsPrefs p;
|
||||
if (p.begin("mqtt", true)) {
|
||||
_enabled = p.getBool("en", false);
|
||||
_port = (uint16_t)p.getUInt("port", 1883);
|
||||
@@ -223,7 +225,7 @@ void MqttBridge::publishChannel(int channelIdx, const char* channelName,
|
||||
void MqttBridge::saveConfig(const char* host, uint16_t port,
|
||||
const char* user, const char* pwd,
|
||||
bool pubDm, bool pubChannel, const char* psk, bool enable) {
|
||||
Preferences p;
|
||||
SdNvsPrefs p;
|
||||
if (!p.begin("mqtt", false)) return;
|
||||
p.putBool("en", enable);
|
||||
p.putString("host", host);
|
||||
|
||||
@@ -1822,6 +1822,20 @@ uint8_t touchPrefsGetSoundVolume() {
|
||||
return v > 100 ? 100 : v;
|
||||
}
|
||||
void touchPrefsSetSoundVolume(uint8_t vol) { if (vol > 100) vol = 100; if (!s_begun) touchPrefsBegin(); prefsPutUChar("snd_vol", vol); }
|
||||
bool touchPrefsGetDndEnabled() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("dnd_en", 0) != 0; }
|
||||
void touchPrefsSetDndEnabled(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("dnd_en", on ? 1 : 0); }
|
||||
uint8_t touchPrefsGetDndStartSlot() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
uint8_t s = s_prefs.getUChar("dnd_ss", 44);
|
||||
return s > 47 ? 47 : s;
|
||||
}
|
||||
void touchPrefsSetDndStartSlot(uint8_t slot) { if (slot > 47) slot = 47; if (!s_begun) touchPrefsBegin(); prefsPutUChar("dnd_ss", slot); }
|
||||
uint8_t touchPrefsGetDndEndSlot() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
uint8_t s = s_prefs.getUChar("dnd_es", 12);
|
||||
return s > 47 ? 47 : s;
|
||||
}
|
||||
void touchPrefsSetDndEndSlot(uint8_t slot) { if (slot > 47) slot = 47; if (!s_begun) touchPrefsBegin(); prefsPutUChar("dnd_es", slot); }
|
||||
uint8_t touchPrefsGetKbdBacklight() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
uint8_t v = s_prefs.getUChar("kbd_bl", 100);
|
||||
|
||||
@@ -378,6 +378,20 @@ void touchPrefsSetSoundDirect(bool on);
|
||||
uint8_t touchPrefsGetSoundVolume(); // 0..100, default 70
|
||||
void touchPrefsSetSoundVolume(uint8_t vol);
|
||||
|
||||
/** Do Not Disturb: silences the incoming-message chime during a daily time
|
||||
* window. Start/end are half-hour slots (0..47, slot = hour*2 + (min>=30)),
|
||||
* so the window can be set in 30-minute steps. The window may wrap past
|
||||
* midnight (start > end means "start..23:59 AND 00:00..end"); start == end
|
||||
* is a zero-length "never active" window. Only gates the sound chokepoint in
|
||||
* newMsgImpl() — chat bubbles/badges/unread counts are unaffected, and
|
||||
* Settings-page sound previews still play while DND is on. */
|
||||
bool touchPrefsGetDndEnabled(); // default false
|
||||
void touchPrefsSetDndEnabled(bool on);
|
||||
uint8_t touchPrefsGetDndStartSlot(); // 0..47, default 44 (22:00)
|
||||
void touchPrefsSetDndStartSlot(uint8_t slot);
|
||||
uint8_t touchPrefsGetDndEndSlot(); // 0..47, default 12 (06:00)
|
||||
void touchPrefsSetDndEndSlot(uint8_t slot);
|
||||
|
||||
/** Per-event notification sound FILE (empty = built-in chime). Slot:
|
||||
* 0 = message, 1 = direct/DM, 2 = @-mention. Stored as a path pref like the
|
||||
* lock wallpaper ("" | "/sounds/x.wav" | "sd:/x.wav"). T-Deck only. */
|
||||
|
||||
+68
-11
@@ -177,6 +177,10 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
|
||||
#if defined(ESP32)
|
||||
volatile int g_boot_phase = 0;
|
||||
extern "C" void set_boot_phase(int phase) { g_boot_phase = phase; }
|
||||
// True once contacts/channels are actually routed to the SD card at boot (SD mounted). The
|
||||
// "Store data on SD" toggle is only a stored intent — if the card didn't mount at boot, contacts
|
||||
// silently stay on internal flash. The Storage settings page reads this to show the REAL location.
|
||||
bool g_contacts_on_sd = false;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -230,15 +234,24 @@ bool meshcomodMigrateSpiffsToSd(bool force) {
|
||||
File s = SPIFFS.open(src, FILE_READ);
|
||||
File d = s ? SD.open(dst, FILE_WRITE) : File(); // FILE_WRITE truncates
|
||||
bool ok = s && d;
|
||||
size_t since_feed = 0;
|
||||
while (ok && s.available()) {
|
||||
const size_t n = s.read(buf, sizeof buf);
|
||||
if (n == 0 || d.write(buf, n) != n) ok = false;
|
||||
// Feed the task-WDT mid-file: a large history file on a slow/cold SD card can take
|
||||
// longer than the 20 s task-WDT to copy in 4 KB chunks, and this runs on loopTask
|
||||
// during setup() — without the feed the dog panics and reboot-loops the boot (GH
|
||||
// #142/#148). The copy still can't tolerate a genuinely wedged FAT layer; the
|
||||
// NVS breadcrumb at the call site handles that.
|
||||
since_feed += n;
|
||||
if (since_feed >= 32768) { esp_task_wdt_reset(); since_feed = 0; }
|
||||
}
|
||||
if (s) s.close();
|
||||
if (d) d.close();
|
||||
if (ok) { ++copied; if (strcmp(src, "/identity/_main.id") == 0) identity_ok = true; }
|
||||
else { ++failed; Serial.printf("[BOOT] SD migrate FAILED: %s\n", src); if (SD.exists(dst)) SD.remove(dst); }
|
||||
f.close();
|
||||
esp_task_wdt_reset();
|
||||
yield();
|
||||
}
|
||||
root.close();
|
||||
@@ -249,6 +262,14 @@ bool meshcomodMigrateSpiffsToSd(bool force) {
|
||||
copied, failed, identity_ok ? "ok" : "MISSING");
|
||||
return identity_ok;
|
||||
}
|
||||
|
||||
// Clear the boot safe-mode latch (see the SPIFFS->SD migration above): called after a
|
||||
// successful manual "Copy internal data to SD" so a deliberate retry re-arms boot-time
|
||||
// auto-adoption. The boot path re-latches on its own if a later migration wedges. GH #142/#148.
|
||||
void meshcomodClearSdMigLatch() {
|
||||
Preferences _mp;
|
||||
if (_mp.begin("touch", false)) { _mp.remove("sd_mig_busy"); _mp.end(); }
|
||||
}
|
||||
#endif
|
||||
|
||||
void setup() {
|
||||
@@ -479,20 +500,32 @@ void setup() {
|
||||
#endif
|
||||
bool sd_mounted = false;
|
||||
if (_spi) {
|
||||
// Try to mount the card on EVERY boot now (not just the full-adoption case):
|
||||
// even a device that keeps identity on SPIFFS wants its churn-heavy
|
||||
// contacts/channels on the card. Full 4-try ladder for the cold-card
|
||||
// first-run adoption; a shorter 2-try for the contacts-overflow case so a
|
||||
// card-less device only pays ~260 ms.
|
||||
int tries = want_full_sd ? 4 : 2;
|
||||
for (int a = 0; a < tries && !sd_mounted; ++a) { // short mount ladder (cold cards)
|
||||
// Try to mount the card on EVERY boot: even a device that keeps identity on SPIFFS
|
||||
// wants its churn-heavy contacts/channels on the card.
|
||||
//
|
||||
// Use the SAME forgiving ladder the map-tile mount (fmSdTryMount) uses — dropping to
|
||||
// 1 MHz then 400 kHz — not a fast 4 MHz-only ladder. A cold / cheap / slow-to-wake card
|
||||
// fails a 4 MHz-only mount here but later succeeds the slow tile mount, so contacts got
|
||||
// silently pushed back to SPIFFS *even though the toggle said SD and tiles worked* — then
|
||||
// SPIFFS churn eventually lost them (gubbinsgalore's "100 repeaters gone overnight").
|
||||
// Matching the tile ladder means contacts land on the card wherever tiles do. A card-less
|
||||
// device still bails fast (3 quick tries at 4 MHz, ~480 ms); only a present-but-cold card
|
||||
// walks down to the slow clocks. delay() feeds the task WDT, so the ~2.8 s worst case
|
||||
// (present cold card only) doesn't trip it.
|
||||
static const struct { uint16_t settle_ms; uint32_t hz; } kBootMount[] = {
|
||||
{ 40, 4000000 }, { 220, 4000000 }, { 220, 4000000 },
|
||||
{ 300, 1000000 }, { 450, 1000000 }, { 650, 400000 }, { 900, 400000 },
|
||||
};
|
||||
int tries = want_full_sd ? 7 : 3;
|
||||
for (int a = 0; a < tries && !sd_mounted; ++a) {
|
||||
SD.end();
|
||||
delay(a == 0 ? 40 : 220);
|
||||
if (SD.begin(PIN_SD_CS, *_spi, 4000000, "/sd", 3) && SD.cardType() != CARD_NONE)
|
||||
delay(kBootMount[a].settle_ms);
|
||||
if (SD.begin(PIN_SD_CS, *_spi, kBootMount[a].hz, "/sd", 3) && SD.cardType() != CARD_NONE)
|
||||
sd_mounted = true;
|
||||
}
|
||||
}
|
||||
if (sd_mounted) {
|
||||
g_contacts_on_sd = true; // every branch below routes contacts/channels to the card
|
||||
if (want_full_sd) {
|
||||
// beta_36 upgrade heal: adopting the card while the LIVE data still sits
|
||||
// on SPIFFS (the pre-beta_36 "toggle ignored" state) must migrate FIRST,
|
||||
@@ -502,8 +535,26 @@ void setup() {
|
||||
// rather than adopting a card without the identity on it.
|
||||
bool adopt = true;
|
||||
if (SPIFFS.exists("/identity/_main.id") && !SD.exists("/meshcomod/identity/_main.id")) {
|
||||
adopt = meshcomodMigrateSpiffsToSd(false);
|
||||
if (!adopt) Serial.println("[BOOT] SD migration incomplete -> staying on SPIFFS this boot");
|
||||
// Boot safe-mode (GH #142/#148): a wedged or corrupt SD card can hang / WDT-reboot the
|
||||
// device mid-migration, stranding it on the boot screen EVERY boot (reset can't escape,
|
||||
// only a downgrade could). Drop an NVS breadcrumb before migrating and clear it only if
|
||||
// the copy RETURNS. If it's still set at the next boot, the last migration never finished
|
||||
// -> skip it and boot from SPIFFS (the data is safe there); Settings > "Copy internal data
|
||||
// to SD" re-arms a deliberate retry. A merely-slow (healthy) card completes thanks to the
|
||||
// in-loop WDT feed, so it never latches here.
|
||||
Preferences _mp;
|
||||
const bool mp_ok = _mp.begin("touch", false);
|
||||
const bool mig_busy = mp_ok && _mp.getBool("sd_mig_busy", false);
|
||||
if (mig_busy) {
|
||||
if (mp_ok) _mp.end();
|
||||
adopt = false;
|
||||
Serial.println("[BOOT] prior SD migration did not complete -> skipping (staying on SPIFFS); retry via Settings > Copy internal data to SD");
|
||||
} else {
|
||||
if (mp_ok) { _mp.putBool("sd_mig_busy", true); _mp.end(); }
|
||||
adopt = meshcomodMigrateSpiffsToSd(false);
|
||||
Preferences _mp2; if (_mp2.begin("touch", false)) { _mp2.remove("sd_mig_busy"); _mp2.end(); }
|
||||
if (!adopt) Serial.println("[BOOT] SD migration incomplete -> staying on SPIFFS this boot");
|
||||
}
|
||||
}
|
||||
if (adopt) {
|
||||
sd_storage = store.useSdStorage();
|
||||
@@ -694,8 +745,14 @@ void setup() {
|
||||
// hit the overflow) still get the big ring; default GPS-off keeps the stock 256 B. A user
|
||||
// who enables GPS mid-session picks it up on the next reboot (gps_enabled is persisted).
|
||||
{
|
||||
#if defined(ATTAKY_MESH_SERIES)
|
||||
// This fixed stack always carries the GPS, so take the larger RX ring
|
||||
// unconditionally; the default 256 B ring gives the slowest first fix.
|
||||
Serial1.setRxBufferSize(4096);
|
||||
#else
|
||||
auto* np = the_mesh.getNodePrefs();
|
||||
if (np && np->gps_enabled) Serial1.setRxBufferSize(4096);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
sensors.begin();
|
||||
|
||||
@@ -626,8 +626,9 @@ static const char* hw_de_upper[26] = {
|
||||
static const char* hw_de_digits[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr };
|
||||
static const char* hw_de_digits_shift[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr };
|
||||
|
||||
/* Dutch keeps the standard alpha matrix but exposes the common IJ digraph on a
|
||||
* digit slot so compact hardware still gets a language-specific shortcut. */
|
||||
/* Dutch keeps the standard alpha matrix. (The IJ digraph used to sit on a hardware
|
||||
* digit slot, but slot 1 is the "1" key AND the back-mapped "!" key, so it broke both
|
||||
* — GH #144/#164 — and the digraph now lives only on the on-screen keyboard's key.) */
|
||||
static const char* hw_nl_lower[26] = {
|
||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
|
||||
@@ -638,8 +639,8 @@ static const char* hw_nl_upper[26] = {
|
||||
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z"
|
||||
};
|
||||
static const char* hw_nl_digits[10] = { nullptr, "ij", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
|
||||
static const char* hw_nl_digits_shift[10] = { nullptr, "IJ", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
|
||||
static const char* hw_nl_digits[10] = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
|
||||
static const char* hw_nl_digits_shift[10] = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
|
||||
|
||||
/* Spanish stays close to US QWERTY; use the number row to expose n-tilde on
|
||||
* compact hardware where the semicolon key is absent. */
|
||||
|
||||
@@ -37,6 +37,18 @@ void SnakeGame::launch() {
|
||||
if (!g->open()) { s_active = nullptr; delete g; }
|
||||
}
|
||||
|
||||
void SnakeGame::dismiss() {
|
||||
// Close from outside the game (a status-bar tap). The in-game X can sit UNDER a taller status
|
||||
// bar on big-screen boards (the P4's bar is set taller at boot + raised over the game), leaving
|
||||
// no reachable exit — this is the guaranteed way out. Mirrors closeCb's teardown. Safe to del
|
||||
// synchronously here: the triggering event target is the status bar, not a Snake object.
|
||||
if (!s_active) return;
|
||||
SnakeGame* g = s_active;
|
||||
s_active = nullptr;
|
||||
g->close();
|
||||
delete g;
|
||||
}
|
||||
|
||||
void SnakeGame::placeFood() {
|
||||
const int cells = cols_ * rows_;
|
||||
for (int tries = 0; tries < 4 * cells + 16; ++tries) {
|
||||
|
||||
@@ -17,6 +17,7 @@ public:
|
||||
static void launch(); // open (no-op if already open)
|
||||
static bool isOpen(); // UITask: gate the trackball + tab bar
|
||||
static void steer(int dx, int dy); // UITask: trackball motion -> direction
|
||||
static void dismiss(); // close from outside (status-bar tap) — guaranteed exit
|
||||
|
||||
private:
|
||||
// The playfield grid is sized to the screen at open() (cols_ x rows_), capped
|
||||
|
||||
+1428
-123
File diff suppressed because it is too large.
Load diff
@@ -167,7 +167,7 @@ private:
|
||||
bool _composer_mode;
|
||||
int _composer_char_idx;
|
||||
int _composer_action_idx;
|
||||
char _compose_buf[128];
|
||||
char _compose_buf[MAX_MSG_TEXT + 1]; // full LoRa text length; was 128 -> silently chopped sends at 127 bytes (GH #119)
|
||||
unsigned long _next_mesh_thread_refresh;
|
||||
TouchUiScreen _touch_screen;
|
||||
bool _active_dm_contact_set;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,175 @@
|
||||
/*******************************************************************************
|
||||
* Size: 16 px
|
||||
* Bpp: 4
|
||||
* Opts: --no-compress --bpp 4 --size 16 --font fa-solid-900.ttf -r 0xF023 -r 0xF0F3 -r 0xF1F6 --format lvgl --lv-font-name cc_icons_16 --no-kerning -o /Users/kaj/wadamesh/src/ui-touch/cc_icons_16.c
|
||||
******************************************************************************/
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
#ifndef CC_ICONS_16
|
||||
#define CC_ICONS_16 1
|
||||
#endif
|
||||
|
||||
#if CC_ICONS_16
|
||||
|
||||
/*-----------------
|
||||
* BITMAPS
|
||||
*----------------*/
|
||||
|
||||
/*Store the image of the glyphs*/
|
||||
static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
|
||||
/* U+F023 "" */
|
||||
0x0, 0x0, 0x3b, 0xff, 0xb3, 0x0, 0x0, 0x0,
|
||||
0x5, 0xff, 0xff, 0xff, 0x50, 0x0, 0x0, 0x1f,
|
||||
0xf8, 0x11, 0x8f, 0xe1, 0x0, 0x0, 0x5f, 0xc0,
|
||||
0x0, 0xc, 0xf5, 0x0, 0x0, 0x7f, 0x80, 0x0,
|
||||
0x8, 0xf7, 0x0, 0x0, 0x8f, 0x80, 0x0, 0x8,
|
||||
0xf8, 0x0, 0x4e, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xe4, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xfe, 0x4e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe4,
|
||||
|
||||
/* U+F0F3 "" */
|
||||
0x0, 0x0, 0x0, 0xc, 0xc0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x3f, 0xf3, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x1c, 0xff, 0xff, 0xc1, 0x0, 0x0,
|
||||
0x0, 0x1, 0xef, 0xff, 0xff, 0xfe, 0x10, 0x0,
|
||||
0x0, 0x8, 0xff, 0xff, 0xff, 0xff, 0x80, 0x0,
|
||||
0x0, 0xe, 0xff, 0xff, 0xff, 0xff, 0xd0, 0x0,
|
||||
0x0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x0,
|
||||
0x0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x0,
|
||||
0x0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x0,
|
||||
0x0, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0,
|
||||
0x2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20,
|
||||
0xc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
|
||||
0xb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0xef, 0xfe, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x4e, 0xe4, 0x0, 0x0, 0x0,
|
||||
|
||||
/* U+F1F6 "" */
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0xd, 0x90, 0x0, 0x0, 0x0,
|
||||
0xcc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xfc,
|
||||
0x10, 0x0, 0x3, 0xff, 0x30, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x4f, 0xe4, 0x2, 0xcf, 0xff, 0xfc,
|
||||
0x10, 0x0, 0x0, 0x0, 0x0, 0x2, 0xdf, 0x8e,
|
||||
0xff, 0xff, 0xff, 0xe1, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0xa, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x6f, 0xff, 0xff,
|
||||
0xff, 0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x3, 0xef, 0xff, 0xff, 0xff, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0xa0, 0x1c, 0xff, 0xff, 0xff,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xfd, 0x20,
|
||||
0x9f, 0xff, 0xff, 0x30, 0x0, 0x0, 0x0, 0x0,
|
||||
0x8, 0xff, 0xf4, 0x5, 0xff, 0xff, 0x80, 0x0,
|
||||
0x0, 0x0, 0x0, 0x2f, 0xff, 0xff, 0x80, 0x2d,
|
||||
0xff, 0xf2, 0x0, 0x0, 0x0, 0x0, 0xcf, 0xff,
|
||||
0xff, 0xfb, 0x11, 0xbf, 0xfc, 0x0, 0x0, 0x0,
|
||||
0x0, 0xbf, 0xff, 0xff, 0xff, 0xd2, 0x7, 0xff,
|
||||
0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x4e, 0xf4, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0xe, 0xff, 0xe0, 0x0, 0x2, 0xcf, 0x70,
|
||||
0x0, 0x0, 0x0, 0x0, 0x4, 0xee, 0x40, 0x0,
|
||||
0x0, 0x9, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0
|
||||
};
|
||||
|
||||
|
||||
/*---------------------
|
||||
* GLYPH DESCRIPTION
|
||||
*--------------------*/
|
||||
|
||||
static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
|
||||
{.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */,
|
||||
{.bitmap_index = 0, .adv_w = 224, .box_w = 14, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
|
||||
{.bitmap_index = 112, .adv_w = 224, .box_w = 16, .box_h = 16, .ofs_x = -1, .ofs_y = -2},
|
||||
{.bitmap_index = 240, .adv_w = 320, .box_w = 22, .box_h = 18, .ofs_x = -1, .ofs_y = -3}
|
||||
};
|
||||
|
||||
/*---------------------
|
||||
* CHARACTER MAPPING
|
||||
*--------------------*/
|
||||
|
||||
static const uint16_t unicode_list_0[] = {
|
||||
0x0, 0xd0, 0x1d3
|
||||
};
|
||||
|
||||
/*Collect the unicode lists and glyph_id offsets*/
|
||||
static const lv_font_fmt_txt_cmap_t cmaps[] =
|
||||
{
|
||||
{
|
||||
.range_start = 61475, .range_length = 468, .glyph_id_start = 1,
|
||||
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 3, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*--------------------
|
||||
* ALL CUSTOM DATA
|
||||
*--------------------*/
|
||||
|
||||
#if LVGL_VERSION_MAJOR == 8
|
||||
/*Store all the custom data of the font*/
|
||||
static lv_font_fmt_txt_glyph_cache_t cache;
|
||||
#endif
|
||||
|
||||
#if LVGL_VERSION_MAJOR >= 8
|
||||
static const lv_font_fmt_txt_dsc_t font_dsc = {
|
||||
#else
|
||||
static lv_font_fmt_txt_dsc_t font_dsc = {
|
||||
#endif
|
||||
.glyph_bitmap = glyph_bitmap,
|
||||
.glyph_dsc = glyph_dsc,
|
||||
.cmaps = cmaps,
|
||||
.kern_dsc = NULL,
|
||||
.kern_scale = 0,
|
||||
.cmap_num = 1,
|
||||
.bpp = 4,
|
||||
.kern_classes = 0,
|
||||
.bitmap_format = 0,
|
||||
#if LVGL_VERSION_MAJOR == 8
|
||||
.cache = &cache
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*-----------------
|
||||
* PUBLIC FONT
|
||||
*----------------*/
|
||||
|
||||
/*Initialize a public general font descriptor*/
|
||||
#if LVGL_VERSION_MAJOR >= 8
|
||||
const lv_font_t cc_icons_16 = {
|
||||
#else
|
||||
lv_font_t cc_icons_16 = {
|
||||
#endif
|
||||
.get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/
|
||||
.get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/
|
||||
.line_height = 18, /*The maximum line height required by the font*/
|
||||
.base_line = 3, /*Baseline measured from the bottom of the line*/
|
||||
#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0)
|
||||
.subpx = LV_FONT_SUBPX_NONE,
|
||||
#endif
|
||||
#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8
|
||||
.underline_position = -1,
|
||||
.underline_thickness = 1,
|
||||
#endif
|
||||
.dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
|
||||
#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
|
||||
.fallback = NULL,
|
||||
#endif
|
||||
.user_data = NULL,
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif /*#if CC_ICONS_16*/
|
||||
|
||||
@@ -118,6 +118,16 @@
|
||||
#define CAP_OTA 1 // standalone dual-OTA app
|
||||
#define CAP_LOCK_SCREEN 1
|
||||
|
||||
#elif defined(ATTAKY_MESH_SERIES)
|
||||
#define CAP_TOUCH 1
|
||||
#define CAP_ROTATABLE 0
|
||||
#define CAP_LARGE_SCREEN 0
|
||||
#define CAP_SD 0
|
||||
#define CAP_FILESYSTEM 0
|
||||
#define CAP_GPS 1
|
||||
#define CAP_OTA 1
|
||||
#define CAP_LOCK_SCREEN 0
|
||||
|
||||
#else // ===== Heltec V4 TFT (default) =====
|
||||
#define CAP_TOUCH 1 // capacitive touch panel
|
||||
#define CAP_ROTATABLE 1 // user can flip portrait/landscape
|
||||
|
||||
@@ -616,6 +616,9 @@ static const I18nEntry kI18n[] = {
|
||||
{ "Message deleted", { 0, "Bericht verwijderd","Nachricht gelöscht","Message supprimé","Mensaje eliminado","Messaggio eliminato","Сообщение удалено","Повідомлення видалено","Съобщението е изтрито","Порука обрисана","Το μήνυμα διαγράφηκε","Mensagem excluída" } },
|
||||
{ "Message didn't get through.\nResend it?", { 0, "Bericht is niet aangekomen.\nOpnieuw versturen?","Nachricht kam nicht an.\nErneut senden?","Le message n'est pas passé.\nRenvoyer ?","El mensaje no llegó.\n¿Reenviarlo?","Il messaggio non è arrivato.\nInviarlo di nuovo?","Сообщение не дошло.\nОтправить снова?","Повідомлення не дійшло.\nНадіслати знову?","Съобщението не стигна.\nДа го изпратя ли пак?","Порука није стигла.\nПослати поново?","Το μήνυμα δεν έφτασε.\nΝα σταλεί ξανά;","A mensagem não chegou.\nReenviar?" } },
|
||||
{ "Message sound", { 0, "Berichtgeluid","Nachrichtenton","Son des messages","Sonido de mensaje","Suono messaggi","Звук сообщений","Звук повідомлень","Звук на съобщения","Звук порука","Ήχος μηνυμάτων","Som de mensagem" } },
|
||||
{ "Do Not Disturb", { 0 } },
|
||||
{ "Start time", { 0 } },
|
||||
{ "End time", { 0 } },
|
||||
{ "Multi-ack %s · Repeat %s · RX boost %s", { 0, "Multi-ack %s · Herhalen %s · RX-boost %s","Multi-Ack %s · Wdh. %s · RX-Boost %s","Multi-ack %s · Répéter %s · RX boost %s","Multi-ack %s · Repetir %s · RX boost %s","Multi-ack %s · Ripeti %s · RX boost %s","Мульти-ack %s · Повтор %s · RX boost %s","Мульти-ack %s · Повтор %s · RX boost %s","Мулти-ack %s · Повтор %s · RX boost %s","Мулти-ack %s · Понови %s · RX boost %s","Multi-ack %s · Επανάλ. %s · RX boost %s","Multi-ack %s · Repetir %s · RX boost %s" } },
|
||||
{ "Mute @", { 0, "Demp @","@ stumm","Muet @","Silenciar @","Muto @","Без звука @","Без звуку @","Заглуши @","Утишај @","Σίγαση @","Silenciar @" } },
|
||||
{ "Mute msgs", { 0, "Berichten dempen","Nachr. stumm","Muet msgs","Silenciar msjs","Muto msg","Заглушить сообщ.","Вимкнути звук повід.","Заглуши съобщ.","Утишај поруке","Σίγαση μην.","Silenciar msgs" } },
|
||||
|
||||
@@ -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$")
|
||||
|
||||
|
||||
@@ -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 ---
|
||||
@@ -70,8 +69,12 @@ set(WADA_DEFS
|
||||
# PIN_GPS_TX = the GPIO wired to the MODULE'S TX pin; PIN_GPS_RX = wired to the module's RX.
|
||||
# Matches LilyGo's t_display_p4_config.h "GPS_TX 22 / GPS_RX 23" naming. The first cut had them
|
||||
# swapped (silent: UART opens fine, just never hears NMEA). Wake: XL9535 GPS_WAKE must be HIGH
|
||||
# (powerOnSequence) or the L76K sleeps. Baud: L76K power-on default 9600 = the core default. ---
|
||||
ENV_INCLUDE_GPS=1 PIN_GPS_TX=22 PIN_GPS_RX=23
|
||||
# (powerOnSequence) or the L76K sleeps. Baud: L76K power-on default 9600 = the core default.
|
||||
# ENV_SKIP_GPS_DETECT: like the T-Deck, force gps_detected=true — the P4's heavy boot can miss the
|
||||
# 1 s "any NMEA byte" detect window, and a false result then HIDES the GPS toggle and no-ops the
|
||||
# saved-pref resume, so GPS stays dead with no recovery until a lucky reboot. GPS_BAUD_RATE=9600
|
||||
# pins the correct L76K baud (a stale 38400 NVS override will still need clearing on-device). ---
|
||||
ENV_INCLUDE_GPS=1 PIN_GPS_TX=22 PIN_GPS_RX=23 ENV_SKIP_GPS_DETECT=1 GPS_BAUD_RATE=9600
|
||||
|
||||
# --- microSD on SDMMC slot 0 (CLK43/CMD44/D0-3=39-42); C6 esp-hosted is slot 1 ---
|
||||
BOARD_HAS_SDMMC=1 BOARD_SDMMC_SLOT=0
|
||||
@@ -81,6 +84,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).
|
||||
@@ -99,3 +118,12 @@ endif()
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_RELEASE_TAG=\"${WADA_FW_TAG}\"" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_BUILD_DATE=\"${WADA_FW_DATE}\"" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_VERSION=\"wadamesh ${WADA_FW_TAG}\"" APPEND)
|
||||
|
||||
# On-device OTA / update-channel UI (the "Get test builds (beta)" toggle + the Install-update button
|
||||
# in UITask.cpp) is gated on a NON-EMPTY FIRMWARE_OTA_ENV. The 8 S3 boards set this per-env in
|
||||
# platformio.ini; the P4 is an IDF build and must set it here, or the whole update UI stays hidden
|
||||
# (the symptom testers hit on beta_49: no beta toggle, stuck reading the stable channel). The value is
|
||||
# only a presence marker — the OTA download URL is built from OTA_BIN_NAME (set per-SKU in UITask.cpp),
|
||||
# not from this. The P4 partition table has dual ota_0/ota_1 slots and the C6 WiFi shim routes the
|
||||
# HTTPClient + Update.h writer, so the OTA path itself is wired; this just unhides the UI.
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_OTA_ENV=\"tdisplay_p4\"" APPEND)
|
||||
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "AttakyMeshSeriesBoard.h"
|
||||
#include "AttakySharedI2C.h"
|
||||
#include <SPI.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#define MAX17048_ADDR 0x36
|
||||
#define MAX17048_REG_VCELL 0x02
|
||||
|
||||
void AttakyMeshSeriesBoard::begin() {
|
||||
ESP32Board::begin();
|
||||
|
||||
attakyI2cInit();
|
||||
|
||||
esp_reset_reason_t reason = esp_reset_reason();
|
||||
if (reason == ESP_RST_DEEPSLEEP) {
|
||||
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
|
||||
if (wakeup_source & (1 << P_LORA_DIO_1)) {
|
||||
startup_reason = BD_STARTUP_RX_PACKET;
|
||||
}
|
||||
|
||||
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
|
||||
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
|
||||
}
|
||||
|
||||
// The global-SPI display path does not initialize the bus.
|
||||
#if defined(PIN_TFT_LEDA_CTL) && (PIN_TFT_LEDA_CTL >= 0)
|
||||
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
|
||||
#endif
|
||||
SPI.begin(PIN_TFT_SCL, -1, PIN_TFT_SDA, PIN_TFT_CS);
|
||||
}
|
||||
|
||||
void AttakyMeshSeriesBoard::onBeforeTransmit(void) { }
|
||||
|
||||
void AttakyMeshSeriesBoard::onAfterTransmit(void) { }
|
||||
|
||||
void AttakyMeshSeriesBoard::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
|
||||
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
|
||||
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
|
||||
|
||||
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
|
||||
|
||||
if (pin_wake_btn < 0) {
|
||||
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH);
|
||||
} else {
|
||||
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH);
|
||||
}
|
||||
|
||||
if (secs > 0) {
|
||||
esp_sleep_enable_timer_wakeup(secs * 1000000);
|
||||
}
|
||||
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
void AttakyMeshSeriesBoard::powerOff() {
|
||||
enterDeepSleep(0);
|
||||
}
|
||||
|
||||
// MAX17048 VCELL is 78.125 uV/LSB.
|
||||
uint16_t AttakyMeshSeriesBoard::getBattMilliVolts() {
|
||||
uint32_t now = millis();
|
||||
if (batt_cache_mv != 0 && (uint32_t)(now - batt_last_ms) < 2000) return batt_cache_mv;
|
||||
if (!attakyI2cLock(30)) return batt_cache_mv ? batt_cache_mv : 4000;
|
||||
uint16_t raw = 0;
|
||||
bool ok = false;
|
||||
Wire.beginTransmission(MAX17048_ADDR);
|
||||
Wire.write(MAX17048_REG_VCELL);
|
||||
if (Wire.endTransmission(false) == 0 && Wire.requestFrom((int)MAX17048_ADDR, 2) == 2) {
|
||||
raw = ((uint16_t)Wire.read() << 8) | Wire.read();
|
||||
ok = true;
|
||||
}
|
||||
attakyI2cUnlock();
|
||||
if (ok) {
|
||||
batt_cache_mv = (uint16_t)(((uint32_t)raw * 5) / 64);
|
||||
batt_last_ms = now;
|
||||
}
|
||||
return batt_cache_mv ? batt_cache_mv : 4000;
|
||||
}
|
||||
|
||||
const char* AttakyMeshSeriesBoard::getManufacturerName() const {
|
||||
return "Attaky Core";
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <helpers/ESP32Board.h>
|
||||
#include <driver/rtc_io.h>
|
||||
|
||||
#ifndef ADC_MULTIPLIER
|
||||
#define ADC_MULTIPLIER 1.0
|
||||
#endif
|
||||
|
||||
class AttakyMeshSeriesBoard : public ESP32Board {
|
||||
|
||||
protected:
|
||||
float adc_mult = ADC_MULTIPLIER;
|
||||
// MAX17048 fuel-gauge readout cache: getBattMilliVolts() is polled from the UI
|
||||
// render / mesh-telemetry path, so the actual I2C read is throttled to ~2 s and
|
||||
// the last value is served in between (0 = not yet read).
|
||||
uint16_t batt_cache_mv = 0;
|
||||
uint32_t batt_last_ms = 0;
|
||||
|
||||
public:
|
||||
AttakyMeshSeriesBoard() { }
|
||||
|
||||
void begin();
|
||||
void onBeforeTransmit(void) override;
|
||||
void onAfterTransmit(void) override;
|
||||
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
|
||||
void powerOff() override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
bool setAdcMultiplier(float multiplier) override {
|
||||
if (multiplier == 0.0f) {
|
||||
adc_mult = ADC_MULTIPLIER;
|
||||
} else {
|
||||
adc_mult = multiplier;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
float getAdcMultiplier() const override { return adc_mult; }
|
||||
const char* getManufacturerName() const override;
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include "AttakyMeshSeriesKeyboard.h"
|
||||
|
||||
#if defined(HAS_ATTAKY_MESH_KEYBOARD) && defined(ESP32)
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include "AttakySharedI2C.h"
|
||||
|
||||
static const uint8_t AW_REG_IN_P0 = 0x00;
|
||||
static const uint8_t AW_REG_OUT_P1 = 0x03;
|
||||
static const uint8_t AW_REG_CFG_P0 = 0x04;
|
||||
static const uint8_t AW_REG_CFG_P1 = 0x05;
|
||||
static const uint8_t AW_REG_OUT_P0 = 0x02;
|
||||
static const uint8_t AW_REG_MODE_P0 = 0x12;
|
||||
static const uint8_t AW_REG_MODE_P1 = 0x13;
|
||||
static const uint8_t AW_REG_SWRST = 0x7F;
|
||||
|
||||
static const uint8_t KB_ADDR[2] = { 0x5A, 0x5B };
|
||||
|
||||
static const uint8_t P1_IDLE = 0xE0;
|
||||
static const uint8_t ROW_DRIVE[5] = { 0xFE, 0xFD, 0xFB, 0xF7, 0xEF };
|
||||
|
||||
#define K_NOP 0x00
|
||||
#define K_SFT 0x01
|
||||
#define K_BSP 0x08
|
||||
#define K_TAB 0x09
|
||||
#define K_ENT 0x0D
|
||||
#define K_SPC 0x20
|
||||
|
||||
static const uint8_t KEYMAP_L[5][5] = {
|
||||
{ '1', '2', '3', '4', '5' },
|
||||
{ 'q', 'w', 'e', 'r', 't' },
|
||||
{ 'a', 's', 'd', 'f', 'g' },
|
||||
{ K_SFT, 'z', 'x', 'c', 'v' },
|
||||
{ K_NOP, K_TAB, K_NOP, ',', K_SPC },
|
||||
};
|
||||
static const uint8_t KEYMAP_R[5][5] = {
|
||||
{ '6', '7', '8', '9', '0' },
|
||||
{ 'y', 'u', 'i', 'o', 'p' },
|
||||
{ 'h', 'j', 'k', 'l', K_BSP },
|
||||
{ 'b', 'n', 'm', K_NOP, K_ENT },
|
||||
{ K_NOP, '.', K_NOP, '#', K_NOP },
|
||||
};
|
||||
static const uint8_t SHIFT_HALF = 0, SHIFT_ROW = 3, SHIFT_COL = 0;
|
||||
|
||||
static bool s_inited = false;
|
||||
static bool s_present[2] = { false, false };
|
||||
static uint8_t s_prev[2][5] = { { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF },
|
||||
{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } };
|
||||
|
||||
static volatile uint8_t s_ring[16];
|
||||
static volatile uint8_t s_head = 0, s_tail = 0;
|
||||
|
||||
static void ringPush(uint8_t b) {
|
||||
uint8_t nh = (uint8_t)((s_head + 1) & 15);
|
||||
if (nh != s_tail) { s_ring[s_head] = b; s_head = nh; }
|
||||
}
|
||||
static void ringClear() { s_head = s_tail = 0; }
|
||||
|
||||
static bool awWrite(uint8_t addr, uint8_t reg, uint8_t val) {
|
||||
Wire.beginTransmission(addr);
|
||||
Wire.write(reg);
|
||||
Wire.write(val);
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
static uint8_t awRead(uint8_t addr, uint8_t reg) {
|
||||
Wire.beginTransmission(addr);
|
||||
Wire.write(reg);
|
||||
if (Wire.endTransmission(false) != 0) return 0xFF;
|
||||
if (Wire.requestFrom((int)addr, 1) != 1) return 0xFF;
|
||||
return (uint8_t)Wire.read();
|
||||
}
|
||||
|
||||
static bool initHalf(uint8_t addr) {
|
||||
bool ok = true;
|
||||
ok &= awWrite(addr, AW_REG_SWRST, 0x00);
|
||||
delay(2);
|
||||
ok &= awWrite(addr, AW_REG_MODE_P0, 0xFF);
|
||||
ok &= awWrite(addr, AW_REG_MODE_P1, 0xFF);
|
||||
ok &= awWrite(addr, AW_REG_CFG_P0, 0xFF);
|
||||
ok &= awWrite(addr, AW_REG_CFG_P1, 0x00);
|
||||
ok &= awWrite(addr, AW_REG_OUT_P0, 0x00);
|
||||
ok &= awWrite(addr, AW_REG_OUT_P1, P1_IDLE);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void ensureInit() {
|
||||
if (s_inited) return;
|
||||
s_inited = true;
|
||||
for (int h = 0; h < 2; h++) {
|
||||
if (!attakyI2cLock(30)) continue;
|
||||
s_present[h] = initHalf(KB_ADDR[h]);
|
||||
attakyI2cUnlock();
|
||||
}
|
||||
}
|
||||
|
||||
static void scanHalf(uint8_t addr, uint8_t cols[5]) {
|
||||
for (uint8_t row = 0; row < 5; row++) {
|
||||
awWrite(addr, AW_REG_OUT_P1, (uint8_t)(ROW_DRIVE[row] | 0xE0));
|
||||
delayMicroseconds(50);
|
||||
cols[row] = (uint8_t)(awRead(addr, AW_REG_IN_P0) & 0x1F);
|
||||
}
|
||||
awWrite(addr, AW_REG_OUT_P1, P1_IDLE);
|
||||
}
|
||||
|
||||
void attakyKeyboardPoll(bool active) {
|
||||
ensureInit();
|
||||
|
||||
if (!active) {
|
||||
for (int h = 0; h < 2; h++)
|
||||
for (int r = 0; r < 5; r++) s_prev[h][r] = 0xFF;
|
||||
ringClear();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t cur[2][5] = { { 0x1F,0x1F,0x1F,0x1F,0x1F }, { 0x1F,0x1F,0x1F,0x1F,0x1F } };
|
||||
for (int h = 0; h < 2; h++) {
|
||||
if (!s_present[h]) continue;
|
||||
if (!attakyI2cLock(20)) continue;
|
||||
scanHalf(KB_ADDR[h], cur[h]);
|
||||
attakyI2cUnlock();
|
||||
}
|
||||
|
||||
const bool shift_down =
|
||||
s_present[SHIFT_HALF] && !(cur[SHIFT_HALF][SHIFT_ROW] & (1u << SHIFT_COL));
|
||||
|
||||
for (int h = 0; h < 2; h++) {
|
||||
if (!s_present[h]) continue;
|
||||
for (int row = 0; row < 5; row++) {
|
||||
uint8_t now_bits = cur[h][row];
|
||||
uint8_t prev_bits = s_prev[h][row];
|
||||
for (int col = 0; col < 5; col++) {
|
||||
const uint8_t mask = (uint8_t)(1u << col);
|
||||
const bool down_now = !(now_bits & mask);
|
||||
const bool down_prev = !(prev_bits & mask);
|
||||
if (down_now && !down_prev) {
|
||||
uint8_t k = (h == 0) ? KEYMAP_L[row][col] : KEYMAP_R[row][col];
|
||||
if (k == K_NOP || k == K_SFT) continue;
|
||||
if (shift_down && k >= 'a' && k <= 'z') k = (uint8_t)(k - 0x20);
|
||||
ringPush(k);
|
||||
}
|
||||
}
|
||||
s_prev[h][row] = now_bits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int attakyKeyboardReadKey() {
|
||||
if (s_tail == s_head) return 0;
|
||||
uint8_t b = s_ring[s_tail];
|
||||
s_tail = (uint8_t)((s_tail + 1) & 15);
|
||||
return b;
|
||||
}
|
||||
|
||||
bool attakyKeyboardPresent() { return s_present[0] || s_present[1]; }
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#if defined(HAS_ATTAKY_MESH_KEYBOARD) && defined(ESP32)
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void attakyKeyboardPoll(bool active);
|
||||
int attakyKeyboardReadKey();
|
||||
bool attakyKeyboardPresent();
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,103 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include "AttakyMeshSeriesKeys.h"
|
||||
|
||||
#if defined(ATTAKY_MESH_SERIES) && defined(ESP32)
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include "AttakySharedI2C.h"
|
||||
|
||||
static const uint8_t KEYS_ADDR = 0x59;
|
||||
|
||||
static const uint8_t AW_REG_IN_P0 = 0x00;
|
||||
static const uint8_t AW_REG_CFG_P0 = 0x04;
|
||||
static const uint8_t AW_REG_MODE_P0 = 0x12; // 0x12 = P0 LED-mode select (0x13 is P1)
|
||||
|
||||
static const uint8_t POWER_BIT = (1u << 7); // P07 = POWER_BTN
|
||||
|
||||
// Debounce: the buttons sit behind a shared-bus I2C expander, so require a
|
||||
// stable read across several polls before a press counts.
|
||||
static const uint8_t DEBOUNCE_POLLS = 2;
|
||||
static const uint32_t POLL_INTERVAL_MS = 30;
|
||||
|
||||
static bool s_inited = false;
|
||||
static bool s_present = false;
|
||||
static uint32_t s_next_ms = 0;
|
||||
|
||||
static bool s_power_down = false; // debounced state
|
||||
static uint8_t s_pending_n = 0; // consecutive polls agreeing with s_pending_down
|
||||
static bool s_pending_down = false;
|
||||
static volatile bool s_power_event = false;
|
||||
|
||||
static bool awWrite(uint8_t reg, uint8_t val) {
|
||||
Wire.beginTransmission(KEYS_ADDR);
|
||||
Wire.write(reg);
|
||||
Wire.write(val);
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
|
||||
// Reads IN_P0. Returns false on I2C failure without touching *val, so a failed
|
||||
// read is never mistaken for 0xFF ("nothing pressed").
|
||||
static bool awReadInP0(uint8_t* val) {
|
||||
Wire.beginTransmission(KEYS_ADDR);
|
||||
Wire.write(AW_REG_IN_P0);
|
||||
if (Wire.endTransmission(false) != 0) return false;
|
||||
if (Wire.requestFrom((int)KEYS_ADDR, 1) != 1) return false;
|
||||
*val = (uint8_t)Wire.read();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ensureInit() {
|
||||
if (s_inited) return;
|
||||
s_inited = true;
|
||||
if (!attakyI2cLock(30)) { s_inited = false; return; } // retry on the next poll
|
||||
// P0 as GPIO inputs (POR defaults, but the touch driver shares this expander,
|
||||
// so set them explicitly rather than depend on init order).
|
||||
bool ok = awWrite(AW_REG_MODE_P0, 0xFF); // GPIO mode, not LED mode
|
||||
ok &= awWrite(AW_REG_CFG_P0, 0xFF); // all inputs
|
||||
s_present = ok;
|
||||
attakyI2cUnlock();
|
||||
}
|
||||
|
||||
void attakyKeysPoll() {
|
||||
ensureInit();
|
||||
if (!s_present) return;
|
||||
|
||||
const uint32_t now = millis();
|
||||
if ((int32_t)(now - s_next_ms) < 0) return;
|
||||
s_next_ms = now + POLL_INTERVAL_MS;
|
||||
|
||||
uint8_t p0 = 0xFF;
|
||||
bool ok = false;
|
||||
if (attakyI2cLock(10)) {
|
||||
ok = awReadInP0(&p0);
|
||||
attakyI2cUnlock();
|
||||
}
|
||||
if (!ok) return; // bus busy or read failed — hold the previous state
|
||||
|
||||
const bool down = (p0 & POWER_BIT) == 0; // active low
|
||||
|
||||
if (down != s_pending_down) { s_pending_down = down; s_pending_n = 1; return; }
|
||||
if (s_pending_n < DEBOUNCE_POLLS) { s_pending_n++; return; }
|
||||
|
||||
if (down != s_power_down) {
|
||||
s_power_down = down;
|
||||
if (down) s_power_event = true; // fire on the press edge
|
||||
}
|
||||
}
|
||||
|
||||
bool attakyPowerKeyPressed() {
|
||||
if (!s_power_event) return false;
|
||||
s_power_event = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool attakyKeysPresent() { return s_present; }
|
||||
|
||||
#else
|
||||
|
||||
void attakyKeysPoll() {}
|
||||
bool attakyPowerKeyPressed() { return false; }
|
||||
bool attakyKeysPresent() { return false; }
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Front buttons on the board's AW9523 @0x59 (P00-P07, active-low). Only
|
||||
// POWER_BTN (P07) is used: short-press toggles the panel. A long press is the
|
||||
// board's hardware power-cut, outside firmware control.
|
||||
|
||||
/** Poll the expander. Call once per UI tick; rate-limits its own I2C traffic. */
|
||||
void attakyKeysPoll();
|
||||
|
||||
/** True once per POWER_BTN press edge (consumes the event). */
|
||||
bool attakyPowerKeyPressed();
|
||||
|
||||
/** Whether the @0x59 expander answered at init (false = keys unavailable). */
|
||||
bool attakyKeysPresent();
|
||||
@@ -0,0 +1,429 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "../../src/helpers/input/HeltecV4CapTouch.h"
|
||||
|
||||
#if (defined(HAS_HELTEC_V4_CAP_TOUCH) || defined(HAS_TOUCH_UI)) && defined(ESP32)
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include "AttakySharedI2C.h"
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
|
||||
#ifndef PIN_BOARD_SDA
|
||||
#define PIN_BOARD_SDA 2
|
||||
#endif
|
||||
#ifndef PIN_BOARD_SCL
|
||||
#define PIN_BOARD_SCL 1
|
||||
#endif
|
||||
|
||||
static const uint8_t FT6636_ADDR = 0x38;
|
||||
static const uint8_t FT6636_REG_MODE = 0x00;
|
||||
static const uint8_t FT6636_REG_TD_ST = 0x02;
|
||||
static const uint8_t FT6636_REG_VENDOR = 0xA8;
|
||||
|
||||
static const uint8_t AW9523_ADDR = 0x59;
|
||||
static const uint8_t AW9523_REG_OUT_P1 = 0x03;
|
||||
static const uint8_t AW9523_REG_CFG_P1 = 0x05;
|
||||
static const uint8_t AW9523_REG_LEDM_P1 = 0x13;
|
||||
static const uint8_t AW9523_P13_BIT = (1u << 3);
|
||||
|
||||
#ifndef HELTEC_V4_TOUCH_LONG_MS
|
||||
#define HELTEC_V4_TOUCH_LONG_MS 1000
|
||||
#endif
|
||||
#ifndef HELTEC_V4_TOUCH_LONG_MOVE_MAX
|
||||
#define HELTEC_V4_TOUCH_LONG_MOVE_MAX 18
|
||||
#endif
|
||||
#ifndef HELTEC_V4_TOUCH_SWIPE_MIN
|
||||
#define HELTEC_V4_TOUCH_SWIPE_MIN 36
|
||||
#endif
|
||||
#ifndef HELTEC_V4_TOUCH_SWIPE_INVERT
|
||||
#define HELTEC_V4_TOUCH_SWIPE_INVERT 0
|
||||
#endif
|
||||
|
||||
static bool s_present = false;
|
||||
static bool s_init_attempted = false;
|
||||
static bool s_init_ok = false;
|
||||
static unsigned long s_last_init_try_ms = 0;
|
||||
static bool s_tap_pending = false;
|
||||
static uint16_t s_tap_x = 0, s_tap_y = 0;
|
||||
static bool s_swipe_pending = false;
|
||||
static int8_t s_swipe_x = 0, s_swipe_y = 0;
|
||||
static bool s_live = false;
|
||||
static uint16_t s_live_x = 0, s_live_y = 0;
|
||||
static uint16_t s_raw_x = 0, s_raw_y = 0;
|
||||
static uint8_t s_vendor_id = 0;
|
||||
static char s_debug[48] = "";
|
||||
static bool s_swiping_now = false;
|
||||
|
||||
static uint8_t s_ui_rotation = 0;
|
||||
void heltecV4CapTouchSetRotation(uint8_t lvgl_rot) { s_ui_rotation = lvgl_rot & 3; }
|
||||
|
||||
static void rotateSwipeDelta(int dx, int dy, int* odx, int* ody) {
|
||||
switch (s_ui_rotation) {
|
||||
case 1: *odx = -dy; *ody = dx; break;
|
||||
case 2: *odx = -dx; *ody = -dy; break;
|
||||
case 3: *odx = dy; *ody = -dx; break;
|
||||
default: *odx = dx; *ody = dy; break;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t s_point_rotation = 0;
|
||||
void heltecV4CapTouchSetPointRotation(uint8_t r) { s_point_rotation = r & 3; }
|
||||
|
||||
static void applyPointRotation(uint16_t* x, uint16_t* y) {
|
||||
if (s_point_rotation == 0) return;
|
||||
const int W = 240;
|
||||
const int H = 320;
|
||||
const int px = *x, py = *y;
|
||||
int lx, ly;
|
||||
switch (s_point_rotation) {
|
||||
case 1: lx = (H - 1) - py; ly = px; break;
|
||||
case 2: lx = (W - 1) - px; ly = (H - 1) - py; break;
|
||||
case 3: lx = py; ly = (W - 1) - px; break;
|
||||
default: lx = px; ly = py; break;
|
||||
}
|
||||
if (lx < 0) lx = 0;
|
||||
if (ly < 0) ly = 0;
|
||||
*x = (uint16_t)lx;
|
||||
*y = (uint16_t)ly;
|
||||
}
|
||||
|
||||
static void mapTouchToDisplay(uint16_t in_x, uint16_t in_y, uint16_t* out_x, uint16_t* out_y) {
|
||||
#ifndef DISPLAY_WIDTH
|
||||
#define DISPLAY_WIDTH 240
|
||||
#endif
|
||||
#ifndef DISPLAY_HEIGHT
|
||||
#define DISPLAY_HEIGHT 320
|
||||
#endif
|
||||
#ifndef DISPLAY_ROTATION
|
||||
#define DISPLAY_ROTATION 0
|
||||
#endif
|
||||
|
||||
int x = in_x;
|
||||
int y = in_y;
|
||||
int w = DISPLAY_WIDTH;
|
||||
int h = DISPLAY_HEIGHT;
|
||||
switch (DISPLAY_ROTATION & 3) {
|
||||
case 1: {
|
||||
int nx = h - 1 - y;
|
||||
int ny = x;
|
||||
x = nx;
|
||||
y = ny;
|
||||
} break;
|
||||
case 2:
|
||||
#if defined(UI_LVGL)
|
||||
break;
|
||||
#else
|
||||
x = w - 1 - x;
|
||||
y = h - 1 - y;
|
||||
break;
|
||||
#endif
|
||||
case 3: {
|
||||
int nx = y;
|
||||
int ny = w - 1 - x;
|
||||
x = nx;
|
||||
y = ny;
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Landscape rotations swap output dimensions.
|
||||
int w_out = w, h_out = h;
|
||||
if ((DISPLAY_ROTATION & 3) == 1 || (DISPLAY_ROTATION & 3) == 3) { w_out = h; h_out = w; }
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
if (x >= w_out) x = w_out - 1;
|
||||
if (y >= h_out) y = h_out - 1;
|
||||
if (out_x) *out_x = (uint16_t)x;
|
||||
if (out_y) *out_y = (uint16_t)y;
|
||||
}
|
||||
|
||||
static bool i2c_read_reg(uint8_t addr, uint8_t reg, uint8_t* buf, uint8_t n) {
|
||||
if (!attakyI2cLock(20)) return false;
|
||||
Wire.beginTransmission(addr);
|
||||
Wire.write(reg);
|
||||
if (Wire.endTransmission(false) != 0) { attakyI2cUnlock(); return false; }
|
||||
uint8_t got = Wire.requestFrom((int)addr, (int)n);
|
||||
if (got != n) { attakyI2cUnlock(); return false; }
|
||||
for (uint8_t i = 0; i < n; i++) buf[i] = Wire.read();
|
||||
attakyI2cUnlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool aw9523_read(uint8_t reg, uint8_t* val) {
|
||||
return i2c_read_reg(AW9523_ADDR, reg, val, 1);
|
||||
}
|
||||
|
||||
static bool aw9523_write(uint8_t reg, uint8_t val) {
|
||||
if (!attakyI2cLock(20)) return false;
|
||||
Wire.beginTransmission(AW9523_ADDR);
|
||||
Wire.write(reg);
|
||||
Wire.write(val);
|
||||
bool ok = (Wire.endTransmission() == 0);
|
||||
attakyI2cUnlock();
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool aw9523_set_bit(uint8_t reg, uint8_t bitmask, bool set) {
|
||||
uint8_t v = 0;
|
||||
if (!aw9523_read(reg, &v)) return false;
|
||||
uint8_t nv = set ? (uint8_t)(v | bitmask) : (uint8_t)(v & ~bitmask);
|
||||
if (nv == v) return true;
|
||||
return aw9523_write(reg, nv);
|
||||
}
|
||||
|
||||
static void ft6636_hw_reset() {
|
||||
bool ok = true;
|
||||
ok &= aw9523_set_bit(AW9523_REG_LEDM_P1, AW9523_P13_BIT, true);
|
||||
ok &= aw9523_set_bit(AW9523_REG_CFG_P1, AW9523_P13_BIT, false);
|
||||
ok &= aw9523_set_bit(AW9523_REG_OUT_P1, AW9523_P13_BIT, false);
|
||||
if (!ok) {
|
||||
delay(300);
|
||||
return;
|
||||
}
|
||||
delay(10);
|
||||
aw9523_set_bit(AW9523_REG_OUT_P1, AW9523_P13_BIT, true);
|
||||
delay(300);
|
||||
}
|
||||
|
||||
static int ft6636_read_touch(uint16_t* x, uint16_t* y) {
|
||||
uint8_t buf[7];
|
||||
if (!i2c_read_reg(FT6636_ADDR, FT6636_REG_TD_ST, buf, 7)) {
|
||||
static uint32_t ec = 0; if ((ec++ & 0x3F) == 0) Serial.printf("[TCH] i2c read FAIL\n");
|
||||
return -1;
|
||||
}
|
||||
uint8_t touch_count = buf[0] & 0x0F;
|
||||
uint8_t event_flag = (buf[1] >> 6) & 0x03;
|
||||
uint16_t px = (uint16_t)(((buf[1] & 0x0F) << 8) | buf[2]);
|
||||
uint16_t py = (uint16_t)(((buf[3] & 0x0F) << 8) | buf[4]);
|
||||
bool down = (touch_count >= 1) && (event_flag != 1);
|
||||
if (!down) return -1;
|
||||
if (x) *x = px;
|
||||
if (y) *y = py;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool heltecV4CapTouchBegin() {
|
||||
if (s_init_ok) return true;
|
||||
unsigned long now = millis();
|
||||
if (s_init_attempted && (unsigned long)(now - s_last_init_try_ms) < 400) return false;
|
||||
s_init_attempted = true;
|
||||
s_last_init_try_ms = now;
|
||||
|
||||
attakyI2cInit();
|
||||
|
||||
ft6636_hw_reset();
|
||||
|
||||
uint8_t vid = 0;
|
||||
if (!i2c_read_reg(FT6636_ADDR, FT6636_REG_VENDOR, &vid, 1) || vid == 0) {
|
||||
s_vendor_id = vid;
|
||||
s_present = false;
|
||||
s_init_ok = false;
|
||||
snprintf(s_debug, sizeof(s_debug), "FT6636 absent (vid=0x%02X)", vid);
|
||||
return false;
|
||||
}
|
||||
s_vendor_id = vid;
|
||||
s_present = true;
|
||||
|
||||
if (attakyI2cLock(20)) {
|
||||
Wire.beginTransmission(FT6636_ADDR);
|
||||
Wire.write(FT6636_REG_MODE);
|
||||
Wire.write((uint8_t)0x00);
|
||||
Wire.endTransmission();
|
||||
attakyI2cUnlock();
|
||||
}
|
||||
|
||||
snprintf(s_debug, sizeof(s_debug), "FT6636 ok (vid=0x%02X)", vid);
|
||||
s_init_ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
int heltecV4CapTouchCheck() {
|
||||
if (!s_init_ok || !s_present) {
|
||||
s_live = false;
|
||||
return BUTTON_EVENT_NONE;
|
||||
}
|
||||
|
||||
static bool down = false;
|
||||
static unsigned long down_at = 0;
|
||||
static bool long_dispatched = false;
|
||||
static uint16_t start_x = 0, start_y = 0;
|
||||
static uint16_t last_x = 0, last_y = 0;
|
||||
// Ignore one transient miss before ending a gesture.
|
||||
static uint8_t release_misses = 0;
|
||||
constexpr uint8_t RELEASE_DEBOUNCE = 2;
|
||||
|
||||
uint16_t x = 0, y = 0;
|
||||
int r = ft6636_read_touch(&x, &y);
|
||||
bool now = (r == 0);
|
||||
if (now) { s_raw_x = x; s_raw_y = y; }
|
||||
|
||||
if (!now && down && release_misses < RELEASE_DEBOUNCE) {
|
||||
++release_misses;
|
||||
return BUTTON_EVENT_NONE;
|
||||
}
|
||||
if (now) release_misses = 0;
|
||||
|
||||
if (now && !down) {
|
||||
down = true;
|
||||
down_at = millis();
|
||||
long_dispatched = false;
|
||||
start_x = last_x = x;
|
||||
start_y = last_y = y;
|
||||
s_swiping_now = false;
|
||||
mapTouchToDisplay(start_x, start_y, &s_live_x, &s_live_y);
|
||||
applyPointRotation(&s_live_x, &s_live_y);
|
||||
s_live = true;
|
||||
} else if (now && down) {
|
||||
last_x = x;
|
||||
last_y = y;
|
||||
mapTouchToDisplay(last_x, last_y, &s_live_x, &s_live_y);
|
||||
applyPointRotation(&s_live_x, &s_live_y);
|
||||
s_live = true;
|
||||
// Only horizontal swipes suppress LVGL clicks.
|
||||
if (!s_swiping_now) {
|
||||
int rdx = static_cast<int>(last_x) - static_cast<int>(start_x);
|
||||
int rdy = static_cast<int>(last_y) - static_cast<int>(start_y);
|
||||
int dx, dy;
|
||||
rotateSwipeDelta(rdx, rdy, &dx, &dy);
|
||||
int adx = dx < 0 ? -dx : dx;
|
||||
int ady = dy < 0 ? -dy : dy;
|
||||
if (adx >= HELTEC_V4_TOUCH_SWIPE_MIN && adx > ady) {
|
||||
s_swiping_now = true;
|
||||
}
|
||||
}
|
||||
} else if (!now && down) {
|
||||
s_live = false;
|
||||
down = false;
|
||||
s_swiping_now = false;
|
||||
unsigned long dur = (down_at > 0) ? (unsigned long)(millis() - down_at) : 0;
|
||||
down_at = 0;
|
||||
uint16_t sx = 0, sy = 0, ex = 0, ey = 0;
|
||||
mapTouchToDisplay(start_x, start_y, &sx, &sy);
|
||||
mapTouchToDisplay(last_x, last_y, &ex, &ey);
|
||||
int rdx = (int)ex - (int)sx;
|
||||
int rdy = (int)ey - (int)sy;
|
||||
int dx, dy;
|
||||
rotateSwipeDelta(rdx, rdy, &dx, &dy);
|
||||
int adx = dx < 0 ? -dx : dx;
|
||||
int ady = dy < 0 ? -dy : dy;
|
||||
if (!long_dispatched && adx >= HELTEC_V4_TOUCH_SWIPE_MIN && adx > (ady + 8)) {
|
||||
bool swipe_left = dx < 0;
|
||||
#if HELTEC_V4_TOUCH_SWIPE_INVERT
|
||||
swipe_left = !swipe_left;
|
||||
#endif
|
||||
s_swipe_x = swipe_left ? -1 : 1;
|
||||
s_swipe_y = 0;
|
||||
s_swipe_pending = true;
|
||||
// Button events carry horizontal swipe direction.
|
||||
return swipe_left ? BUTTON_EVENT_DOUBLE_CLICK : BUTTON_EVENT_TRIPLE_CLICK;
|
||||
}
|
||||
if (!long_dispatched && ady >= HELTEC_V4_TOUCH_SWIPE_MIN && ady > (adx + 8)) {
|
||||
s_swipe_x = 0;
|
||||
s_swipe_y = (dy < 0) ? -1 : 1;
|
||||
s_swipe_pending = true;
|
||||
return BUTTON_EVENT_NONE;
|
||||
}
|
||||
if (!long_dispatched && dur >= 12 && dur < (unsigned long)HELTEC_V4_TOUCH_LONG_MS) {
|
||||
s_tap_x = ex;
|
||||
s_tap_y = ey;
|
||||
applyPointRotation(&s_tap_x, &s_tap_y);
|
||||
s_tap_pending = true;
|
||||
return BUTTON_EVENT_CLICK;
|
||||
}
|
||||
long_dispatched = false;
|
||||
} else if (down && !long_dispatched && down_at > 0 &&
|
||||
(unsigned long)(millis() - down_at) >= (unsigned long)HELTEC_V4_TOUCH_LONG_MS) {
|
||||
int dx = (int)last_x - (int)start_x;
|
||||
int dy = (int)last_y - (int)start_y;
|
||||
int adx = dx < 0 ? -dx : dx;
|
||||
int ady = dy < 0 ? -dy : dy;
|
||||
if (adx > HELTEC_V4_TOUCH_LONG_MOVE_MAX || ady > HELTEC_V4_TOUCH_LONG_MOVE_MAX) {
|
||||
return BUTTON_EVENT_NONE;
|
||||
}
|
||||
long_dispatched = true;
|
||||
return BUTTON_EVENT_LONG_PRESS;
|
||||
}
|
||||
|
||||
return BUTTON_EVENT_NONE;
|
||||
}
|
||||
|
||||
bool heltecV4CapTouchPopTap(uint16_t* x, uint16_t* y) {
|
||||
if (!s_tap_pending) return false;
|
||||
s_tap_pending = false;
|
||||
if (x) *x = s_tap_x;
|
||||
if (y) *y = s_tap_y;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool heltecV4CapTouchGetLive(uint16_t* x, uint16_t* y) {
|
||||
if (!s_live) return false;
|
||||
if (x) *x = s_live_x;
|
||||
if (y) *y = s_live_y;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool heltecV4CapTouchPopSwipe(int8_t* x_dir, int8_t* y_dir) {
|
||||
if (!s_swipe_pending) return false;
|
||||
s_swipe_pending = false;
|
||||
if (x_dir) *x_dir = s_swipe_x;
|
||||
if (y_dir) *y_dir = s_swipe_y;
|
||||
s_swipe_x = 0;
|
||||
s_swipe_y = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Poll touch independently from LVGL rendering.
|
||||
static TaskHandle_t s_poll_task = nullptr;
|
||||
static uint32_t s_poll_period_ms = 8;
|
||||
static volatile bool s_async_active = false;
|
||||
|
||||
static void touchPollTaskFn(void* /*arg*/) {
|
||||
s_async_active = true;
|
||||
const TickType_t period = pdMS_TO_TICKS(s_poll_period_ms ? s_poll_period_ms : 8);
|
||||
TickType_t last_wake = xTaskGetTickCount();
|
||||
for (;;) {
|
||||
(void)heltecV4CapTouchCheck();
|
||||
vTaskDelayUntil(&last_wake, period);
|
||||
}
|
||||
}
|
||||
|
||||
bool heltecV4CapTouchStartBackgroundPoll(uint32_t period_ms) {
|
||||
if (s_poll_task != nullptr) return false;
|
||||
if (!s_init_ok) return false;
|
||||
if (period_ms < 4) period_ms = 4;
|
||||
if (period_ms > 100) period_ms = 100;
|
||||
s_poll_period_ms = period_ms;
|
||||
BaseType_t ok = xTaskCreatePinnedToCore(
|
||||
touchPollTaskFn,
|
||||
"touch_poll",
|
||||
4096,
|
||||
nullptr,
|
||||
1,
|
||||
&s_poll_task,
|
||||
0);
|
||||
if (ok != pdPASS) {
|
||||
s_poll_task = nullptr;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool heltecV4CapTouchIsAsyncPolling() {
|
||||
return s_async_active;
|
||||
}
|
||||
|
||||
bool heltecV4CapTouchIsSwiping() {
|
||||
return s_swiping_now;
|
||||
}
|
||||
|
||||
const char* heltecV4CapTouchDebug() { return s_debug; }
|
||||
|
||||
void heltecV4CapTouchGetRaw(uint16_t* rx, uint16_t* ry) {
|
||||
if (rx) *rx = s_raw_x;
|
||||
if (ry) *ry = s_raw_y;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "AttakySharedI2C.h"
|
||||
#include <Wire.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
static SemaphoreHandle_t s_i2c_mtx = nullptr;
|
||||
static bool s_inited = false;
|
||||
|
||||
void attakyI2cInit() {
|
||||
if (s_inited) return;
|
||||
Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL, 400000);
|
||||
#if defined(ESP32)
|
||||
Wire.setTimeOut(20);
|
||||
#endif
|
||||
s_i2c_mtx = xSemaphoreCreateMutex();
|
||||
s_inited = true;
|
||||
}
|
||||
|
||||
bool attakyI2cLock(uint32_t timeout_ms) {
|
||||
if (!s_i2c_mtx) return true;
|
||||
return xSemaphoreTake(s_i2c_mtx, pdMS_TO_TICKS(timeout_ms)) == pdTRUE;
|
||||
}
|
||||
|
||||
void attakyI2cUnlock() {
|
||||
if (s_i2c_mtx) xSemaphoreGive(s_i2c_mtx);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
void attakyI2cInit();
|
||||
|
||||
bool attakyI2cLock(uint32_t timeout_ms = 50);
|
||||
void attakyI2cUnlock();
|
||||
@@ -0,0 +1,55 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#ifndef Pins_Arduino_h
|
||||
#define Pins_Arduino_h
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
static const uint8_t TX = 43;
|
||||
static const uint8_t RX = 44;
|
||||
|
||||
static const uint8_t SDA = 2;
|
||||
static const uint8_t SCL = 1;
|
||||
|
||||
static const uint8_t SS = 8;
|
||||
static const uint8_t MOSI = 6;
|
||||
static const uint8_t MISO = 7;
|
||||
static const uint8_t SCK = 5;
|
||||
|
||||
static const uint8_t A0 = 1;
|
||||
static const uint8_t A1 = 2;
|
||||
static const uint8_t A2 = 3;
|
||||
static const uint8_t A3 = 4;
|
||||
static const uint8_t A4 = 5;
|
||||
static const uint8_t A5 = 6;
|
||||
static const uint8_t A6 = 7;
|
||||
static const uint8_t A7 = 8;
|
||||
static const uint8_t A8 = 9;
|
||||
static const uint8_t A9 = 10;
|
||||
static const uint8_t A10 = 11;
|
||||
static const uint8_t A11 = 12;
|
||||
static const uint8_t A12 = 13;
|
||||
static const uint8_t A13 = 14;
|
||||
static const uint8_t A14 = 15;
|
||||
static const uint8_t A15 = 16;
|
||||
static const uint8_t A16 = 17;
|
||||
static const uint8_t A17 = 18;
|
||||
static const uint8_t A18 = 19;
|
||||
static const uint8_t A19 = 20;
|
||||
|
||||
static const uint8_t T1 = 1;
|
||||
static const uint8_t T2 = 2;
|
||||
static const uint8_t T3 = 3;
|
||||
static const uint8_t T4 = 4;
|
||||
static const uint8_t T5 = 5;
|
||||
static const uint8_t T6 = 6;
|
||||
static const uint8_t T7 = 7;
|
||||
static const uint8_t T8 = 8;
|
||||
static const uint8_t T9 = 9;
|
||||
static const uint8_t T10 = 10;
|
||||
static const uint8_t T11 = 11;
|
||||
static const uint8_t T12 = 12;
|
||||
static const uint8_t T13 = 13;
|
||||
static const uint8_t T14 = 14;
|
||||
|
||||
#endif /* Pins_Arduino_h */
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
|
||||
namespace {
|
||||
struct BootTrace {
|
||||
BootTrace() { set_boot_phase(1); }
|
||||
} _boot_trace;
|
||||
}
|
||||
|
||||
AttakyMeshSeriesBoard board;
|
||||
|
||||
#if defined(P_LORA_SCLK)
|
||||
static SPIClass spi;
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
|
||||
#else
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
#endif
|
||||
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
ESP32RTCClock fallback_clock;
|
||||
ClockFloorRTC rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
DISPLAY_CLASS display(NULL);
|
||||
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
|
||||
#endif
|
||||
|
||||
bool radio_init() {
|
||||
fallback_clock.begin();
|
||||
rtc_clock.begin(Wire);
|
||||
|
||||
#if defined(P_LORA_SCLK)
|
||||
return radio.std_init(&spi);
|
||||
#else
|
||||
return radio.std_init();
|
||||
#endif
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" void set_boot_phase(int phase);
|
||||
#endif
|
||||
|
||||
#define RADIOLIB_STATIC_ONLY 1
|
||||
#include <RadioLib.h>
|
||||
#include <helpers/radiolib/RadioLibWrappers.h>
|
||||
#include <AttakyMeshSeriesBoard.h>
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
#include "../../src/helpers/ClockFloorRTC.h"
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include <helpers/ui/ST7789LCDDisplay.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
#endif
|
||||
|
||||
extern AttakyMeshSeriesBoard board;
|
||||
extern WRAPPER_CLASS radio_driver;
|
||||
extern RADIO_CLASS radio;
|
||||
extern ClockFloorRTC rtc_clock;
|
||||
extern EnvironmentSensorManager sensors;
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
extern DISPLAY_CLASS display;
|
||||
extern MomentaryButton user_btn;
|
||||
#endif
|
||||
|
||||
bool radio_init();
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
@@ -29,7 +29,15 @@ LGFXDisplay::LGFXDisplay(RefCountedDigitalPin* peripher_power)
|
||||
cfg.dma_channel = SPI_DMA_CH_AUTO;
|
||||
cfg.pin_sclk = PIN_TFT_SCL; // 16
|
||||
cfg.pin_mosi = PIN_TFT_SDA; // 15
|
||||
cfg.pin_miso = -1; // panel is write-only; MISO=45 belongs to SD
|
||||
// MISO=45 MUST be routed here even though the ST7789 panel is write-only: the
|
||||
// shared micro-SD (CS=3) READS its data back over this same pin. LovyanGFX owns
|
||||
// SPI2 (it runs first in setup — the display renders fine), so if LGFX leaves
|
||||
// MISO unrouted (-1), the later Arduino SPI.begin() can't retrofit it onto the
|
||||
// already-initialised bus and every SD read returns nothing. That is exactly the
|
||||
// tester signature: a known-good FAT32 card "wants formatting" (FAT can't be read)
|
||||
// and f_mkfs hangs (writes go out on MOSI, the read-back verify never lands).
|
||||
// readable=false in the panel config still keeps LGFX from ever reading the panel.
|
||||
cfg.pin_miso = PIN_TFT_MISO; // 45 — needed for shared micro-SD reads
|
||||
cfg.pin_dc = PIN_TFT_DC; // 48
|
||||
_bus.config(cfg);
|
||||
_panel.setBus(&_bus);
|
||||
@@ -143,4 +151,13 @@ void LGFXDisplay::setDisplayRotation(uint8_t r) {
|
||||
setLogicalSize(_lcd.width(), _lcd.height());
|
||||
}
|
||||
|
||||
// Anti-burn-in panel sleep over LGFX's own SPI2 bus (SLPIN stops the oscillator/
|
||||
// booster/LC drive; panel RAM is retained so wake = SLPOUT and the old frame is
|
||||
// back with no redraw). Using LGFX's bus — not a second HSPI SPIClass on the same
|
||||
// pins — is what keeps the shared FSPI display bus intact across a wake.
|
||||
void LGFXDisplay::panelSleep(bool sleep) {
|
||||
if (sleep) _lcd.sleep(); // SLPIN
|
||||
else _lcd.wakeup(); // SLPOUT
|
||||
}
|
||||
|
||||
#endif // HELTEC_LORA_V4_R8
|
||||
@@ -52,4 +52,11 @@ public:
|
||||
|
||||
// ---- Hardware panel rotation ----
|
||||
void setDisplayRotation(uint8_t r);
|
||||
|
||||
// ---- Anti-burn-in panel sleep (SLPIN/SLPOUT) ----
|
||||
// Sends the ST7789 sleep commands over LovyanGFX's OWN SPI2 bus. The shared
|
||||
// touchPanelSleep() path in UITask must call this on the R8 instead of its
|
||||
// HSPI s_cmd_spi shim — that shim re-routes GPIO16/15 to HSPI and steals the
|
||||
// FSPI display pins from LGFX, wedging the bus on wake (frozen last frame).
|
||||
void panelSleep(bool sleep);
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
// 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 "driver/ledc.h" // GPIO 51 PWM backlight — the TFT-LCD has a real LED backlight
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
// The HI8561 TFT-LCD backlight is a hardware LED driven by PWM on GPIO 51 (LilyGo's
|
||||
// hi8561::kScreenBacklight). Unlike the AMOLED (RM69A10, self-emissive, brightness via DCS 0x51),
|
||||
// the LCD panel renders but stays dark until this PWM is driven — the "no backlight, works under a
|
||||
// flashlight" report on the P4 LCD. Set up LEDC and drive it from setBrightness().
|
||||
static constexpr int kBacklightGpio = 51;
|
||||
static constexpr ledc_timer_t kBlTimer = LEDC_TIMER_0;
|
||||
static constexpr ledc_channel_t kBlChan = LEDC_CHANNEL_0;
|
||||
static bool s_hi8561_bl_ready = false;
|
||||
|
||||
// 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);
|
||||
|
||||
// Backlight: bring up the LEDC PWM on GPIO 51 (the LCD's real LED backlight). Without this the
|
||||
// panel renders but is dark. 20 kHz avoids audible/visible flicker; 8-bit duty maps 0..255 directly.
|
||||
{
|
||||
ledc_timer_config_t bl_tmr = {};
|
||||
bl_tmr.speed_mode = LEDC_LOW_SPEED_MODE;
|
||||
bl_tmr.timer_num = kBlTimer;
|
||||
bl_tmr.duty_resolution = LEDC_TIMER_8_BIT;
|
||||
bl_tmr.freq_hz = 20000;
|
||||
bl_tmr.clk_cfg = LEDC_AUTO_CLK;
|
||||
ledc_channel_config_t bl_ch = {};
|
||||
bl_ch.gpio_num = kBacklightGpio;
|
||||
bl_ch.speed_mode = LEDC_LOW_SPEED_MODE;
|
||||
bl_ch.channel = kBlChan;
|
||||
bl_ch.timer_sel = kBlTimer;
|
||||
bl_ch.duty = 0;
|
||||
bl_ch.hpoint = 0;
|
||||
s_hi8561_bl_ready = (ledc_timer_config(&bl_tmr) == ESP_OK) &&
|
||||
(ledc_channel_config(&bl_ch) == ESP_OK);
|
||||
if (!s_hi8561_bl_ready) Serial.println("[HI8561] backlight PWM init FAILED");
|
||||
}
|
||||
// Keep the DBI IO handle so setBrightness() can also send DCS 0x51 (harmless if the panel
|
||||
// ignores it); the real light is the PWM above. 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) {
|
||||
// The real backlight is the LEDC PWM on GPIO 51 (drives the LCD's LED); the DCS 0x51 is a
|
||||
// best-effort no-op the AMOLED path used. Drive both so the CC slider dims the LCD.
|
||||
if (s_hi8561_bl_ready) {
|
||||
ledc_set_duty(LEDC_LOW_SPEED_MODE, kBlChan, b);
|
||||
ledc_update_duty(LEDC_LOW_SPEED_MODE, kBlChan);
|
||||
}
|
||||
if (_dbi_io) 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
|
||||
@@ -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; }
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user