Compare commits

..

263 Commits

Author SHA1 Message Date
Jason P
6273c95d6e Merge branch 'develop' into multi-message-Storage 2025-12-22 22:26:24 -06:00
Jorropo
5a3855b208 in shame.py do not complain about missing targets (#9032)
PR CI only runs a small subset of all tests.

It is very likely a file we didn't found in the PR is just not tested in PR.
2025-12-21 08:11:18 -05:00
WillyJL
8fdba1f1e2 RTC: PCF85063 support, port to SensorLib 0.3.1 (#8061)
* RTC: PCF85063 support, port to SensorLib 0.3.1

* Tidy up defines

* Remove RTC/PCF8563 mentions from unrelated variants

* Bump SensorLib 0.3.2

* Use SensorRtcHelper

* Consistent warning message

* Fix oversight

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-20 19:10:21 -06:00
Jorropo
db64a5b51e implement basic github action comment reporting target diffs (#9022)
This is missing logic:
- report average
- don't bother reporting if the results are negligeable
- praise the user if it's improving the situation
- shame the user if it's not improving the situation
2025-12-20 19:29:01 -05:00
Jason P
3ff86ce3f8 Merge branch 'develop' into multi-message-Storage 2025-12-20 18:08:33 -06:00
zeropt
3371d3372c Adding support for InkHUD joystick navigation for the Seeed Wio Tracker L1 E-ink (#8678)
* TwoButtonExtened mirrors TwoButton but added joystick functionality

* basic ui navigation with a joystick

settings->joystick.enabled setting added and SETTINGS_VERSION
incremented by one in InkHUD/Persistence.h

in seeed_wio_tracker_L1_eink/nicheGraphics.h enable joystick and
disable "Next Tile" menu item in

implement prevTile and prevApplet functions in
InkHUD/WindowManager.h,cpp and InkHUD/InkHUD.h,cpp

onStickCenterShort, onStickCenterLong, onStickUp, onStickDown,
onStickLeft, and onStickRight functions added to:
- InkHUD/InkHUD.h,cpp
- InkHUD/Events.h,cpp
- InkHUD/Applet.h

change navigation actions in InkHUD/Events.cpp events based on
whether the joystick is enabled or not

in seeed_wio_tracker_L1_eink/nicheGraphics.h connect joystick events to
the new joystick handler functions

* handle joystick input in NotificationApplet and TipsApplet

Both the joystick center short press and the user button short press can
be used to advance through the Tips applet.

dismiss notifications with any joystick input

* MenuApplet controls
allows menu navigation including a back button

* add AlignStickApplet for aligning the joystick with the screen

add joystick.aligned and joystick.alignment to InkHUD/Persistence.h for
storing alignment status and relative angle

create AlignStick applet that prompts the user for a joystick input and
rotates the controls to align with the screen

AlignStick applet is run after the tips applet if the joystick is
enabled and not aligned

add menu item for opening the AlignStick applet

* update tips applet with joystick controls

* format InkHUD additions

* fix stroke consistency when resizing joystick graphic

* tweak button tips for order consistency

* increase joystick debounce

* fix comments

* remove unnecessary '+'

* remap joystick controls to match standard inkHUD behavior

Input with a joystick now behaves as follows

User Button (joystick center):
- short press in applet -> opens menu
- long press in applet -> opens menu
- short press in menu -> selects
- long press in menu -> selects

Exit Button:
- short press in applet -> switches tile
- long press in applet -> nothing for now
- short press in menu -> closes menu
- long press in menu -> nothing for now

---------

Co-authored-by: scobert <scobert57@gmail.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2025-12-20 15:15:42 -05:00
github-actions[bot]
d97e38bafc Update protobufs (#9028)
Co-authored-by: jp-bennett <5630967+jp-bennett@users.noreply.github.com>
2025-12-20 13:00:50 -06:00
Jonathan Bennett
cadf151826 In protobuf update, allow develop branch to auto-update (#9027) 2025-12-20 11:11:21 -06:00
Jason P
5c7084d476 Merge branch 'develop' into multi-message-Storage 2025-12-20 10:07:29 -06:00
Ben Meadors
4fe1c87e54 Merge branch 'master' into develop 2025-12-20 09:52:08 -06:00
github-actions[bot]
4f94354f60 Automated version bumps (#9025)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-12-20 09:49:25 -06:00
Ben Meadors
6a93cb7b69 Emoji naming convention consistency 2025-12-20 08:07:36 -06:00
Ixitxachitl
b11f292cc4 Additional Emoji (#9020)
* Refactor emote dimensions to 16x16 pixels

Updated the dimensions of various emotes in emotes.h from 30x30 or 25x25 to 16x16 pixels for consistency and optimization. Added new emotes including heart_smile, Heart_eyes, and others, all with the same 16x16 size. This change improves memory usage and aligns with the design specifications for smaller emotes.

* Add new emotes and their corresponding bitmap definitions

* Add strong emoji and first quarter moon face

* Add definitions for new emoji graphics

* Fix missing newline at end of file in emotes.cpp

* Add new emotes: eyes, eye, shrug, turkey, turkey leg

* Add turkey and related emote definitions

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/graphics/emotes.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/graphics/emotes.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/graphics/emotes.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/graphics/emotes.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/graphics/emotes.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/graphics/emotes.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/graphics/emotes.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-20 07:50:03 -06:00
Jason P
3bbcb93e97 Merge branch 'develop' into multi-message-Storage 2025-12-19 12:43:05 -06:00
korbinianbauer
ee6449746b CLIENT_BASE: Act like ROUTER_LATE for fav'd nodes, instead of like ROUTER (#8567)
* Client_Base - Dont rebroadcast in early (Router) window

Removed early rebroadcast check for CLIENT_BASE role.

* Client_Base - Clamp rebroadcast to late (Router_Late) window on dupe

* Only clamp to Router_Late window if packet from fav'd node

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-12-19 07:45:35 -06:00
Jason P
2df2c1ff16 Merge branch 'develop' into multi-message-Storage 2025-12-17 15:53:23 -06:00
Jason P
e9db03d185 Macro guard heap_caps_malloc_extmem_enable from SENSECAP_INDICATOR (#9007) 2025-12-17 14:46:35 -06:00
Austin
176d8def48 PlatformIO: Restructure networking_base for re-use (#8964) 2025-12-17 12:47:09 -06:00
Jonathan Bennett
5b299f3ede Prep work for better Store and Forward (#8999)
* make channels.h getHash public

* router.* make the encrypted packet copy available for modules to access

* Update src/mesh/Router.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Set p_encrypted to nullptr after release

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-17 12:03:29 -06:00
Austin
96c42229b0 Renovate all the things (#8994) 2025-12-17 11:05:48 -06:00
Jason P
3088e37d24 Mistakes happen - restoring NodeList Renderer line 2025-12-17 10:54:32 -06:00
Jason P
eba01d3163 Merge branch 'develop' into multi-message-Storage 2025-12-17 08:41:16 -06:00
Jason P
6dfa5329a3 Migrate Unit C6L macro guards into currentResolution UltraLow checks 2025-12-17 08:40:52 -06:00
Jonathan Bennett
f1aefc4eef Detect if NTP is active on native (#8962)
* Detect if NTP is active on native

* Drop debug warning
2025-12-16 20:40:29 -06:00
Jason P
a0b4e8270e Rework isHighResolution to be an enum called ScreenResolution 2025-12-16 17:27:08 -06:00
Ben Meadors
22ee679ea5 Merge branch 'develop' into multi-message-Storage 2025-12-16 11:49:23 -06:00
Ben Meadors
203826374c Merge branch 'master' into develop 2025-12-16 11:45:08 -06:00
Austin
917794ebab PIO: Remove useless inheritence (references extends env) (#8987)
Remove lib_deps section for all PlatformIO envs which are unneeded (only references the `extends` lib_deps, thus pointless)

This makes the configs more concise and make future PIO variants/ libdeps audits easier.
2025-12-16 15:38:10 +11:00
Austin
ed77ba5612 Replace PIO fuzzy version matches (reproducible builds) (#8984)
This change does not introduce version *changes*, but simply "updates" to the version already being referenced by the fuzzy-match (^)
2025-12-15 19:48:34 -06:00
Austin
eafa8c7b47 PIO: Fix ESP32 sub-variant inheritance (#8983) 2025-12-15 19:04:03 -06:00
Austin
024ac74f5c rp2xx0: Update to arduino-pico 5.4.4 (#8979) 2025-12-15 16:09:59 -06:00
Jason P
d08fe49942 Merge branch 'develop' into multi-message-Storage 2025-12-14 15:01:18 -06:00
Ben Meadors
d0d375f1ff Merge pull request #8973 from meshtastic/master
Backmerge
2025-12-14 14:51:16 -06:00
Jason P
e8ebfc0513 Add Rebooting to DFU mode notification as a simple pop-up (#8970)
* Add DFU notification as a simple pop-up

* Add safe conditional of IF_SCREEN

* Forgot #if HAS_SCREEN
2025-12-14 14:50:41 -06:00
HarukiToreda
634aa9857f Fix for "delete this chat" now it does delete the current one 2025-12-14 00:44:01 -05:00
HarukiToreda
eb0cdc44ba Fix for content cutting off when from us 2025-12-14 00:32:39 -05:00
HarukiToreda
ddc8d2424b Fix to scrolling 2025-12-13 19:44:54 -05:00
Austin
bf32f17f28 Actions: Compact manifest job output summary (#8957) 2025-12-13 12:32:01 +11:00
Jason P
0597ae42c9 Update naming of Frame Visibility toggles 2025-12-12 17:12:21 -06:00
HarukiToreda
001699f16d Merge branch 'multi-message-Storage' of https://github.com/meshtastic/firmware into multi-message-Storage 2025-12-12 16:56:26 -05:00
Tom Fifield
f09183f5f9 Update exempt labels for stale bot workflow
Adds triaged and backlog to the list of exempt labels.
2025-12-12 13:40:00 -06:00
Jonathan Bennett
59e25ff1c2 Merge branch 'develop' into multi-message-Storage 2025-12-12 13:08:05 -06:00
Jason P
11cd6370d4 Dynamic scaling of column counts based upon screen size, clean up box drawing 2025-12-12 12:53:37 -06:00
HarukiToreda
bca169ddaf Page counters 2025-12-12 12:53:26 -06:00
HarukiToreda
b150c56735 Pagination fix for Latest to oldest per page 2025-12-12 12:53:10 -06:00
HarukiToreda
d6bf0a330a Add scrolling to Node list 2025-12-12 12:52:52 -06:00
Jason P
f3ebdec49e Short or Long Names for everyone! 2025-12-12 12:52:36 -06:00
Jason P
f68ef9f7d7 Reflow Node Lists and TLora Pager Views (#8942)
* Add files via upload

* Move files into the right place
2025-12-12 12:52:11 -06:00
Jason P
2dc54cdd4b Dynamic scaling of column counts based upon screen size, clean up box drawing 2025-12-11 22:42:40 -06:00
HarukiToreda
1b7104b1e2 Page counters 2025-12-11 23:08:56 -05:00
HarukiToreda
ef99939d6f Pagination fix for Latest to oldest per page 2025-12-11 22:43:21 -05:00
Jason P
4abd3f9a1f Merge branch 'develop' into multi-message-Storage 2025-12-11 20:57:06 -06:00
HarukiToreda
cfea55d77d Add scrolling to Node list 2025-12-11 20:28:43 -05:00
Jason P
71bc99938c Short or Long Names for everyone! 2025-12-11 17:22:17 -06:00
Jason P
eeb5d0478e Reflow Node Lists and TLora Pager Views (#8942)
* Add files via upload

* Move files into the right place
2025-12-11 14:25:48 -06:00
Jason P
4e7b87b099 Merge branch 'develop' into multi-message-Storage 2025-12-11 13:16:45 -06:00
Jason P
2634978e57 Revert "Set nodeName to maximum size"
This reverts commit e254f39925.
2025-12-11 09:29:37 -06:00
Jason P
7515123307 Merge branch 'develop' into multi-message-Storage 2025-12-10 20:49:00 -06:00
Jason P
bbfddea3af Merge branch 'develop' into multi-message-Storage 2025-12-10 20:44:01 -06:00
Jason P
4a35fd8317 Merge branch 'develop' into multi-message-Storage 2025-12-10 16:39:11 -06:00
Jason P
e254f39925 Set nodeName to maximum size 2025-12-10 15:45:32 -06:00
Jason P
0b22382cbb Merge branch 'develop' into multi-message-Storage 2025-12-10 11:29:24 -06:00
Jason P
9d5b69f9b4 Merge branch 'develop' into multi-message-Storage 2025-12-09 17:17:13 -06:00
Jason P
d4b44b1c63 Merge branch 'develop' into multi-message-Storage 2025-12-09 08:56:59 -06:00
Jason P
5ae962731b Merge branch 'develop' into multi-message-Storage 2025-12-09 07:41:02 -06:00
Ben Meadors
20887eb1f6 Merge pull request #8907 from mtoolstec/multi-message-storage
Multi message storage supports on UpDown Endoer device
2025-12-09 06:22:00 -06:00
whywilson
9d7bc381a1 Add nudge scroll on UpDownEncoder devices. 2025-12-09 11:06:51 +08:00
Jason P
6c02f82d87 Merge branch 'develop' into multi-message-Storage 2025-12-08 20:18:20 -06:00
Jason P
2bcf342d3d Merge branch 'develop' into multi-message-Storage 2025-12-08 14:47:34 -06:00
Jason P
85c2226c63 Update Screen.cpp to repair a merge issue 2025-12-08 07:32:20 -06:00
Jason P
6df2ae9f4a Update Screen.h for handleTextMessage 2025-12-08 07:22:57 -06:00
Jason P
12d1f50015 Merge branch 'develop' into multi-message-Storage 2025-12-08 07:14:57 -06:00
Jason P
b86a4c0b43 Merge branch 'develop' into multi-message-Storage 2025-12-07 21:52:42 -06:00
Jason P
f4fd3e7812 Merge branch 'develop' into multi-message-Storage 2025-12-07 17:42:13 -06:00
Jason P
fb932461c1 Correct up/down destinations on textMessage frame 2025-12-05 18:43:58 -06:00
Jason P
03dba769dd UpDown situational destination for textMessage 2025-12-05 14:56:03 -06:00
Jason P
87c0090e98 Restore CannedMessages on Home Frame 2025-12-05 09:05:07 -06:00
HarukiToreda
c0728b0eba Fix 2025-12-05 02:42:21 -05:00
HarukiToreda
1381bb1dfd Manual message scrolling 2025-12-05 02:01:23 -05:00
Jason P
903ddb5b95 Merge branch 'develop' into multi-message-Storage 2025-12-04 17:15:01 -06:00
Jason P
34b1652f38 Merge branch 'develop' into multi-message-Storage 2025-12-03 13:41:08 -06:00
Jason P
b29fe39f59 Correct string length calculation for signal bars 2025-12-03 13:40:30 -06:00
Jason P
0b43c5b52d Merge branch 'develop' into multi-message-Storage 2025-12-02 08:42:01 -06:00
Jason P
493eb32095 Merge branch 'develop' into multi-message-Storage 2025-12-01 19:35:40 -06:00
Jason P
ab044414a7 Merge branch 'develop' into multi-message-Storage 2025-12-01 18:58:11 -06:00
Jason P
bde661260b Merge branch 'develop' into multi-message-Storage 2025-11-30 22:04:16 -06:00
Jonathan Bennett
0d8c549269 Merge branch 'develop' into multi-message-Storage 2025-11-30 21:34:10 -06:00
Jason P
0295685be7 Merge branch 'develop' into multi-message-Storage 2025-11-30 21:28:36 -06:00
Jonathan Bennett
7d9f629df3 Remove the up/down shortcut to launch canned messages (#8370)
* Remove the up/down shortcut to launch canned messages

* Enabled MQTT and WEBSERVER by default (#8679)

Signed-off-by: kur1k0 <zhuzirun@m5stack.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>

---------

Signed-off-by: kur1k0 <zhuzirun@m5stack.com>
Co-authored-by: Riker <zhuzirun@m5stack.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-11-30 20:53:15 -06:00
Jonathan Bennett
ce6278f351 Merge branch 'develop' into multi-message-Storage 2025-11-30 19:42:31 -06:00
Jason P
4bf543bf85 Merge branch 'develop' into multi-message-Storage 2025-11-28 20:25:15 -06:00
Jason P
ebf8446c94 Merge branch 'develop' into multi-message-Storage 2025-11-28 18:50:03 -06:00
Jason P
a3cf1fb468 Merge branch 'develop' into multi-message-Storage 2025-11-28 13:01:35 -06:00
Jason P
ed0fadcdd2 Trunk Fixes 2025-11-28 13:01:15 -06:00
Jason P
a2600723d5 Merge branch 'develop' into multi-message-Storage 2025-11-27 07:06:17 -06:00
Jason P
7236e3647b Merge branch 'develop' into multi-message-Storage 2025-11-26 13:55:22 -06:00
Jason P
6ae6b3663c Fix for Muzi_Base 2025-11-26 08:15:18 -06:00
Jason P
f0af449dc2 Merge branch 'develop' into multi-message-Storage 2025-11-26 07:02:59 -06:00
Jason P
43cfe0336b Merge branch 'develop' into multi-message-Storage 2025-11-25 13:57:02 -06:00
Jason P
ceca3d131e Optimize code for background image 2025-11-25 09:54:54 -06:00
Jason P
69363d3bf4 Merge branch 'develop' into multi-message-Storage 2025-11-25 09:37:47 -06:00
Jason P
0607476e76 Create a background on the connected icon to reduce overlap impact 2025-11-24 10:42:05 -06:00
Jason P
922d15620f Merge branch 'develop' into multi-message-Storage 2025-11-22 09:03:53 -06:00
Jason P
1fcbee34f4 Merge branch 'develop' into multi-message-Storage 2025-11-21 06:51:50 -06:00
Jason P
da7d2815fc Merge branch 'develop' into multi-message-Storage 2025-11-20 10:14:43 -06:00
Jason P
586d4d2b99 Merge branch 'develop' into multi-message-Storage 2025-11-20 09:59:10 -06:00
Jason P
f6cf92ca84 Merge branch 'develop' into multi-message-Storage 2025-11-19 16:00:30 -06:00
Jason P
3a396e551d Merge branch 'develop' into multi-message-Storage 2025-11-17 21:12:36 -06:00
Jason P
b306bf042c Merge branch 'develop' into multi-message-Storage 2025-11-16 22:07:11 -06:00
Jason P
c6f3b2d76a Merge branch 'develop' into multi-message-Storage 2025-11-16 14:18:35 -06:00
Jason P
1ecfbe276d Merge branch 'develop' into multi-message-Storage 2025-11-16 09:13:38 -06:00
Jason P
8929231290 Merge branch 'develop' into multi-message-Storage 2025-11-14 22:29:27 -06:00
Jason P
aefebbbfc8 Desperate times call for desperate measures 2025-11-14 22:28:44 -06:00
Jason P
bfcfcb6805 Merge branch 'develop' into multi-message-Storage 2025-11-10 11:24:43 -06:00
Jason P
cde626cf56 Rework Delete flow 2025-11-10 11:23:25 -06:00
Jason P
fb2223ada3 Clean up some menu options and remove some Unit C6L ifdefines 2025-11-10 10:46:00 -06:00
HarukiToreda
46391ff5e3 trunk fix 2025-11-10 01:43:34 -05:00
HarukiToreda
9441f0c143 Message menu cleanup 2025-11-10 01:32:45 -05:00
HarukiToreda
b3f4e81827 fix for delete this chat 2025-11-10 00:53:52 -05:00
HarukiToreda
c30eb4b659 More delete options and cleanup of code 2025-11-10 00:42:49 -05:00
HarukiToreda
5c600ded1c Unkwnown nodes no longer show as ??? on message thread 2025-11-09 23:49:20 -05:00
Jason P
dd4b5d443a Merge branch 'develop' into multi-message-Storage 2025-11-08 23:10:17 -06:00
Jason P
09a38139f5 Fix broken endifs 2025-11-07 15:20:04 -06:00
Jason P
efc304ccf6 Merge branch 'develop' into multi-message-Storage 2025-11-07 15:08:01 -06:00
Jason P
65a5cec1c1 Don't run message persistent in MUI 2025-11-06 08:30:12 -06:00
Jason P
aace45305d Merge branch 'develop' into multi-message-Storage 2025-11-06 07:40:52 -06:00
Jason P
9e9eff0a43 Don't favorite if WE are CLIENT_BASE role 2025-11-05 10:31:15 -06:00
Jason P
6c3d2fd7b9 Merge branch 'develop' into multi-message-Storage 2025-11-05 10:19:17 -06:00
Jason P
e2d44829fe Restore auto favorite; but only if not CLIENT_BASE 2025-11-05 10:01:32 -06:00
Jason P
9f6b5f0a17 Improve getSafeNodeName further 2025-11-04 08:12:57 -06:00
Jason P
ccdb27d377 Improve getSafeNodeName / sanitizeString code. 2025-11-04 07:27:56 -06:00
Jason P
ef5016aa12 Merge branch 'develop' into multi-message-Storage 2025-11-04 06:59:34 -06:00
Jason P
33cbf35251 Merge branch 'develop' into multi-message-Storage 2025-11-03 12:56:04 -06:00
Jason P
6647900196 Merge branch 'develop' into multi-message-Storage 2025-11-02 21:32:07 -06:00
Jason P
c3b4e6bc9e Merge branch 'develop' into multi-message-Storage 2025-11-02 06:21:51 -06:00
Jason P
4c176d7829 Merge branch 'develop' into multi-message-Storage 2025-11-01 23:39:38 -05:00
Jason P
541a6b55ba Merge branch 'develop' into multi-message-Storage 2025-10-31 22:28:47 -05:00
Jason P
5b10510577 Add "Delete All Chats" to all chat views 2025-10-31 16:32:55 -05:00
Jason P
01a7389241 Merge branch 'develop' into multi-message-Storage 2025-10-31 07:57:49 -05:00
Jason P
022825e53b Merge branch 'develop' into multi-message-Storage 2025-10-27 13:17:41 -05:00
Jason P
db62d92cf6 Merge branch 'develop' into multi-message-Storage 2025-10-26 08:40:05 -05:00
Jason P
263ae8c79f Merge branch 'develop' into multi-message-Storage 2025-10-24 09:52:22 -05:00
Jason P
9ffdaedb48 Merge branch 'develop' into multi-message-Storage 2025-10-23 07:42:15 -05:00
Jason P
6926a50d70 Fix nullPointerRedundantCheck warning on ESP32 2025-10-22 15:44:37 -05:00
Jason P
ec04aa0a89 Fix sprintfOverlappingData issue 2025-10-22 15:21:46 -05:00
Jason P
c9aeafd227 Fix short name displays 2025-10-22 14:52:00 -05:00
Jason P
2c05baa1b4 Apply shortening to longNames in Select Destination 2025-10-22 14:33:20 -05:00
Jason P
3cb4e0e195 Merge branch 'develop' into multi-message-Storage 2025-10-20 08:23:05 -05:00
Jason P
d25110f971 Merge branch 'develop' into multi-message-Storage 2025-10-19 19:15:49 -05:00
Jason P
9a12304769 Revert changes to RedirectablePrint.cpp 2025-10-19 09:30:40 -05:00
Jason P
bc9c1877d8 Merge branch 'develop' into multi-message-Storage 2025-10-19 07:44:39 -05:00
Jason P
dd95748966 Merge branch 'develop' into multi-message-Storage 2025-10-19 07:14:12 -05:00
Jason P
a635ee3de2 Fix potential crash for undefined variable 2025-10-19 07:11:18 -05:00
Jason P
69f489ee70 Merge branch 'develop' into multi-message-Storage 2025-10-18 18:51:55 -05:00
Jason P
b26c95d4b9 Improve layout of messages screen 2025-10-18 10:26:45 -05:00
HarukiToreda
f09ac16f19 Trunk fix 2025-10-17 01:21:16 -04:00
HarukiToreda
c0361d2aea log messages sent from apps 2025-10-17 01:12:47 -04:00
Jason P
13458d3a6a Shorten longNames to not exceed message popups 2025-10-16 22:53:01 -05:00
HarukiToreda
14dfce5e03 Merge branch 'develop' into multi-message-Storage 2025-10-16 21:31:38 -04:00
HarukiToreda
f012f98024 cleanup 2025-10-16 21:28:51 -04:00
HarukiToreda
fbf7ab0455 force PKI 2025-10-16 20:38:30 -04:00
HarukiToreda
4f79475b3c improved destination filtering 2025-10-16 17:10:34 -04:00
Jason P
4aad9083bb Merge branch 'develop' into multi-message-Storage 2025-10-16 08:27:38 -05:00
Jason P
04c7720f4b Remove legacy function renderMessageContent 2025-10-16 08:16:25 -05:00
Jason P
3e3f03c78d Restore ellipsis to end of long names 2025-10-15 10:56:13 -05:00
Jason P
caff68f2f3 Merge branch 'develop' into multi-message-Storage 2025-10-15 08:33:00 -05:00
HarukiToreda
71353874a1 build error fixes 2025-10-15 04:04:20 -04:00
HarukiToreda
c8f3cbb0f9 Free Heap when not on Message screen 2025-10-15 03:06:59 -04:00
HarukiToreda
62eaabc940 More optimization 2025-10-15 01:57:51 -04:00
HarukiToreda
67c24c08cc Removing old left over code 2025-10-14 15:17:11 -04:00
HarukiToreda
4bd53500c6 Switch from dynamic std::string storage to fixed-size char[] 2025-10-14 14:14:28 -04:00
Jason P
6cd64cc228 Merge branch 'multi-message-Storage' of https://github.com/meshtastic/firmware into multi-message-Storage 2025-10-14 09:37:40 -05:00
Jason P
a05936f655 Revert only RangeTestModule.cpp change 2025-10-14 09:37:37 -05:00
HarukiToreda
aaf4a7e59e remove memory usage debug 2025-10-14 10:18:49 -04:00
Jason P
c180f23026 Implement Haruki's ClockRenderer and broadcast decomposeTime across various files. Attempt 2! 2025-10-14 08:09:12 -05:00
Jason P
ee3c7f2272 Revert "Implement Haruki's ClockRenderer and broadcast decomposeTime across various files."
This reverts commit 2f65721774.
2025-10-13 22:44:37 -05:00
Jason P
2f65721774 Implement Haruki's ClockRenderer and broadcast decomposeTime across various files. 2025-10-13 22:29:42 -05:00
Jason P
849a749b81 Fixup Waypoint screen with BaseUI code 2025-10-13 17:07:48 -05:00
HarukiToreda
214aa8b59d Trunk fix 2025-10-13 17:07:26 -04:00
HarukiToreda
8fb825e0e0 Waypoint cleanup 2025-10-13 16:57:42 -04:00
HarukiToreda
4ca56ec9cb Fn symbol code removed 2025-10-13 16:09:46 -04:00
HarukiToreda
3bb5a3341c More cleanup 2025-10-13 15:16:22 -04:00
HarukiToreda
7aa5b93895 Dimiss key combo function deprecated 2025-10-13 13:57:04 -04:00
Jason P
835f13031c Merge branch 'develop' into multi-message-Storage 2025-10-13 12:51:43 -05:00
Jason P
5f8f3cf8be Fix another build error on occassion 2025-10-13 12:00:03 -05:00
Jason P
56656a4e6a Merge branch 'develop' into multi-message-Storage 2025-10-13 11:11:31 -05:00
Jason P
2f53f3f1dc Remove unused dismissNewestMessage 2025-10-13 11:03:16 -05:00
Jason P
8611175628 Remove used getConversationWith 2025-10-13 11:01:48 -05:00
Jason P
b0c0faa075 Merge branch 'develop' into multi-message-Storage 2025-10-13 07:00:21 -05:00
HarukiToreda
f35f72edb1 More cleanup 2025-10-13 03:21:37 -04:00
HarukiToreda
0b11f93880 more fixes 2025-10-12 22:36:15 -04:00
Jason P
e38925834d Optimize Hi Rez Chirpy to save space 2025-10-12 19:43:14 -05:00
HarukiToreda
551086324b Trunk fixes 2025-10-12 16:34:59 -04:00
HarukiToreda
50a65a1393 cleanup to get more space 2025-10-12 16:24:40 -04:00
Jason P
65dcd8254e Missed a comma in merge conflicts 2025-10-12 08:28:45 -05:00
Jason P
f9e31558d1 Merge branch 'develop' into multi-message-Storage 2025-10-12 08:27:32 -05:00
Jason P
6dfaf23baf Update channel mute for adjusted protobuf 2025-10-12 08:04:01 -05:00
Jason P
544331d367 Merge branch 'develop' into multi-message-Storage 2025-10-12 07:49:53 -05:00
Jason P
5e5a449226 Merge branch 'develop' into multi-message-Storage 2025-10-11 16:10:25 -05:00
Jason P
9e9d2af7c8 Attempt to fix memory usage of invalidLifetime 2025-10-11 09:19:48 -05:00
Jason P
e934f8f0b3 Use text aligns for message layout where necessary 2025-10-11 08:56:26 -05:00
Jason P
1d7fe20520 More C6L fixes and clean up header lines 2025-10-10 14:54:41 -05:00
HarukiToreda
abeeb12d96 preset aware signal strength display 2025-10-10 15:14:28 -04:00
HarukiToreda
97578fb9c0 Fix to many warnings related to BaseUI 2025-10-10 13:46:40 -04:00
Jason P
e606d88297 Remove duplicate code, fix more Chats, and fix C6L MessageRenderer 2025-10-10 11:02:16 -05:00
HarukiToreda
163d8e0540 fix for notification renderer 2025-10-10 11:25:09 -04:00
HarukiToreda
784e71f2fa Signal bars for message ack 2025-10-10 10:50:15 -04:00
Jason P
84cef67b82 Fix builds for HELTEC_MESH_SOLAR 2025-10-10 09:34:43 -05:00
Jason P
6d899c9fd9 Clean up how muting works along with when we wake the screen 2025-10-10 08:58:27 -05:00
HarukiToreda
6069dc2ad8 trunk fix 2025-10-10 00:18:36 -04:00
HarukiToreda
e6093533ab Mute channel fix 2025-10-09 20:25:48 -04:00
Jason P
334089aa97 Consolidate wording on "Chats" 2025-10-09 18:30:01 -05:00
Jason P
e93b65706b Reorder Favorite Action Menu with simple word modifications 2025-10-09 17:35:34 -05:00
HarukiToreda
7970a32074 Go to thread from favorite screen 2025-10-09 18:19:59 -04:00
Jason P
20e9703e1b Merge branch 'develop' into multi-message-Storage 2025-10-07 22:32:34 -05:00
Jason P
541e050e40 Reword menus to better reflect actions 2025-10-07 16:41:44 -05:00
Jason P
974b3e6133 Merge branch 'develop' into multi-message-Storage 2025-10-07 15:59:17 -05:00
Jason P
9e520d008b Reorder menu options and reword Respond 2025-10-07 09:47:57 -05:00
Jason P
def5018c9d Merge branch 'develop' into multi-message-Storage 2025-10-07 06:57:55 -05:00
Jason P
d5ce4696f3 Provide some extra spacing for low hanging characters in messages 2025-10-06 17:42:03 -05:00
HarukiToreda
cb8a8a2c52 Merge branch 'develop' into multi-message-Storage 2025-10-06 16:00:09 -04:00
Jonathan Bennett
993e874eec Don't error out with unset MAC address in unit tests 2025-10-06 12:31:29 -05:00
HarukiToreda
9f62007d94 Merge branch 'multi-message-Storage' of https://github.com/meshtastic/firmware into multi-message-Storage 2025-10-06 12:29:46 -04:00
HarukiToreda
67c0000f87 Build fail fix 2025-10-06 12:29:43 -04:00
Jason P
e7cdc7cc29 Merge branch 'develop' into multi-message-Storage 2025-10-06 08:47:34 -05:00
HarukiToreda
b549786bbb revert 2025-10-06 01:05:12 -04:00
HarukiToreda
103f73e7c9 gating for message storage when not using a screen 2025-10-06 00:49:06 -04:00
HarukiToreda
383d95ade1 Eink autoscroll dissabled 2025-10-06 00:48:17 -04:00
Jason P
9b57b21ab4 Merge branch 'develop' into multi-message-Storage 2025-10-05 22:48:51 -05:00
Jason P
0b96486e7e Fix outbound labels based to avoid creating delays 2025-10-05 22:38:31 -05:00
HarukiToreda
b8d33a3280 crash fix for confirmation nodes 2025-10-05 23:34:17 -04:00
Jason P
c9314c78ca Better to say "in" vs "on" 2025-10-05 15:39:55 -05:00
Jason P
bbec5177ba Add context for incoming messages 2025-10-05 15:33:53 -05:00
Jason P
8860f6195f Continue unifying display, also show message status on the "isMine" lines 2025-10-05 15:01:22 -05:00
Jason P
bebb1e9e8d Change DM to @ in order to unify on a single method 2025-10-05 14:33:30 -05:00
Jason P
13eb53fcf6 Lengthen channel name and finalize cleanup removal of Broadcast 2025-10-05 14:19:46 -05:00
Jason P
4e840943c6 Sanity checks are okay sometimes 2025-10-05 13:54:31 -05:00
HarukiToreda
65bb78b34f Build error fix 2025-10-01 23:14:28 -04:00
HarukiToreda
0476eeec9e Merge remote-tracking branch 'upstream/develop' into multi-message-Storage 2025-10-01 20:13:07 -04:00
HarukiToreda
7642d0c401 Memory size debug 2025-09-29 20:10:05 -04:00
HarukiToreda
526c7f8e4d Merge remote-tracking branch 'upstream/develop' into multi-message-Storage 2025-09-28 19:20:35 -04:00
HarukiToreda
7f1dc4e76a Merge remote-tracking branch 'upstream/develop' into multi-message-Storage 2025-09-28 18:09:29 -04:00
HarukiToreda
34bb858588 Emote picker fix 2025-09-28 02:32:28 -04:00
HarukiToreda
4ae5e8a48e removed legacy temporary messages 2025-09-28 01:41:51 -04:00
HarukiToreda
e3553c4eb3 Dismiss feature fixed 2025-09-28 01:31:32 -04:00
HarukiToreda
3e4f654f58 Ack message cleanup 2025-09-28 00:58:48 -04:00
HarukiToreda
28502c93c9 Ack on messages sent 2025-09-26 16:51:09 -04:00
HarukiToreda
07d3726cde Cannedmessage cleanup and emotes fixed 2025-09-26 15:10:19 -04:00
HarukiToreda
ebbb8a6f9f Decoupled message packets from screen.cpp and cleaned up 2025-09-26 00:28:25 -04:00
HarukiToreda
dd7a5cf31f Messages from phone show on screen 2025-09-25 22:44:23 -04:00
HarukiToreda
46a8a9a89e Merge remote-tracking branch 'upstream/develop' into multi-message-Storage 2025-09-24 10:04:21 -04:00
HarukiToreda
4d10026185 Merge remote-tracking branch 'upstream/develop' into multi-message-Storage 2025-09-23 14:00:13 -04:00
HarukiToreda
91e1029a3b dismiss all live fix 2025-09-23 09:46:54 -04:00
HarukiToreda
8333ceb55e rename Select View Mode to Select Conversation 2025-09-23 09:35:52 -04:00
HarukiToreda
ea7638b4ec Reply in thread feature 2025-09-23 03:42:32 -04:00
HarukiToreda
3780290581 rename of view mode to Conversations 2025-09-23 01:21:45 -04:00
HarukiToreda
6ead5c04bd fix for message time 2025-09-23 01:05:22 -04:00
HarukiToreda
4e61016a44 Fix for DM threading 2025-09-22 21:07:52 -04:00
HarukiToreda
8040bb20b4 trunk fix 2025-09-22 20:20:31 -04:00
HarukiToreda
3d8b4a68b8 Add channel name instead of channel slot 2025-09-22 20:10:05 -04:00
HarukiToreda
abcc166f3a Message view mode 2025-09-22 03:30:16 -04:00
HarukiToreda
d779821f0e Nrf built issue fix 2025-09-21 18:28:37 -04:00
HarukiToreda
cf9bc7ac00 First try at multimessage storage and display 2025-09-21 17:40:26 -04:00
225 changed files with 6000 additions and 2216 deletions

View File

@@ -21,7 +21,7 @@ jobs:
# Use 'arctastic' self-hosted runner pool when building in the main repo
runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }}
outputs:
artifact-id: ${{ steps.upload.outputs.artifact-id }}
artifact-id: ${{ steps.upload-firmware.outputs.artifact-id }}
steps:
- uses: actions/checkout@v6
with:
@@ -71,7 +71,7 @@ jobs:
- name: Store binaries as an artifact
uses: actions/upload-artifact@v6
id: upload
id: upload-firmware
with:
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
overwrite: true
@@ -84,3 +84,12 @@ jobs:
release/*.zip
release/device-*.sh
release/device-*.bat
- name: Store manifests as an artifact
uses: actions/upload-artifact@v6
id: upload-manifest
with:
name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
overwrite: true
path: |
release/*.mt.json

View File

@@ -233,6 +233,40 @@ jobs:
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }}
shame:
if: github.repository == 'meshtastic/firmware'
continue-on-error: true
runs-on: ubuntu-latest
needs: [build]
steps:
- uses: actions/checkout@v6
with:
filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head)
fetch-depth: 0
- name: Download the current manifests
uses: actions/download-artifact@v7
with:
path: ./manifests-new/
pattern: manifest-*
merge-multiple: true
- name: Upload combined manifests for later commit and global stats crunching.
uses: actions/upload-artifact@v6
id: upload-manifest
with:
name: manifests-all
overwrite: true
path: |
manifests-new/*.mt.json
- name: Find the merge base
run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV
env:
base: ${{ github.base_ref }}
head: ${{ github.head_ref }}
- name: Download the old manifests
run: gh run download -R ${{ github.repository }} --commit ${{ env.MERGE_BASE }} --name manifests-all --dir manifest-old/
- name: Do scan and post comment
run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/
release-artifacts:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}

View File

@@ -16,7 +16,7 @@ jobs:
submodules: true
- name: Update submodule
if: ${{ github.ref == 'refs/heads/master' }}
if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }}
run: |
git submodule update --remote protobufs

View File

@@ -21,14 +21,13 @@ rm -f $BUILDDIR/firmware*
export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
ota_basename=${basename}-ota
pio run --environment $1 -t mtjson # -v
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
echo "Copying NRF52 dfu (OTA) file"
cp $BUILDDIR/$basename.zip $OUTDIR/$ota_basename.zip
cp $BUILDDIR/$basename.zip $OUTDIR/$basename.zip
echo "Copying NRF52 UF2 file"
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2

View File

@@ -87,6 +87,9 @@
</screenshots>
<releases>
<release version="2.7.18" date="2025-12-20">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18</url>
</release>
<release version="2.7.17" date="2025-11-28">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17</url>
</release>

View File

@@ -17,8 +17,6 @@ lfsbin = f"{progname.replace('firmware-', 'littlefs-')}.bin"
def manifest_gather(source, target, env):
out = []
board_platform = env.BoardConfig().get("platform")
needs_ota_suffix = board_platform == "nordicnrf52"
check_paths = [
progname,
f"{progname}.elf",
@@ -34,11 +32,8 @@ def manifest_gather(source, target, env):
for p in check_paths:
f = env.File(env.subst(f"$BUILD_DIR/{p}"))
if f.exists():
manifest_name = p
if needs_ota_suffix and p == f"{progname}.zip":
manifest_name = f"{progname}-ota.zip"
d = {
"name": manifest_name,
"name": p,
"md5": f.get_content_hash(), # Returns MD5 hash
"bytes": f.get_size() # Returns file size in bytes
}

95
bin/shame.py Normal file
View File

@@ -0,0 +1,95 @@
import sys
import os
import json
from github import Github
def parseFile(path):
with open(path, "r") as f:
data = json.loads(f)
for file in data["files"]:
if file["name"].endswith(".bin"):
return file["name"], file["bytes"]
if len(sys.argv) != 4:
print(f"expected usage: {sys.argv[0]} <PR number> <path to old-manifests> <path to new-manifests>")
sys.exit(1)
pr_number = int(sys.argv[1])
token = os.getenv("GITHUB_TOKEN")
if not token:
raise EnvironmentError("GITHUB_TOKEN not found in environment.")
repo_name = os.getenv("GITHUB_REPOSITORY") # "owner/repo"
if not repo_name:
raise EnvironmentError("GITHUB_REPOSITORY not found in environment.")
oldFiles = sys.argv[2]
old = set(os.path.join(oldFiles, f) for f in os.listdir(oldFiles) if os.path.isfile(f))
newFiles = sys.argv[3]
new = set(os.path.join(newFiles, f) for f in os.listdir(newFiles) if os.path.isfile(f))
startMarkdown = "# Target Size Changes\n\n"
markdown = ""
newlyIntroduced = new - old
if len(newlyIntroduced) > 0:
markdown += "## Newly Introduced Targets\n\n"
# create a table
markdown += "| File | Size |\n"
markdown += "| ---- | ---- |\n"
for f in newlyIntroduced:
name, size = parseFile(f)
markdown += f"| `{name}` | {size}b |\n"
# do not log removed targets
# PRs only run a small subset of builds, so removed targets are not meaningful
# since they are very likely to just be not ran in PR CI
both = old & new
degradations = []
improvements = []
for f in both:
oldName, oldSize = parseFile(f)
_, newSize = parseFile(f)
if oldSize != newSize:
if newSize < oldSize:
improvements.append((oldName, oldSize, newSize))
else:
degradations.append((oldName, oldSize, newSize))
if len(degradations) > 0:
markdown += "\n## Degradation\n\n"
# create a table
markdown += "| File | Difference | Old Size | New Size |\n"
markdown += "| ---- | ---------- | -------- | -------- |\n"
for oldName, oldSize, newSize in degradations:
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
if len(improvements) > 0:
markdown += "\n## Improvement\n\n"
# create a table
markdown += "| File | Difference | Old Size | New Size |\n"
markdown += "| ---- | ---------- | -------- | -------- |\n"
for oldName, oldSize, newSize in improvements:
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
if len(markdown) == 0:
markdown = "No changes in target sizes detected."
g = Github(token)
repo = g.get_repo(repo_name)
pr = repo.get_pull(pr_number)
existing_comment = None
for comment in pr.get_issue_comments():
if comment.body.startswith(startMarkdown):
existing_comment = comment
break
final_markdown = startMarkdown + markdown
if existing_comment:
existing_comment.edit(body=final_markdown)
else:
pr.create_issue_comment(body=final_markdown)

6
debian/changelog vendored
View File

@@ -1,3 +1,9 @@
meshtasticd (2.7.18.0) unstable; urgency=medium
* Version 2.7.18
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Sat, 20 Dec 2025 15:47:25 +0000
meshtasticd (2.7.17.0) unstable; urgency=medium
* Version 2.7.17

View File

@@ -103,17 +103,13 @@ lib_deps =
thingsboard/TBPubSubClient@2.12.1
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
arduino-libraries/NTPClient@3.2.1
; Extra TCP/IP networking libs for supported devices
[networking_extra]
lib_deps =
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
arcao/Syslog@2.0.0
; Minimal networking libs for nrf52 (excludes Syslog to save flash)
[nrf52_networking_base]
lib_deps =
# renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient
thingsboard/TBPubSubClient@2.12.1
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
arduino-libraries/NTPClient@3.2.1
[radiolib_base]
lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
@@ -162,8 +158,8 @@ lib_deps =
emotibit/EmotiBit MLX90632@1.0.8
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
adafruit/Adafruit MLX90614 Library@2.1.5
# renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221
https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a
# renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow
https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
mprograms/QMC5883LCompass@1.2.3
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU

426
src/MessageStore.cpp Normal file
View File

@@ -0,0 +1,426 @@
#include "configuration.h"
#if HAS_SCREEN
#include "FSCommon.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "SPILock.h"
#include "SafeFile.h"
#include "gps/RTC.h"
#include "graphics/draw/MessageRenderer.h"
#include <cstring> // memcpy
#ifndef MESSAGE_TEXT_POOL_SIZE
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
#endif
// Global message text pool and state
static char *g_messagePool = nullptr;
static size_t g_poolWritePos = 0;
// Reset pool (called on boot or clear)
static inline void resetMessagePool()
{
if (!g_messagePool) {
g_messagePool = static_cast<char *>(malloc(MESSAGE_TEXT_POOL_SIZE));
if (!g_messagePool) {
LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE);
return;
}
}
g_poolWritePos = 0;
memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE);
}
// Allocate text in pool and return offset
// If not enough space remains, wrap around (ring buffer style)
static inline uint16_t storeTextInPool(const char *src, size_t len)
{
if (len >= MAX_MESSAGE_SIZE)
len = MAX_MESSAGE_SIZE - 1;
// Wrap pool if out of space
if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) {
g_poolWritePos = 0;
}
uint16_t offset = g_poolWritePos;
memcpy(&g_messagePool[g_poolWritePos], src, len);
g_messagePool[g_poolWritePos + len] = '\0';
g_poolWritePos += (len + 1);
return offset;
}
// Retrieve a const pointer to message text by offset
static inline const char *getTextFromPool(uint16_t offset)
{
if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE)
return "";
return &g_messagePool[offset];
}
// Helper: assign a timestamp (RTC if available, else boot-relative)
static inline void assignTimestamp(StoredMessage &sm)
{
uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
if (nowSecs) {
sm.timestamp = nowSecs;
sm.isBootRelative = false;
} else {
sm.timestamp = millis() / 1000;
sm.isBootRelative = true;
}
}
// Generic push with cap (used by live + persisted queues)
template <typename T> static inline void pushWithLimit(std::deque<T> &queue, const T &msg)
{
if (queue.size() >= MAX_MESSAGES_SAVED)
queue.pop_front();
queue.push_back(msg);
}
template <typename T> static inline void pushWithLimit(std::deque<T> &queue, T &&msg)
{
if (queue.size() >= MAX_MESSAGES_SAVED)
queue.pop_front();
queue.emplace_back(std::move(msg));
}
MessageStore::MessageStore(const std::string &label)
{
filename = "/Messages_" + label + ".msgs";
resetMessagePool(); // initialize text pool on boot
}
// Live message handling (RAM only)
void MessageStore::addLiveMessage(StoredMessage &&msg)
{
pushWithLimit(liveMessages, std::move(msg));
}
void MessageStore::addLiveMessage(const StoredMessage &msg)
{
pushWithLimit(liveMessages, msg);
}
// Add from incoming/outgoing packet
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
{
StoredMessage sm;
assignTimestamp(sm);
sm.channelIndex = packet.channel;
const char *payload = reinterpret_cast<const char *>(packet.decoded.payload.bytes);
size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1);
sm.textOffset = storeTextInPool(payload, len);
sm.textLength = len;
// Determine sender
uint32_t localNode = nodeDB->getNodeNum();
sm.sender = (packet.from == 0) ? localNode : packet.from;
sm.dest = packet.to;
bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST);
if (packet.from == 0) {
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
sm.ackStatus = AckStatus::NONE;
} else {
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
sm.ackStatus = AckStatus::ACKED;
}
addLiveMessage(sm);
return liveMessages.back();
}
// Outgoing/manual message
void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text)
{
StoredMessage sm;
// Always use our local time (helper handles RTC vs boot time)
assignTimestamp(sm);
sm.sender = sender;
sm.channelIndex = channelIndex;
sm.textOffset = storeTextInPool(text.c_str(), text.size());
sm.textLength = text.size();
// Use the provided destination
sm.dest = sender;
sm.type = MessageType::DM_TO_US;
// Outgoing messages always start with unknown ack status
sm.ackStatus = AckStatus::NONE;
addLiveMessage(sm);
}
#if ENABLE_MESSAGE_PERSISTENCE
// Compact, fixed-size on-flash representation using offset + length
struct __attribute__((packed)) StoredMessageRecord {
uint32_t timestamp;
uint32_t sender;
uint8_t channelIndex;
uint32_t dest;
uint8_t isBootRelative;
uint8_t ackStatus; // static_cast<uint8_t>(AckStatus)
uint8_t type; // static_cast<uint8_t>(MessageType)
uint16_t textLength; // message length
char text[MAX_MESSAGE_SIZE]; // store actual text here
};
// Serialize one StoredMessage to flash
static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m)
{
StoredMessageRecord rec = {};
rec.timestamp = m.timestamp;
rec.sender = m.sender;
rec.channelIndex = m.channelIndex;
rec.dest = m.dest;
rec.isBootRelative = m.isBootRelative;
rec.ackStatus = static_cast<uint8_t>(m.ackStatus);
rec.type = static_cast<uint8_t>(m.type);
rec.textLength = m.textLength;
// Copy the actual text into the record from RAM pool
const char *txt = getTextFromPool(m.textOffset);
strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1);
rec.text[MAX_MESSAGE_SIZE - 1] = '\0';
f.write(reinterpret_cast<const uint8_t *>(&rec), sizeof(rec));
}
// Deserialize one StoredMessage from flash; returns false on short read
static inline bool readMessageRecord(File &f, StoredMessage &m)
{
StoredMessageRecord rec = {};
if (f.readBytes(reinterpret_cast<char *>(&rec), sizeof(rec)) != sizeof(rec))
return false;
m.timestamp = rec.timestamp;
m.sender = rec.sender;
m.channelIndex = rec.channelIndex;
m.dest = rec.dest;
m.isBootRelative = rec.isBootRelative;
m.ackStatus = static_cast<AckStatus>(rec.ackStatus);
m.type = static_cast<MessageType>(rec.type);
m.textLength = rec.textLength;
// 💡 Re-store text into pool and update offset
m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1);
m.textOffset = storeTextInPool(rec.text, m.textLength);
return true;
}
void MessageStore::saveToFlash()
{
#ifdef FSCom
// Ensure root exists
spiLock->lock();
FSCom.mkdir("/");
spiLock->unlock();
SafeFile f(filename.c_str(), false);
spiLock->lock();
uint8_t count = static_cast<uint8_t>(liveMessages.size());
if (count > MAX_MESSAGES_SAVED)
count = MAX_MESSAGES_SAVED;
f.write(&count, 1);
for (uint8_t i = 0; i < count; ++i) {
writeMessageRecord(f, liveMessages[i]);
}
spiLock->unlock();
f.close();
#endif
}
void MessageStore::loadFromFlash()
{
std::deque<StoredMessage>().swap(liveMessages);
resetMessagePool(); // reset pool when loading
#ifdef FSCom
concurrency::LockGuard guard(spiLock);
if (!FSCom.exists(filename.c_str()))
return;
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
if (!f)
return;
uint8_t count = 0;
f.readBytes(reinterpret_cast<char *>(&count), 1);
if (count > MAX_MESSAGES_SAVED)
count = MAX_MESSAGES_SAVED;
for (uint8_t i = 0; i < count; ++i) {
StoredMessage m;
if (!readMessageRecord(f, m))
break;
liveMessages.push_back(m);
}
f.close();
#endif
}
#else
// If persistence is disabled, these functions become no-ops
void MessageStore::saveToFlash() {}
void MessageStore::loadFromFlash() {}
#endif
// Clear all messages (RAM + persisted queue)
void MessageStore::clearAllMessages()
{
std::deque<StoredMessage>().swap(liveMessages);
resetMessagePool();
#ifdef FSCom
SafeFile f(filename.c_str(), false);
uint8_t count = 0;
f.write(&count, 1); // write "0 messages"
f.close();
#endif
}
// Internal helper: erase first or last message matching a predicate
template <typename Predicate> static void eraseIf(std::deque<StoredMessage> &deque, Predicate pred, bool fromBack = false)
{
if (fromBack) {
// Iterate from the back and erase all matches from the end
for (auto it = deque.rbegin(); it != deque.rend();) {
if (pred(*it)) {
it = std::deque<StoredMessage>::reverse_iterator(deque.erase(std::next(it).base()));
} else {
++it;
}
}
} else {
// Manual forward search to erase all matches
for (auto it = deque.begin(); it != deque.end();) {
if (pred(*it)) {
it = deque.erase(it);
} else {
++it;
}
}
}
}
// Delete oldest message (RAM + persisted queue)
void MessageStore::deleteOldestMessage()
{
eraseIf(liveMessages, [](StoredMessage &) { return true; });
saveToFlash();
}
// Delete oldest message in a specific channel
void MessageStore::deleteOldestMessageInChannel(uint8_t channel)
{
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
eraseIf(liveMessages, pred);
saveToFlash();
}
void MessageStore::deleteAllMessagesInChannel(uint8_t channel)
{
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
eraseIf(liveMessages, pred, false /* delete ALL, not just first */);
saveToFlash();
}
void MessageStore::deleteAllMessagesWithPeer(uint32_t peer)
{
uint32_t local = nodeDB->getNodeNum();
auto pred = [&](const StoredMessage &m) {
if (m.type != MessageType::DM_TO_US)
return false;
uint32_t other = (m.sender == local) ? m.dest : m.sender;
return other == peer;
};
eraseIf(liveMessages, pred, false);
saveToFlash();
}
// Delete oldest message in a direct chat with a node
void MessageStore::deleteOldestMessageWithPeer(uint32_t peer)
{
auto pred = [peer](const StoredMessage &m) {
if (m.type != MessageType::DM_TO_US)
return false;
uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
return other == peer;
};
eraseIf(liveMessages, pred);
saveToFlash();
}
std::deque<StoredMessage> MessageStore::getChannelMessages(uint8_t channel) const
{
std::deque<StoredMessage> result;
for (const auto &m : liveMessages) {
if (m.type == MessageType::BROADCAST && m.channelIndex == channel) {
result.push_back(m);
}
}
return result;
}
std::deque<StoredMessage> MessageStore::getDirectMessages() const
{
std::deque<StoredMessage> result;
for (const auto &m : liveMessages) {
if (m.type == MessageType::DM_TO_US) {
result.push_back(m);
}
}
return result;
}
// Upgrade boot-relative timestamps once RTC is valid
// Only same-boot boot-relative messages are healed.
// Persisted boot-relative messages from old boots stay ??? forever.
void MessageStore::upgradeBootRelativeTimestamps()
{
uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
if (nowSecs == 0)
return; // Still no valid RTC
uint32_t bootNow = millis() / 1000;
auto fix = [&](std::deque<StoredMessage> &dq) {
for (auto &m : dq) {
if (m.isBootRelative && m.timestamp <= bootNow) {
uint32_t bootOffset = nowSecs - bootNow;
m.timestamp += bootOffset;
m.isBootRelative = false;
}
}
};
fix(liveMessages);
}
const char *MessageStore::getText(const StoredMessage &msg)
{
// Wrapper around the internal helper
return getTextFromPool(msg.textOffset);
}
uint16_t MessageStore::storeText(const char *src, size_t len)
{
// Wrapper around the internal helper
return storeTextInPool(src, len);
}
// Global definition
MessageStore messageStore("default");
#endif

131
src/MessageStore.h Normal file
View File

@@ -0,0 +1,131 @@
#pragma once
#if HAS_SCREEN
// Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints
#if defined(HELTEC_MESH_SOLAR)
#define LOG_DEBUG(...)
#endif
// Enable or disable message persistence (flash storage)
// Define -DENABLE_MESSAGE_PERSISTENCE=0 in build_flags to disable it entirely
#ifndef ENABLE_MESSAGE_PERSISTENCE
#define ENABLE_MESSAGE_PERSISTENCE 1
#endif
#include "mesh/generated/meshtastic/mesh.pb.h"
#include <cstdint>
#include <deque>
#include <string>
// How many messages are stored (RAM + flash).
// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage.
#ifndef MESSAGE_HISTORY_LIMIT
#define MESSAGE_HISTORY_LIMIT 20
#endif
// Internal alias used everywhere in code do NOT redefine elsewhere.
#define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT
// Maximum text payload size per message in bytes.
// This still defines the max message length, but we no longer reserve this space per message.
#define MAX_MESSAGE_SIZE 220
// Total shared text pool size for all messages combined.
// The text pool is RAM-only. Text is re-stored from flash into the pool on boot.
#ifndef MESSAGE_TEXT_POOL_SIZE
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
#endif
// Explicit message classification
enum class MessageType : uint8_t {
BROADCAST = 0, // broadcast message
DM_TO_US = 1 // direct message addressed to this node
};
// Delivery status for messages we sent
enum class AckStatus : uint8_t {
NONE = 0, // just sent, waiting (no symbol shown)
ACKED = 1, // got a valid ACK from destination
NACKED = 2, // explicitly failed
TIMEOUT = 3, // no ACK after retry window
RELAYED = 4 // got an ACK from relay, not destination
};
struct StoredMessage {
uint32_t timestamp; // When message was created (secs since boot or RTC)
uint32_t sender; // NodeNum of sender
uint8_t channelIndex; // Channel index used
uint32_t dest; // Destination node (broadcast or direct)
MessageType type; // Derived from dest (explicit classification)
bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute
AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages)
// Text storage metadata — rebuilt from flash at boot
uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash())
uint16_t textLength; // Length of text in bytes
// Default constructor initializes all fields safely
StoredMessage()
: timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false),
ackStatus(AckStatus::NONE), textOffset(0), textLength(0)
{
}
};
class MessageStore
{
public:
explicit MessageStore(const std::string &label);
// Live RAM methods (always current, used by UI and runtime)
void addLiveMessage(StoredMessage &&msg);
void addLiveMessage(const StoredMessage &msg); // convenience overload
const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; }
// Add new messages from packets or manual input
const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only
void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add
// Persistence methods (used only on boot/shutdown)
void saveToFlash(); // Save messages to flash
void loadFromFlash(); // Load messages from flash
// Clear all messages (RAM + persisted queue + text pool)
void clearAllMessages();
// Delete helpers
void deleteOldestMessage(); // remove oldest from RAM (and flash on save)
void deleteOldestMessageInChannel(uint8_t channel);
void deleteOldestMessageWithPeer(uint32_t peer);
void deleteAllMessagesInChannel(uint8_t channel);
void deleteAllMessagesWithPeer(uint32_t peer);
// Unified accessor (for UI code, defaults to RAM buffer)
const std::deque<StoredMessage> &getMessages() const { return liveMessages; }
// Helper filters for future use
std::deque<StoredMessage> getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel
std::deque<StoredMessage> getDirectMessages() const; // Only direct messages
// Upgrade boot-relative timestamps once RTC is valid
void upgradeBootRelativeTimestamps();
// Retrieve the C-string text for a stored message
static const char *getText(const StoredMessage &msg);
// Allocate text into pool (used by sender-side code)
static uint16_t storeText(const char *src, size_t len);
// Used when loading from flash to rebuild the text pool
static uint16_t rebuildTextFromFlash(const char *src, size_t len);
private:
std::deque<StoredMessage> liveMessages; // Single in-RAM message buffer (also used for persistence)
std::string filename; // Flash filename for persistence
};
// Global instance (defined in MessageStore.cpp)
extern MessageStore messageStore;
#endif

View File

@@ -11,6 +11,7 @@
* For more information, see: https://meshtastic.org/
*/
#include "power.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "Throttle.h"
@@ -786,7 +787,9 @@ void Power::shutdown()
playShutdownMelody();
#endif
nodeDB->saveToDisk();
#if HAS_SCREEN
messageStore.saveToFlash();
#endif
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
#ifdef PIN_LED1
ledOff(PIN_LED1);

View File

@@ -131,6 +131,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
int hour = hms / SEC_PER_HOUR;
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
#ifdef ARCH_PORTDUINO
::printf("%s ", logLevel);
if (color) {

View File

@@ -29,8 +29,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#if __has_include("Melopero_RV3028.h")
#include "Melopero_RV3028.h"
#endif
#if __has_include("pcf8563.h")
#include "pcf8563.h"
#if __has_include("SensorRtcHelper.hpp")
#include "SensorRtcHelper.hpp"
#endif
/* Offer chance for variant-specific defines */

View File

@@ -25,8 +25,8 @@ ScanI2C::FoundDevice ScanI2C::firstScreen() const
ScanI2C::FoundDevice ScanI2C::firstRTC() const
{
ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_RX8130CE};
return firstOfOrNONE(3, types);
ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_PCF85063, RTC_RX8130CE};
return firstOfOrNONE(4, types);
}
ScanI2C::FoundDevice ScanI2C::firstKeyboard() const

View File

@@ -14,6 +14,7 @@ class ScanI2C
SCREEN_ST7567,
RTC_RV3028,
RTC_PCF8563,
RTC_PCF85063,
RTC_RX8130CE,
CARDKB,
TDECKKB,

View File

@@ -202,6 +202,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address)
#endif
#ifdef PCF85063_RTC
SCAN_SIMPLE_CASE(PCF85063_RTC, RTC_PCF85063, "PCF85063", (uint8_t)addr.address)
#endif
case CARDKB_ADDR:
// Do we have the RAK14006 instead?
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1);

View File

@@ -66,26 +66,26 @@ RTCSetResult readFromRTC()
currentQuality = RTCQualityDevice;
}
return RTCSetResultSuccess;
} else {
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
}
#elif defined(PCF8563_RTC)
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
#elif defined(PCF85063_RTC)
if (rtc_found.address == PCF85063_RTC) {
#endif
uint32_t now = millis();
PCF8563_Class rtc;
SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
#else
rtc.begin();
rtc.begin(Wire);
#endif
auto tc = rtc.getDateTime();
tm t;
t.tm_year = tc.year - 1900;
t.tm_mon = tc.month - 1;
t.tm_mday = tc.day;
t.tm_hour = tc.hour;
t.tm_min = tc.minute;
t.tm_sec = tc.second;
RTC_DateTime datetime = rtc.getDateTime();
tm t = datetime.toUnixTime();
tv.tv_sec = gm_mktime(&t);
tv.tv_usec = 0;
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
@@ -100,14 +100,16 @@ RTCSetResult readFromRTC()
}
#endif
LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900,
t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
if (currentQuality == RTCQualityNone) {
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
currentQuality = RTCQualityDevice;
}
return RTCSetResultSuccess;
} else {
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
}
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {
@@ -232,20 +234,28 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
} else {
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
}
#elif defined(PCF8563_RTC)
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
PCF8563_Class rtc;
#elif defined(PCF85063_RTC)
if (rtc_found.address == PCF85063_RTC) {
#endif
SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
#else
rtc.begin();
rtc.begin(Wire);
#endif
tm *t = gmtime(&tv->tv_sec);
rtc.setDateTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
LOG_DEBUG("PCF8563_RTC setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
rtc.setDateTime(*t);
LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1,
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
} else {
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
}
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {

View File

@@ -46,6 +46,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif
#include "FSCommon.h"
#include "MeshService.h"
#include "MessageStore.h"
#include "RadioLibInterface.h"
#include "error.h"
#include "gps/GeoCoord.h"
@@ -64,10 +65,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "modules/WaypointModule.h"
#include "sleep.h"
#include "target_specific.h"
using graphics::Emote;
using graphics::emotes;
using graphics::numEmotes;
extern MessageStore messageStore;
#if USE_TFTDISPLAY
extern uint16_t TFT_MESH;
@@ -119,10 +117,6 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100};
// we'll need to hold onto pointers for the modules that can draw a frame.
std::vector<MeshModule *> moduleFrames;
// Global variables for screen function overlay symbols
std::vector<std::string> functionSymbol;
std::string functionSymbolString;
#if HAS_GPS
// GeoCoord object for the screen
GeoCoord geoCoord;
@@ -263,19 +257,11 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
} else {
// otherwise, just display the module frame that's aligned with the current frame
module_frame = state->currentFrame;
// LOG_DEBUG("Screen is not in transition. Frame: %d", module_frame);
}
// LOG_DEBUG("Draw Module Frame %d", module_frame);
MeshModule &pi = *moduleFrames.at(module_frame);
pi.drawFrame(display, state, x, y);
}
// Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet)
{
return packet->from != 0 && !moduleConfig.store_forward.enabled;
}
/**
* Given a recent lat/lon return a guess of the heading the user is walking on.
*
@@ -322,16 +308,16 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
{
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color);
int32_t rawRGB = uiconfig.screen_rgb_color;
if (rawRGB > 0 && rawRGB <= 255255255) {
uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF;
uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF;
uint8_t TFT_MESH_b = rawRGB & 0xFF;
LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) {
TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
// Only validate the combined value once
if (rawRGB > 0 && rawRGB <= 255255255) {
// Extract each component as a normal int first
int r = (rawRGB >> 16) & 0xFF;
int g = (rawRGB >> 8) & 0xFF;
int b = rawRGB & 0xFF;
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
TFT_MESH = COLOR565(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
}
}
@@ -550,10 +536,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
void Screen::setup()
{
// === Enable display rendering ===
// Enable display rendering
useDisplay = true;
// === Load saved brightness from UI config ===
// Load saved brightness from UI config
// For OLED displays (SSD1306), default brightness is 255 if not set
if (uiconfig.screen_brightness == 0) {
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
@@ -565,7 +551,7 @@ void Screen::setup()
brightness = uiconfig.screen_brightness;
}
// === Detect OLED subtype (if supported by board variant) ===
// Detect OLED subtype (if supported by board variant)
#ifdef AutoOLEDWire_h
if (isAUTOOled)
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
@@ -587,7 +573,7 @@ void Screen::setup()
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
#endif
// === Initialize display and UI system ===
// Initialize display and UI system
ui->init();
displayWidth = dispdev->width();
displayHeight = dispdev->height();
@@ -599,7 +585,7 @@ void Screen::setup()
ui->disableAllIndicators(); // Disable page indicator dots
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
// === Apply loaded brightness ===
// Apply loaded brightness
#if defined(ST7789_CS)
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
@@ -607,20 +593,20 @@ void Screen::setup()
#endif
LOG_INFO("Applied screen brightness: %d", brightness);
// === Set custom overlay callbacks ===
// Set custom overlay callbacks
static OverlayCallback overlays[] = {
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
// === Enable UTF-8 to display mapping ===
// Enable UTF-8 to display mapping
dispdev->setFontTableLookupFunction(customFontTableLookup);
#ifdef USERPREFS_OEM_TEXT
logo_timeout *= 2; // Give more time for branded boot logos
#endif
// === Configure alert frames (e.g., "Resuming..." or region name) ===
// Configure alert frames (e.g., "Resuming..." or region name)
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
#ifdef ARCH_ESP32
@@ -636,10 +622,10 @@ void Screen::setup()
ui->setFrames(alertFrames, 1);
ui->disableAutoTransition(); // Require manual navigation between frames
// === Log buffer for on-screen logs (3 lines max) ===
// Log buffer for on-screen logs (3 lines max)
dispdev->setLogBuffer(3, 32);
// === Optional screen mirroring or flipping (e.g. for T-Beam orientation) ===
// Optional screen mirroring or flipping (e.g. for T-Beam orientation)
#ifdef SCREEN_MIRROR
dispdev->mirrorScreen();
#else
@@ -657,7 +643,7 @@ void Screen::setup()
}
#endif
// === Generate device ID from MAC address ===
// Generate device ID from MAC address
uint8_t dmac[6];
getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
@@ -666,9 +652,9 @@ void Screen::setup()
handleSetOn(false); // Ensure proper init for Arduino targets
#endif
// === Turn on display and trigger first draw ===
// Turn on display and trigger first draw
handleSetOn(true);
determineResolution(dispdev->height(), dispdev->width());
graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width());
ui->update();
#ifndef USE_EINK
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
@@ -689,7 +675,7 @@ void Screen::setup()
touchScreenImpl1->init();
#endif
// === Subscribe to device status updates ===
// Subscribe to device status updates
powerStatusObserver.observe(&powerStatus->onNewStatus);
gpsStatusObserver.observe(&gpsStatus->onNewStatus);
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
@@ -697,12 +683,14 @@ void Screen::setup()
#if !MESHTASTIC_EXCLUDE_ADMIN
adminMessageObserver.observe(adminModule);
#endif
if (textMessageModule)
textMessageObserver.observe(textMessageModule);
if (inputBroker)
inputObserver.observe(inputBroker);
// === Notify modules that support UI events ===
// Load persisted messages into RAM
messageStore.loadFromFlash();
LOG_INFO("MessageStore loaded from flash");
// Notify modules that support UI events
MeshModule::observeUIEvents(&uiFrameEventObserver);
}
@@ -773,6 +761,23 @@ int32_t Screen::runOnce()
if (displayHeight == 0) {
displayHeight = dispdev->getHeight();
}
// Detect frame transitions and clear message cache when leaving text message screen
{
static int8_t lastFrameIndex = -1;
int8_t currentFrameIndex = ui->getUiState()->currentFrame;
int8_t textMsgIndex = framesetInfo.positions.textMessage;
if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) {
if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) {
graphics::MessageRenderer::clearMessageCache();
}
}
lastFrameIndex = currentFrameIndex;
}
menuHandler::handleMenuSwitch(dispdev);
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
@@ -828,17 +833,17 @@ int32_t Screen::runOnce()
break;
case Cmd::ON_PRESS:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleOnPress();
showFrame(FrameDirection::NEXT);
}
break;
case Cmd::SHOW_PREV_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleShowPrevFrame();
showFrame(FrameDirection::PREVIOUS);
}
break;
case Cmd::SHOW_NEXT_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleShowNextFrame();
showFrame(FrameDirection::NEXT);
}
break;
case Cmd::START_ALERT_FRAME: {
@@ -859,6 +864,7 @@ int32_t Screen::runOnce()
break;
case Cmd::STOP_ALERT_FRAME:
NotificationRenderer::pauseBanner = false;
break;
case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
@@ -1029,9 +1035,6 @@ void Screen::setFrames(FrameFocus focus)
}
#endif
// Declare this early so its available in FOCUS_PRESERVE block
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
if (!hiddenFrames.home) {
fsi.positions.home = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
@@ -1043,11 +1046,16 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK
if (!hiddenFrames.nodelist) {
fsi.positions.nodelist = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
if (!hiddenFrames.nodelist_nodes) {
fsi.positions.nodelist_nodes = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes;
indicatorIcons.push_back(icon_nodes);
}
if (!hiddenFrames.nodelist_location) {
fsi.positions.nodelist_location = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location;
indicatorIcons.push_back(icon_list);
}
#endif
// Show detailed node views only on E-Ink builds
@@ -1069,11 +1077,13 @@ void Screen::setFrames(FrameFocus focus)
}
#endif
#if HAS_GPS
#ifdef USE_EINK
if (!hiddenFrames.nodelist_bearings) {
fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list);
}
#endif
if (!hiddenFrames.gps) {
fsi.positions.gps = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
@@ -1173,7 +1183,7 @@ void Screen::setFrames(FrameFocus focus)
}
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
this->frameCount = numframes; // Save frame count for use in custom overlay
this->frameCount = numframes; // Save frame count for use in custom overlay
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
ui->setFrames(normalFrames, numframes);
@@ -1193,10 +1203,6 @@ void Screen::setFrames(FrameFocus focus)
case FOCUS_FAULT:
ui->switchToFrame(fsi.positions.fault);
break;
case FOCUS_TEXTMESSAGE:
hasUnreadMessage = false; // ✅ Clear when message is *viewed*
ui->switchToFrame(fsi.positions.textMessage);
break;
case FOCUS_MODULE:
// Whichever frame was marked by MeshModule::requestFocus(), if any
// If no module requested focus, will show the first frame instead
@@ -1239,8 +1245,11 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
void Screen::toggleFrameVisibility(const std::string &frameName)
{
#ifndef USE_EINK
if (frameName == "nodelist") {
hiddenFrames.nodelist = !hiddenFrames.nodelist;
if (frameName == "nodelist_nodes") {
hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes;
}
if (frameName == "nodelist_location") {
hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location;
}
#endif
#ifdef USE_EINK
@@ -1255,9 +1264,11 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
}
#endif
#if HAS_GPS
#ifdef USE_EINK
if (frameName == "nodelist_bearings") {
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
}
#endif
if (frameName == "gps") {
hiddenFrames.gps = !hiddenFrames.gps;
}
@@ -1279,8 +1290,10 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
bool Screen::isFrameHidden(const std::string &frameName) const
{
#ifndef USE_EINK
if (frameName == "nodelist")
return hiddenFrames.nodelist;
if (frameName == "nodelist_nodes")
return hiddenFrames.nodelist_nodes;
if (frameName == "nodelist_location")
return hiddenFrames.nodelist_location;
#endif
#ifdef USE_EINK
if (frameName == "nodelist_lastheard")
@@ -1291,8 +1304,10 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return hiddenFrames.nodelist_distance;
#endif
#if HAS_GPS
#ifdef USE_EINK
if (frameName == "nodelist_bearings")
return hiddenFrames.nodelist_bearings;
#endif
if (frameName == "gps")
return hiddenFrames.gps;
#endif
@@ -1308,37 +1323,6 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return false;
}
// Dismisses the currently displayed screen frame, if possible
// Relevant for text message, waypoint, others in future?
// Triggered with a CardKB keycombo
void Screen::hideCurrentFrame()
{
uint8_t currentFrame = ui->getUiState()->currentFrame;
bool dismissed = false;
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
LOG_INFO("Hide Text Message");
devicestate.has_rx_text_message = false;
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
LOG_DEBUG("Hide Waypoint");
devicestate.has_rx_waypoint = false;
hiddenFrames.waypoint = true;
dismissed = true;
} else if (currentFrame == framesetInfo.positions.wifi) {
LOG_DEBUG("Hide WiFi Screen");
hiddenFrames.wifi = true;
dismissed = true;
} else if (currentFrame == framesetInfo.positions.lora) {
LOG_INFO("Hide LoRa");
hiddenFrames.lora = true;
dismissed = true;
}
if (dismissed) {
setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE
}
}
void Screen::handleStartFirmwareUpdateScreen()
{
LOG_DEBUG("Show firmware screen");
@@ -1391,28 +1375,6 @@ void Screen::decreaseBrightness()
/* TO DO: add little popup in center of screen saying what brightness level it is set to*/
}
void Screen::setFunctionSymbol(std::string sym)
{
if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) {
functionSymbol.push_back(sym);
functionSymbolString = "";
for (auto symbol : functionSymbol) {
functionSymbolString = symbol + " " + functionSymbolString;
}
setFastFramerate();
}
}
void Screen::removeFunctionSymbol(std::string sym)
{
functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end());
functionSymbolString = "";
for (auto symbol : functionSymbol) {
functionSymbolString = symbol + " " + functionSymbolString;
}
setFastFramerate();
}
void Screen::handleOnPress()
{
// If screen was off, just wake it, otherwise advance to next frame
@@ -1424,23 +1386,17 @@ void Screen::handleOnPress()
}
}
void Screen::handleShowPrevFrame()
void Screen::showFrame(FrameDirection direction)
{
// If screen was off, just wake it, otherwise go back to previous frame
// If we are in a transition, the press must have bounced, drop it.
// Only advance frames when UI is stable
if (ui->getUiState()->frameState == FIXED) {
ui->previousFrame();
lastScreenTransition = millis();
setFastFramerate();
}
}
void Screen::handleShowNextFrame()
{
// If screen was off, just wake it, otherwise advance to next frame
// If we are in a transition, the press must have bounced, drop it.
if (ui->getUiState()->frameState == FIXED) {
ui->nextFrame();
if (direction == FrameDirection::NEXT) {
ui->nextFrame();
} else {
ui->previousFrame();
}
lastScreenTransition = millis();
setFastFramerate();
}
@@ -1466,7 +1422,6 @@ void Screen::setFastFramerate()
int Screen::handleStatusUpdate(const meshtastic::Status *arg)
{
// LOG_DEBUG("Screen got status update %d", arg->getStatusType());
switch (arg->getStatusType()) {
case STATUS_TYPE_NODE:
if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) {
@@ -1584,11 +1539,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
screen->showSimpleBanner(banner, 3000);
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
if (longName && longName[0]) {
#if defined(M5STACK_UNITC6L)
strcpy(banner, "New Message");
#else
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
#endif
if (currentResolution == ScreenResolution::UltraLow) {
strcpy(banner, "New Message");
} else {
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
}
} else {
strcpy(banner, "New Message");
}
@@ -1624,16 +1579,26 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event)
if (showingNormalScreen) {
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) {
setFrames(FOCUS_MODULE);
}
// Regenerate the frameset, while Attempt to maintain focus on the current frame
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND)
// Regenerate the frameset, while attempting to maintain focus on the current frame
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) {
setFrames(FOCUS_PRESERVE);
}
// Don't regenerate the frameset, just re-draw whatever is on screen ASAP
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY)
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) {
setFastFramerate();
}
// Jump directly to the Text Message screen
else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) {
setFrames(FOCUS_PRESERVE); // preserve current frame ordering
ui->switchToFrame(framesetInfo.positions.textMessage);
setFastFramerate(); // force redraw ASAP
}
}
return 0;
@@ -1671,7 +1636,48 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::handleMenuSwitch(dispdev);
return 0;
}
// UP/DOWN in message screen scrolls through message threads
if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
if (event->inputEvent == INPUT_BROKER_UP) {
if (messageStore.getMessages().empty()) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else {
graphics::MessageRenderer::scrollUp();
setFastFramerate(); // match existing behavior
return 0;
}
}
if (event->inputEvent == INPUT_BROKER_DOWN) {
if (messageStore.getMessages().empty()) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else {
graphics::MessageRenderer::scrollDown();
setFastFramerate();
return 0;
}
}
}
// UP/DOWN in node list screens scrolls through node pages
if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) {
if (event->inputEvent == INPUT_BROKER_UP) {
graphics::NodeListRenderer::scrollUp();
setFastFramerate();
return 0;
}
if (event->inputEvent == INPUT_BROKER_DOWN) {
graphics::NodeListRenderer::scrollDown();
setFastFramerate();
return 0;
}
}
// Use left or right input from a keyboard to move between frames,
// so long as a mesh module isn't using these events for some other purpose
if (showingNormalScreen) {
@@ -1685,16 +1691,39 @@ int Screen::handleInputEvent(const InputEvent *event)
// If no modules are using the input, move between frames
if (!inputIntercepted) {
#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2
bool handledEncoderScroll = false;
const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 &&
this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
!messageStore.getMessages().empty());
if (isTextMessageFrame) {
if (event->inputEvent == INPUT_BROKER_UP_LONG) {
graphics::MessageRenderer::nudgeScroll(-1);
handledEncoderScroll = true;
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
graphics::MessageRenderer::nudgeScroll(1);
handledEncoderScroll = true;
}
}
if (handledEncoderScroll) {
setFastFramerate();
return 0;
}
#endif
if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) {
showPrevFrame();
showFrame(FrameDirection::PREVIOUS);
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
showNextFrame();
showFrame(FrameDirection::NEXT);
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
// Long press up button for fast frame switching
showPrevFrame();
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
// Long press down button for fast frame switching
showNextFrame();
} else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) &&
this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
menuHandler::homeBaseMenu();
@@ -1709,20 +1738,21 @@ int Screen::handleInputEvent(const InputEvent *event)
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
menuHandler::loraMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
if (devicestate.rx_text_message.from) {
if (!messageStore.getMessages().empty()) {
menuHandler::messageResponseMenu();
} else {
#if defined(M5STACK_UNITC6L)
menuHandler::textMessageMenu();
#else
menuHandler::textMessageBaseMenu();
#endif
if (currentResolution == ScreenResolution::UltraLow) {
menuHandler::textMessageMenu();
} else {
menuHandler::textMessageBaseMenu();
}
}
} else if (framesetInfo.positions.firstFavorite != 255 &&
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
menuHandler::favoriteBaseMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
@@ -1733,7 +1763,7 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::wifiBaseMenu();
}
} else if (event->inputEvent == INPUT_BROKER_BACK) {
showPrevFrame();
showFrame(FrameDirection::PREVIOUS);
} else if (event->inputEvent == INPUT_BROKER_CANCEL) {
setOn(false);
}

View File

@@ -40,7 +40,6 @@ class Screen
FOCUS_DEFAULT, // No specific frame
FOCUS_PRESERVE, // Return to the previous frame
FOCUS_FAULT,
FOCUS_TEXTMESSAGE,
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
FOCUS_CLOCK,
FOCUS_SYSTEM,
@@ -55,8 +54,6 @@ class Screen
void startFirmwareUpdateScreen() {}
void increaseBrightness() {}
void decreaseBrightness() {}
void setFunctionSymbol(std::string) {}
void removeFunctionSymbol(std::string) {}
void startAlert(const char *) {}
void showSimpleBanner(const char *message, uint32_t durationMs = 0) {}
void showOverlayBanner(BannerOverlayOptions) {}
@@ -172,6 +169,8 @@ class Point
namespace graphics
{
enum class FrameDirection { NEXT, PREVIOUS };
// Forward declarations
class Screen;
@@ -211,8 +210,6 @@ class Screen : public concurrency::OSThread
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
CallbackObserver<Screen, const meshtastic::Status *> nodeStatusObserver =
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
CallbackObserver<Screen, const meshtastic_MeshPacket *> textMessageObserver =
CallbackObserver<Screen, const meshtastic_MeshPacket *>(this, &Screen::handleTextMessage);
CallbackObserver<Screen, const UIFrameEvent *> uiFrameEventObserver =
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
CallbackObserver<Screen, const InputEvent *> inputObserver =
@@ -223,6 +220,10 @@ class Screen : public concurrency::OSThread
public:
OLEDDisplay *getDisplayDevice() { return dispdev; }
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
// Screen dimension accessors
inline int getHeight() const { return displayHeight; }
inline int getWidth() const { return displayWidth; }
size_t frameCount = 0; // Total number of active frames
~Screen();
@@ -231,7 +232,6 @@ class Screen : public concurrency::OSThread
FOCUS_DEFAULT, // No specific frame
FOCUS_PRESERVE, // Return to the previous frame
FOCUS_FAULT,
FOCUS_TEXTMESSAGE,
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
FOCUS_CLOCK,
FOCUS_SYSTEM,
@@ -279,6 +279,7 @@ class Screen : public concurrency::OSThread
void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); }
void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); }
void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); }
void showFrame(FrameDirection direction);
// generic alert start
void startAlert(FrameCallback _alertFrame)
@@ -346,9 +347,6 @@ class Screen : public concurrency::OSThread
void increaseBrightness();
void decreaseBrightness();
void setFunctionSymbol(std::string sym);
void removeFunctionSymbol(std::string sym);
/// Stops showing the boot screen.
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
@@ -579,7 +577,7 @@ class Screen : public concurrency::OSThread
// Handle observer events
int handleStatusUpdate(const meshtastic::Status *arg);
int handleTextMessage(const meshtastic_MeshPacket *arg);
int handleTextMessage(const meshtastic_MeshPacket *packet);
int handleUIFrameEvent(const UIFrameEvent *arg);
int handleInputEvent(const InputEvent *arg);
int handleAdminMessage(AdminModule_ObserverData *arg);
@@ -590,9 +588,6 @@ class Screen : public concurrency::OSThread
/// Draws our SSL cert screen during boot (called from WebServer)
void setSSLFrames();
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
void hideCurrentFrame();
// Menu-driven Show / Hide Toggle
void toggleFrameVisibility(const std::string &frameName);
bool isFrameHidden(const std::string &frameName) const;
@@ -640,8 +635,6 @@ class Screen : public concurrency::OSThread
// Implementations of various commands, called from doTask().
void handleSetOn(bool on, FrameCallback einkScreensaver = NULL);
void handleOnPress();
void handleShowNextFrame();
void handleShowPrevFrame();
void handleStartFirmwareUpdateScreen();
// Info collected by setFrames method.
@@ -661,7 +654,8 @@ class Screen : public concurrency::OSThread
uint8_t gps = 255;
uint8_t home = 255;
uint8_t textMessage = 255;
uint8_t nodelist = 255;
uint8_t nodelist_nodes = 255;
uint8_t nodelist_location = 255;
uint8_t nodelist_lastheard = 255;
uint8_t nodelist_hopsignal = 255;
uint8_t nodelist_distance = 255;
@@ -684,7 +678,8 @@ class Screen : public concurrency::OSThread
bool home = false;
bool clock = false;
#ifndef USE_EINK
bool nodelist = false;
bool nodelist_nodes = false;
bool nodelist_location = false;
#endif
#ifdef USE_EINK
bool nodelist_lastheard = false;
@@ -692,7 +687,9 @@ class Screen : public concurrency::OSThread
bool nodelist_distance = false;
#endif
#if HAS_GPS
#ifdef USE_EINK
bool nodelist_bearings = false;
#endif
bool gps = false;
#endif
bool lora = false;

View File

@@ -15,27 +15,49 @@
namespace graphics
{
void determineResolution(int16_t screenheight, int16_t screenwidth)
ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth)
{
#ifdef FORCE_LOW_RES
isHighResolution = false;
return;
#endif
if (screenwidth > 128) {
isHighResolution = true;
return ScreenResolution::Low;
#else
// Unit C6L and other ultra low res screens
if (screenwidth <= 64 || screenheight <= 48) {
return ScreenResolution::UltraLow;
}
// Standard OLED screens
if (screenwidth > 128 && screenheight <= 64) {
isHighResolution = false;
return ScreenResolution::Low;
}
// High Resolutions screens like T114, TDeck, TLora Pager, etc
if (screenwidth > 128) {
return ScreenResolution::High;
}
// Default to low resolution
return ScreenResolution::Low;
#endif
}
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second)
{
hour = 0;
minute = 0;
second = 0;
if (rtc_sec == 0)
return;
uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
hour = hms / SEC_PER_HOUR;
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
second = hms % SEC_PER_MIN;
}
// === Shared External State ===
bool hasUnreadMessage = false;
bool isMuted = false;
bool isHighResolution = false;
ScreenResolution currentResolution = ScreenResolution::Low;
// === Internal State ===
bool isBoltVisibleShared = true;
@@ -91,7 +113,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
display->setColor(BLACK);
display->fillRect(0, 0, screenW, highlightHeight + 2);
display->setColor(WHITE);
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
display->drawLine(0, 20, screenW, 20);
} else {
display->drawLine(0, 14, screenW, 14);
@@ -129,7 +151,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
}
#endif
bool useHorizontalBattery = (isHighResolution && screenW >= screenH);
bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH);
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
int batteryX = 1;
@@ -139,7 +161,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
batteryX += 1;
batteryY += 2;
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution);
batteryX += 20; // Icon + 1 pixel
} else {
@@ -200,8 +222,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
if (rtc_sec > 0) {
// === Build Time String ===
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
int hour = hms / SEC_PER_HOUR;
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int hour, minute, second;
graphics::decomposeTime(rtc_sec, hour, minute, second);
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
// === Build Date String ===
@@ -209,7 +231,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false);
char dateLine[40];
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr);
} else {
if (hasUnreadMessage) {
@@ -285,7 +307,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
}
} else if (isMuted) {
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
@@ -362,7 +384,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
}
} else if (isMuted) {
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
@@ -381,7 +403,7 @@ const int *getTextPositions(OLEDDisplay *display)
{
static int textPositions[7]; // Static array that persists beyond function scope
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
textPositions[0] = textZeroLine;
textPositions[1] = textFirstLine_medium;
textPositions[2] = textSecondLine_medium;
@@ -414,8 +436,12 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
}
if (drawConnectionState) {
if (isHighResolution) {
const int scale = 2;
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
display->setColor(BLACK);
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
(connection_icon_height * scale) + (2 * scale));
display->setColor(WHITE);
if (currentResolution == ScreenResolution::High) {
const int bytesPerRow = (connection_icon_width + 7) / 8;
int iconX = 0;
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);

View File

@@ -42,8 +42,11 @@ namespace graphics
// Shared state (declare inside namespace)
extern bool hasUnreadMessage;
extern bool isMuted;
extern bool isHighResolution;
void determineResolution(int16_t screenheight, int16_t screenwidth);
enum class ScreenResolution : uint8_t { UltraLow = 0, Low = 1, High = 2 };
extern ScreenResolution currentResolution;
ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth);
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second);
// Rounded highlight (used for inverted headers)
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);

View File

@@ -354,8 +354,6 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16
if (screenHeight <= 64) {
textY = boxY + (boxHeight - inputLineH) / 2;
} else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;

View File

@@ -1,15 +1,10 @@
#include "configuration.h"
#if HAS_SCREEN
#include "ClockRenderer.h"
#include "NodeDB.h"
#include "UIRenderer.h"
#include "configuration.h"
#include "gps/GeoCoord.h"
#include "gps/RTC.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h"
#include "graphics/emotes.h"
#include "graphics/images.h"
#include "main.h"
@@ -23,6 +18,31 @@ namespace graphics
namespace ClockRenderer
{
// Segment bitmaps for numerals 0-9 stored in flash to save RAM.
// Each row is a digit, each column is a segment state (1 = on, 0 = off).
// Segment layout reference:
//
// ___1___
// 6 | | 2
// |_7___|
// 5 | | 3
// |___4_|
//
// Segment order: [1, 2, 3, 4, 5, 6, 7]
//
static const uint8_t PROGMEM digitSegments[10][7] = {
{1, 1, 1, 1, 1, 1, 0}, // 0
{0, 1, 1, 0, 0, 0, 0}, // 1
{1, 1, 0, 1, 1, 0, 1}, // 2
{1, 1, 1, 1, 0, 0, 1}, // 3
{0, 1, 1, 0, 0, 1, 1}, // 4
{1, 0, 1, 1, 0, 1, 1}, // 5
{1, 0, 1, 1, 1, 1, 1}, // 6
{1, 1, 1, 0, 0, 1, 0}, // 7
{1, 1, 1, 1, 1, 1, 1}, // 8
{1, 1, 1, 1, 0, 1, 1} // 9
};
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
{
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
@@ -30,7 +50,7 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8;
uint16_t topAndBottomX = x + (4 * scale);
uint16_t topAndBottomX = x + static_cast<uint16_t>(4 * scale);
uint16_t quarterCellHeight = cellHeight / 4;
@@ -43,34 +63,16 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale)
{
// the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of
// segment {innerIndex + 1}
// e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off.
uint8_t numbers[10][7] = {
{1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key
{0, 1, 1, 0, 0, 0, 0}, // 1 1
{1, 1, 0, 1, 1, 0, 1}, // 2 ___
{1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2
{0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_|
{1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3
{1, 0, 1, 1, 1, 1, 1}, // 6 |___|
{1, 1, 1, 0, 0, 1, 0}, // 7
{1, 1, 1, 1, 1, 1, 1}, // 8 4
{1, 1, 1, 1, 0, 1, 1}, // 9
};
// the width and height of each segment's central rectangle:
// _____________________
// ⋰| (only this part, |⋱
// ⋰ | not including | ⋱
// ⋱ | the triangles | ⋰
// ⋱| on the ends) |⋰
// ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Read 7-segment pattern for the digit from flash
uint8_t seg[7];
for (uint8_t i = 0; i < 7; i++) {
seg[i] = pgm_read_byte(&digitSegments[number][i]);
}
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
// segment x and y coordinates
// Precompute segment positions
uint16_t segmentOneX = x + segmentHeight + 2;
uint16_t segmentOneY = y;
@@ -92,33 +94,21 @@ void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t n
uint16_t segmentSevenX = segmentOneX;
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
if (numbers[number][0]) {
graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
}
if (numbers[number][1]) {
graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
}
if (numbers[number][2]) {
graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
}
if (numbers[number][3]) {
graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
}
if (numbers[number][4]) {
graphics::ClockRenderer::drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
}
if (numbers[number][5]) {
graphics::ClockRenderer::drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
}
if (numbers[number][6]) {
graphics::ClockRenderer::drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
}
// Draw only the active segments
if (seg[0])
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
if (seg[1])
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
if (seg[2])
drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
if (seg[3])
drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
if (seg[4])
drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
if (seg[5])
drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
if (seg[6])
drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
}
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height)
@@ -147,42 +137,6 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
}
/*
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
{
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
if (digitalMode) {
uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2;
uint16_t centerX = (x + segmentHeight + 2) + (radius / 2);
uint16_t centerY = (y + segmentHeight + 2) + (radius / 2);
display->drawCircle(centerX, centerY, radius);
display->drawCircle(centerX, centerY, radius + 1);
display->drawLine(centerX, centerY, centerX, centerY - radius + 3);
display->drawLine(centerX, centerY, centerX + radius - 3, centerY);
} else {
uint16_t segmentOneX = x + segmentHeight + 2;
uint16_t segmentOneY = y;
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
uint16_t segmentThreeX = segmentOneX;
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2;
uint16_t segmentFourX = x;
uint16_t segmentFourY = y + segmentHeight + 2;
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
}
}
*/
// Draw a digital clock
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear();
@@ -192,7 +146,6 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
const char *titleStr = "";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
char timeString[16];
@@ -237,7 +190,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
float target_width = display->getWidth() * screenwidth_target_ratio;
float target_height =
display->getHeight() -
(isHighResolution
((currentResolution == ScreenResolution::High)
? 46
: 33); // Be careful adjusting this number, we have to account for header and the text under the time
@@ -268,10 +221,9 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
scaleInitialized = true;
}
size_t len = strlen(timeString);
// calculate hours:minutes string width
uint16_t timeStringWidth = len * 5; // base spacing between characters
size_t len = strlen(timeString);
uint16_t timeStringWidth = len * 5;
for (size_t i = 0; i < len; i++) {
char character = timeString[i];
@@ -310,9 +262,16 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
// draw seconds string + AM/PM
display->setFont(FONT_SMALL);
int xOffset = (isHighResolution) ? 0 : -1;
int xOffset = -1;
if (currentResolution == ScreenResolution::High) {
xOffset = 0;
}
if (hour >= 10) {
xOffset += (isHighResolution) ? 32 : 18;
if (currentResolution == ScreenResolution::High) {
xOffset += 32;
} else {
xOffset += 18;
}
}
if (config.display.use_12h_clock) {
@@ -320,7 +279,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
}
#ifndef USE_EINK
xOffset = (isHighResolution) ? 18 : 10;
xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10;
if (scale >= 2.0f) {
xOffset -= (int)(4.5f * scale);
}
@@ -339,19 +298,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const char *titleStr = "";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
// clock face center coordinates
int16_t centerX = display->getWidth() / 2;
int16_t centerY = display->getHeight() / 2;
// clock face radius
int16_t radius = 0;
if (display->getHeight() < display->getWidth()) {
radius = (display->getHeight() / 2) * 0.9;
} else {
radius = (display->getWidth() / 2) * 0.9;
}
int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9;
#ifdef T_WATCH_S3
radius = (display->getWidth() / 2) * 0.8;
#endif
@@ -366,17 +319,8 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// tick mark outer y coordinate; (first nested circle)
int16_t tickMarkOuterNoonY = secondHandNoonY;
// seconds tick mark inner y coordinate; (second nested circle)
double secondsTickMarkInnerNoonY = (double)noonY + 4;
if (isHighResolution) {
secondsTickMarkInnerNoonY = (double)noonY + 8;
}
// hours tick mark inner y coordinate; (third nested circle)
double hoursTickMarkInnerNoonY = (double)noonY + 6;
if (isHighResolution) {
hoursTickMarkInnerNoonY = (double)noonY + 16;
}
double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4);
double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6);
// minute hand y coordinate
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
@@ -386,7 +330,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// hour hand radius and y coordinate
int16_t hourHandRadius = radius * 0.35;
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
hourHandRadius = radius * 0.55;
}
int16_t hourHandNoonY = centerY - hourHandRadius;
@@ -396,19 +340,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
if (rtc_sec > 0) {
long hms = rtc_sec % SEC_PER_DAY;
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
int hour, minute, second;
decomposeTime(rtc_sec, hour, minute, second);
// Tear apart hms into h:m:s
int hour = hms / SEC_PER_HOUR;
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
bool isPM = hour >= 12;
if (config.display.use_12h_clock) {
isPM = hour >= 12;
bool isPM = hour >= 12;
display->setFont(FONT_SMALL);
int yOffset = isHighResolution ? 1 : 0;
int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0;
#ifdef USE_EINK
yOffset += 3;
#endif
@@ -499,12 +437,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
#else
#ifdef USE_EINK
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
// draw hour number
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
}
#else
if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
if (currentResolution == ScreenResolution::High &&
(hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
// draw hour number
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
}
@@ -516,7 +455,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
// draw minute tick mark
display->drawLine(startX, startY, endX, endY);
}

View File

@@ -48,7 +48,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
// This could draw a "N" indicator or north arrow
// For now, we'll draw a simple north indicator
// const float radius = 17.0f;
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
radius += 4;
}
Point north(0, -radius);
@@ -59,7 +59,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setColor(BLACK);
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
} else {
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);

View File

@@ -282,13 +282,13 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds);
// Line 1 (Still)
#if !defined(M5STACK_UNITC6L)
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
if (config.display.heading_bold)
display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
if (currentResolution != graphics::ScreenResolution::UltraLow) {
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
if (config.display.heading_bold)
display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
display->setColor(WHITE);
#endif
display->setColor(WHITE);
}
// Setup string to assemble analogClock string
std::string analogClock = "";
@@ -301,9 +301,8 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
// Tear apart hms into h:m:s
int hour = hms / SEC_PER_HOUR;
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
int hour, min, sec;
graphics::decomposeTime(rtc_sec, hour, min, sec);
char timebuf[12];
@@ -379,7 +378,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int line = 1;
// === Set Title
const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa";
const char *titleStr = (currentResolution == ScreenResolution::High) ? "LoRa Info" : "LoRa";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
@@ -391,11 +390,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
char shortnameble[35];
getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
#if defined(M5STACK_UNITC6L)
snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId);
#else
snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
#endif
if (currentResolution == ScreenResolution::UltraLow) {
snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId);
} else {
snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
}
int textWidth = display->getStringWidth(shortnameble);
int nameX = (SCREEN_WIDTH - textWidth);
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
@@ -414,11 +413,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
char regionradiopreset[25];
const char *region = myRegion ? myRegion->name : NULL;
if (region != nullptr) {
#if defined(M5STACK_UNITC6L)
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
#else
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
#endif
if (currentResolution == ScreenResolution::UltraLow) {
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
} else {
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
}
}
textWidth = display->getStringWidth(regionradiopreset);
nameX = (SCREEN_WIDTH - textWidth) / 2;
@@ -430,17 +429,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
float freq = RadioLibInterface::instance->getFreq();
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
if (config.lora.channel_num == 0) {
#if defined(M5STACK_UNITC6L)
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr);
#else
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
#endif
if (currentResolution == ScreenResolution::UltraLow) {
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr);
} else {
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
}
} else {
#if defined(M5STACK_UNITC6L)
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
#else
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
#endif
if (currentResolution == ScreenResolution::UltraLow) {
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
} else {
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
}
}
size_t len = strlen(frequencyslot);
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
@@ -456,12 +455,13 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10
: display->getStringWidth(chUtil) + 5;
int chUtil_y = getTextPositions(display)[line] + 3;
int chutil_bar_width = (isHighResolution) ? 100 : 50;
int chutil_bar_height = (isHighResolution) ? 12 : 7;
int extraoffset = (isHighResolution) ? 6 : 3;
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50;
int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7;
int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3;
int chutil_percent = airTime->channelUtilizationPercent();
int centerofscreen = SCREEN_WIDTH / 2;
@@ -530,17 +530,18 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
int line = 1;
const int barHeight = 6;
const int labelX = x;
int barsOffset = (isHighResolution) ? 24 : 0;
int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0;
#ifdef USE_EINK
#ifndef T_DECK_PRO
barsOffset -= 12;
#endif
#endif
#if defined(M5STACK_UNITC6L)
const int barX = x + 45 + barsOffset;
#else
const int barX = x + 40 + barsOffset;
#endif
int barX = x + barsOffset;
if (currentResolution == ScreenResolution::UltraLow) {
barX += 45;
} else {
barX += 40;
}
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
if (total == 0)
return;
@@ -548,7 +549,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
int percent = (used * 100) / total;
char combinedStr[24];
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
total / 1024);
} else {
@@ -628,25 +629,33 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
line += 1;
char appversionstr[35];
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION));
char appversionstr_formatted[40];
char *lastDot = strrchr(appversionstr, '.');
#if defined(M5STACK_UNITC6L)
if (lastDot != nullptr) {
*lastDot = '\0'; // truncate string
const char *ver = optstr(APP_VERSION);
char verbuf[32];
strncpy(verbuf, ver, sizeof(verbuf) - 1);
verbuf[sizeof(verbuf) - 1] = '\0';
char *lastDot = strrchr(verbuf, '.');
if (currentResolution == ScreenResolution::UltraLow) {
if (lastDot != nullptr) {
*lastDot = '\0';
}
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf);
} else {
if (lastDot) {
size_t prefixLen = (size_t)(lastDot - verbuf);
snprintf(appversionstr_formatted, sizeof(appversionstr_formatted), "Ver: %.*s", (int)prefixLen, verbuf);
strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1);
appversionstr[sizeof(appversionstr) - 1] = '\0';
} else {
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf);
}
}
#else
if (lastDot) {
size_t prefixLen = lastDot - appversionstr;
strncpy(appversionstr_formatted, appversionstr, prefixLen);
appversionstr_formatted[prefixLen] = '\0';
strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1);
appversionstr[sizeof(appversionstr) - 1] = '\0';
}
#endif
int textWidth = display->getStringWidth(appversionstr);
int nameX = (SCREEN_WIDTH - textWidth) / 2;
@@ -665,7 +674,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
const char *clientWord = nullptr;
// Determine if narrow or wide screen
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
clientWord = "Client";
} else {
clientWord = "App";
@@ -706,11 +715,23 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
int textX_offset = 10;
if (isHighResolution) {
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
if (currentResolution == ScreenResolution::High) {
textX_offset = textX_offset * 4;
display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
const int scale = 2;
const int bytesPerRow = (chirpy_width + 7) / 8;
for (int yy = 0; yy < chirpy_height; ++yy) {
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
const uint8_t *rowPtr = chirpy + yy * bytesPerRow;
for (int xx = 0; xx < chirpy_width; ++xx) {
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
if (byteVal & bitMask) {
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
}
}
}
} else {
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
}

View File

@@ -11,7 +11,6 @@
#include "graphics/draw/CompassRenderer.h"
#include "graphics/draw/DebugRenderer.h"
#include "graphics/draw/NodeListRenderer.h"
#include "graphics/draw/ScreenRenderer.h"
#include "graphics/draw/UIRenderer.h"
namespace graphics
@@ -30,8 +29,6 @@ using namespace ClockRenderer;
using namespace CompassRenderer;
using namespace DebugRenderer;
using namespace NodeListRenderer;
using namespace ScreenRenderer;
using namespace UIRenderer;
} // namespace DrawRenderers

View File

@@ -1,14 +1,17 @@
#include "configuration.h"
#if HAS_SCREEN
#include "ClockRenderer.h"
#include "Default.h"
#include "GPS.h"
#include "MenuHandler.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "buzz.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
@@ -134,11 +137,10 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
"NP_865",
"BR_902"};
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "LoRa Region";
#else
bannerOptions.message = "Set the LoRa region";
#endif
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "LoRa Region";
}
bannerOptions.durationMs = duration;
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 27;
@@ -426,60 +428,415 @@ void menuHandler::clockMenu()
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::messageResponseMenu()
{
enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 };
#if defined(M5STACK_UNITC6L)
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"};
#else
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"};
#endif
static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset};
int options = 3;
enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, Aloud, enumEnd };
if (kb_found) {
optionsArray[options] = "Reply via Freetext";
optionsEnumArray[options++] = Freetext;
}
static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd];
int options = 0;
auto mode = graphics::MessageRenderer::getThreadMode();
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
// New Reply submenu (replaces Preset and Freetext directly in this menu)
optionsArray[options] = "Reply";
optionsEnumArray[options++] = ReplyMenu;
optionsArray[options] = "View Chats";
optionsEnumArray[options++] = ViewMode;
// Delete submenu
optionsArray[options] = "Delete";
optionsEnumArray[options++] = 900;
#ifdef HAS_I2S
optionsArray[options] = "Read Aloud";
optionsEnumArray[options++] = Aloud;
#endif
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Message";
#else
bannerOptions.message = "Message Action";
#endif
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Message";
} else {
bannerOptions.message = "Message Action";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Dismiss) {
screen->hideCurrentFrame();
} else if (selected == Preset) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
LOG_DEBUG("messageResponseMenu: selected %d", selected);
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
if (selected == ViewMode) {
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
screen->runNow();
// Reply submenu
} else if (selected == ReplyMenu) {
menuHandler::menuQueue = menuHandler::reply_menu;
screen->runNow();
// Delete submenu
} else if (selected == 900) {
menuHandler::menuQueue = menuHandler::delete_messages_menu;
screen->runNow();
// Delete oldest FIRST (only change)
} else if (selected == DeleteOldest) {
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
// Global oldest
messageStore.deleteOldestMessage();
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
// Oldest in current channel
messageStore.deleteOldestMessageInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
// Oldest in current DM
messageStore.deleteOldestMessageWithPeer(peer);
}
} else if (selected == Freetext) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
}
}
// Delete all messages
} else if (selected == DeleteAll) {
messageStore.clearAllMessages();
graphics::MessageRenderer::clearThreadRegistries();
graphics::MessageRenderer::clearMessageCache();
#ifdef HAS_I2S
else if (selected == Aloud) {
} else if (selected == Aloud) {
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
audioThread->readAloud(msg);
}
#endif
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::replyMenu()
{
enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd };
static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd];
int options = 0;
// Back
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
// Preset reply
optionsArray[options] = "With Preset";
optionsEnumArray[options++] = ReplyPreset;
// Freetext reply (only when keyboard exists)
if (kb_found) {
optionsArray[options] = "With Freetext";
optionsEnumArray[options++] = ReplyFreetext;
}
BannerOverlayOptions bannerOptions;
// Dynamic title based on thread mode
auto mode = graphics::MessageRenderer::getThreadMode();
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
bannerOptions.message = "Reply to Channel";
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
bannerOptions.message = "Reply to DM";
} else {
// View All
bannerOptions.message = "Reply to Last Msg";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
bannerOptions.InitialSelected = 1;
bannerOptions.bannerCallback = [](int selected) -> void {
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow();
return;
}
// Preset reply
if (selected == ReplyPreset) {
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
cannedMessageModule->LaunchWithDestination(peer);
} else {
// Fallback for last received message
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
}
}
return;
}
// Freetext reply
if (selected == ReplyFreetext) {
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
cannedMessageModule->LaunchFreetextWithDestination(peer);
} else {
// Fallback for last received message
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
}
}
return;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::deleteMessagesMenu()
{
enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd };
static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd];
int options = 0;
auto mode = graphics::MessageRenderer::getThreadMode();
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
optionsArray[options] = "Delete Oldest";
optionsEnumArray[options++] = DeleteOldest;
// If viewing ALL chats → hide “Delete This Chat”
if (mode != graphics::MessageRenderer::ThreadMode::ALL) {
optionsArray[options] = "Delete This Chat";
optionsEnumArray[options++] = DeleteThis;
}
if (currentResolution == ScreenResolution::UltraLow) {
optionsArray[options] = "Delete All";
} else {
optionsArray[options] = "Delete All Chats";
}
optionsEnumArray[options++] = DeleteAll;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Delete Messages";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [mode](int selected) -> void {
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow();
return;
}
if (selected == DeleteAll) {
LOG_INFO("Deleting all messages");
messageStore.clearAllMessages();
graphics::MessageRenderer::clearThreadRegistries();
graphics::MessageRenderer::clearMessageCache();
return;
}
if (selected == DeleteOldest) {
LOG_INFO("Deleting oldest message");
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
messageStore.deleteOldestMessage();
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
messageStore.deleteOldestMessageInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteOldestMessageWithPeer(peer);
}
return;
}
// This only appears in non-ALL modes
if (selected == DeleteThis) {
LOG_INFO("Deleting all messages in this thread");
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
messageStore.deleteAllMessagesInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteAllMessagesWithPeer(peer);
}
return;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::messageViewModeMenu()
{
auto encodeChannelId = [](int ch) -> int { return 100 + ch; };
auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; };
static std::vector<std::string> labels;
static std::vector<int> ids;
static std::vector<uint32_t> idToPeer; // DM lookup
labels.clear();
ids.clear();
idToPeer.clear();
labels.push_back("Back");
ids.push_back(-1);
labels.push_back("View All Chats");
ids.push_back(-2);
// Channels with messages
for (int ch = 0; ch < 8; ++ch) {
auto msgs = messageStore.getChannelMessages((uint8_t)ch);
if (!msgs.empty()) {
char buf[40];
const char *cname = channels.getName(ch);
snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
labels.push_back(buf);
ids.push_back(encodeChannelId(ch));
LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch));
}
}
// Registry channels
for (int ch : graphics::MessageRenderer::getSeenChannels()) {
if (ch < 0 || ch >= 8)
continue;
auto msgs = messageStore.getChannelMessages((uint8_t)ch);
if (msgs.empty())
continue;
int enc = encodeChannelId(ch);
if (std::find(ids.begin(), ids.end(), enc) == ids.end()) {
char buf[40];
const char *cname = channels.getName(ch);
snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
labels.push_back(buf);
ids.push_back(enc);
LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc);
}
}
// Gather unique peers
auto dms = messageStore.getDirectMessages();
std::vector<uint32_t> uniquePeers;
for (auto &m : dms) {
uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
uniquePeers.push_back(peer);
}
for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) {
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
uniquePeers.push_back(peer);
}
std::sort(uniquePeers.begin(), uniquePeers.end());
// Encode peers
for (size_t i = 0; i < uniquePeers.size(); ++i) {
uint32_t peer = uniquePeers[i];
auto node = nodeDB->getMeshNode(peer);
std::string name;
if (node && node->has_user)
name = sanitizeString(node->user.long_name).substr(0, 15);
else {
char buf[20];
snprintf(buf, sizeof(buf), "Node %08X", peer);
name = buf;
}
labels.push_back("@" + name);
int encPeer = 1000 + (int)idToPeer.size();
ids.push_back(encPeer);
idToPeer.push_back(peer);
LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer);
}
// Active ID
int activeId = -2;
auto mode = graphics::MessageRenderer::getThreadMode();
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL)
activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel());
else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
uint32_t cur = graphics::MessageRenderer::getThreadPeer();
for (size_t i = 0; i < idToPeer.size(); ++i)
if (idToPeer[i] == cur) {
activeId = 1000 + (int)i;
break;
}
}
LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId);
// Build banner
static std::vector<const char *> options;
static std::vector<int> optionIds;
options.clear();
optionIds.clear();
int initialIndex = 0;
for (size_t i = 0; i < labels.size(); i++) {
options.push_back(labels[i].c_str());
optionIds.push_back(ids[i]);
if (ids[i] == activeId)
initialIndex = (int)i;
}
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Select Conversation";
bannerOptions.optionsArrayPtr = options.data();
bannerOptions.optionsEnumPtr = optionIds.data();
bannerOptions.optionsCount = options.size();
bannerOptions.InitialSelected = initialIndex;
bannerOptions.bannerCallback = [=](int selected) -> void {
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
if (selected == -1) {
menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow();
} else if (selected == -2) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
} else if (isChannelSel(selected)) {
int ch = selected - 100;
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch);
} else if (selected >= 1000) {
int idx = selected - 1000;
if (idx >= 0 && (size_t)idx < idToPeer.size()) {
uint32_t peer = idToPeer[idx];
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer);
}
}
};
screen->showOverlayBanner(bannerOptions);
}
@@ -505,23 +862,12 @@ void menuHandler::homeBaseMenu()
optionsArray[options] = "Send Node Info";
}
optionsEnumArray[options++] = Position;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "New Preset";
#else
optionsArray[options] = "New Preset Msg";
#endif
optionsEnumArray[options++] = Preset;
if (kb_found) {
optionsArray[options] = "New Freetext Msg";
optionsEnumArray[options++] = Freetext;
}
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Home";
#else
bannerOptions.message = "Home Action";
#endif
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Home";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
@@ -606,21 +952,22 @@ void menuHandler::systemBaseMenu()
optionsArray[options] = "Display Options";
optionsEnumArray[options++] = ScreenOptions;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "Bluetooth";
#else
optionsArray[options] = "Bluetooth Toggle";
#endif
if (currentResolution == ScreenResolution::UltraLow) {
optionsArray[options] = "Bluetooth";
} else {
optionsArray[options] = "Bluetooth Toggle";
}
optionsEnumArray[options++] = Bluetooth;
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
optionsArray[options] = "WiFi Toggle";
optionsEnumArray[options++] = WiFiToggle;
#endif
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "Power";
#else
optionsArray[options] = "Reboot/Shutdown";
#endif
if (currentResolution == ScreenResolution::UltraLow) {
optionsArray[options] = "Power";
} else {
optionsArray[options] = "Reboot/Shutdown";
}
optionsEnumArray[options++] = PowerMenu;
if (test_enabled) {
@@ -629,11 +976,10 @@ void menuHandler::systemBaseMenu()
}
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "System";
#else
bannerOptions.message = "System Action";
#endif
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "System";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
@@ -670,32 +1016,49 @@ void menuHandler::systemBaseMenu()
void menuHandler::favoriteBaseMenu()
{
enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd };
#if defined(M5STACK_UNITC6L)
static const char *optionsArray[enumEnd] = {"Back", "New Preset"};
#else
static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"};
#endif
static int optionsEnumArray[enumEnd] = {Back, Preset};
int options = 2;
enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
// Only show "View Conversation" if a message exists with this node
uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum;
bool hasConversation = false;
for (const auto &m : messageStore.getMessages()) {
if ((m.sender == peer || m.dest == peer)) {
hasConversation = true;
break;
}
}
if (hasConversation) {
optionsArray[options] = "Go To Chat";
optionsEnumArray[options++] = GoToChat;
}
if (currentResolution == ScreenResolution::UltraLow) {
optionsArray[options] = "New Preset";
} else {
optionsArray[options] = "New Preset Msg";
}
optionsEnumArray[options++] = Preset;
if (kb_found) {
optionsArray[options] = "New Freetext Msg";
optionsEnumArray[options++] = Freetext;
}
#if !defined(M5STACK_UNITC6L)
optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute;
#endif
if (currentResolution != ScreenResolution::UltraLow) {
optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute;
}
optionsArray[options] = "Remove Favorite";
optionsEnumArray[options++] = Remove;
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Favorites";
#else
bannerOptions.message = "Favorites Action";
#endif
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Favorites";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
@@ -704,6 +1067,17 @@ void menuHandler::favoriteBaseMenu()
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
} else if (selected == Freetext) {
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
}
// Handle new Go To Thread action
else if (selected == GoToChat) {
// Switch thread to direct conversation with this node
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1,
graphics::UIRenderer::currentFavoriteNodeNum);
// Manually create and send a UIFrameEvent to trigger the jump
UIFrameEvent evt;
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
screen->handleUIFrameEvent(&evt);
} else if (selected == Remove) {
menuHandler::menuQueue = menuHandler::remove_favorite;
screen->runNow();
@@ -753,20 +1127,33 @@ void menuHandler::positionBaseMenu()
void menuHandler::nodeListMenu()
{
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd };
#if defined(M5STACK_UNITC6L)
static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"};
#else
static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"};
#endif
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
optionsArray[options] = "Add Favorite";
optionsEnumArray[options++] = Favorite;
optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute;
if (currentResolution != ScreenResolution::UltraLow) {
optionsArray[options] = "Key Verification";
optionsEnumArray[options++] = Verify;
}
if (currentResolution != ScreenResolution::UltraLow) {
optionsArray[options] = "Show Long/Short Name";
optionsEnumArray[options++] = NodeNameLength;
}
optionsArray[options] = "Reset NodeDB";
optionsEnumArray[options++] = Reset;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Node Action";
bannerOptions.optionsArrayPtr = optionsArray;
#if defined(M5STACK_UNITC6L)
bannerOptions.optionsCount = 3;
#else
bannerOptions.optionsCount = 5;
#endif
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Favorite) {
menuQueue = add_favorite;
@@ -780,6 +1167,9 @@ void menuHandler::nodeListMenu()
} else if (selected == TraceRoute) {
menuQueue = trace_route_menu;
screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
@@ -803,7 +1193,7 @@ void menuHandler::nodeNameLengthMenu()
LOG_INFO("Setting names to short");
config.display.use_long_node_name = false;
} else if (selected == Back) {
menuQueue = screen_options_menu;
menuQueue = node_base_menu;
screen->runNow();
}
};
@@ -831,6 +1221,9 @@ void menuHandler::resetNodeDBMenu()
LOG_INFO("Initiate node-db reset but keeping favorites");
nodeDB->resetNodes(1);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
} else if (selected == 0) {
menuQueue = node_base_menu;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
@@ -904,13 +1297,14 @@ void menuHandler::GPSFormatMenu()
{
static const char *optionsArray[] = {"Back",
isHighResolution ? "Decimal Degrees" : "DEC",
isHighResolution ? "Degrees Minutes Seconds" : "DMS",
isHighResolution ? "Universal Transverse Mercator" : "UTM",
isHighResolution ? "Military Grid Reference System" : "MGRS",
isHighResolution ? "Open Location Code" : "OLC",
isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR",
isHighResolution ? "Maidenhead Locator" : "MLS"};
(currentResolution == ScreenResolution::High) ? "Decimal Degrees" : "DEC",
(currentResolution == ScreenResolution::High) ? "Degrees Minutes Seconds" : "DMS",
(currentResolution == ScreenResolution::High) ? "Universal Transverse Mercator" : "UTM",
(currentResolution == ScreenResolution::High) ? "Military Grid Reference System"
: "MGRS",
(currentResolution == ScreenResolution::High) ? "Open Location Code" : "OLC",
(currentResolution == ScreenResolution::High) ? "Ordnance Survey Grid Ref" : "OSGR",
(currentResolution == ScreenResolution::High) ? "Maidenhead Locator" : "MLS"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "GPS Format";
bannerOptions.optionsArrayPtr = optionsArray;
@@ -958,11 +1352,10 @@ void menuHandler::BluetoothToggleMenu()
{
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Bluetooth";
#else
bannerOptions.message = "Toggle Bluetooth";
#endif
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Bluetooth";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
@@ -1178,17 +1571,17 @@ void menuHandler::rebootMenu()
{
static const char *optionsArray[] = {"Back", "Confirm"};
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Reboot";
#else
bannerOptions.message = "Reboot Device?";
#endif
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Reboot";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
nodeDB->saveToDisk();
messageStore.saveToFlash();
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
} else {
menuQueue = power_menu;
@@ -1202,11 +1595,10 @@ void menuHandler::shutdownMenu()
{
static const char *optionsArray[] = {"Back", "Confirm"};
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Shutdown";
#else
bannerOptions.message = "Shutdown Device?";
#endif
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Shutdown";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
@@ -1223,12 +1615,13 @@ void menuHandler::shutdownMenu()
void menuHandler::addFavoriteMenu()
{
#if defined(M5STACK_UNITC6L)
screen->showNodePicker("Node Favorite", 30000, [](uint32_t nodenum) -> void {
#else
screen->showNodePicker("Node To Favorite", 30000, [](uint32_t nodenum) -> void {
#endif
const char *NODE_PICKER_TITLE;
if (currentResolution == ScreenResolution::UltraLow) {
NODE_PICKER_TITLE = "Node Favorite";
} else {
NODE_PICKER_TITLE = "Node To Favorite";
}
screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void {
LOG_WARN("Nodenum: %u", nodenum);
nodeDB->set_favorite(true, nodenum);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
@@ -1393,16 +1786,11 @@ void menuHandler::screenOptionsMenu()
hasSupportBrightness = false;
#endif
enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor, FrameToggles, DisplayUnits };
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits };
static const char *optionsArray[5] = {"Back"};
static int optionsEnumArray[5] = {Back};
int options = 1;
#if defined(T_DECK) || defined(T_LORA_PAGER) || defined(HACKADAY_COMMUNICATOR)
optionsArray[options] = "Show Long/Short Name";
optionsEnumArray[options++] = NodeNameLength;
#endif
// Only show brightness for B&W displays
if (hasSupportBrightness) {
optionsArray[options] = "Brightness";
@@ -1416,7 +1804,7 @@ void menuHandler::screenOptionsMenu()
optionsEnumArray[options++] = ScreenColor;
#endif
optionsArray[options] = "Frame Visibility Toggle";
optionsArray[options] = "Frame Visibility";
optionsEnumArray[options++] = FrameToggles;
optionsArray[options] = "Display Units";
@@ -1434,9 +1822,6 @@ void menuHandler::screenOptionsMenu()
} else if (selected == ScreenColor) {
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
@@ -1471,11 +1856,10 @@ void menuHandler::powerMenu()
#endif
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Power";
#else
bannerOptions.message = "Reboot / Shutdown";
#endif
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Power";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
@@ -1532,7 +1916,8 @@ void menuHandler::FrameToggles_menu()
{
enum optionsNumbers {
Finish,
nodelist,
nodelist_nodes,
nodelist_location,
nodelist_lastheard,
nodelist_hopsignal,
nodelist_distance,
@@ -1553,20 +1938,25 @@ void menuHandler::FrameToggles_menu()
static int lastSelectedIndex = 0;
#ifndef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List";
optionsEnumArray[options++] = nodelist;
#endif
#ifdef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists";
optionsEnumArray[options++] = nodelist_nodes;
#else
optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
optionsEnumArray[options++] = nodelist_lastheard;
optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
optionsEnumArray[options++] = nodelist_hopsignal;
#endif
#if HAS_GPS
#ifndef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists";
optionsEnumArray[options++] = nodelist_location;
#else
optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
optionsEnumArray[options++] = nodelist_distance;
#endif
#if HAS_GPS
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings";
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings";
optionsEnumArray[options++] = nodelist_bearings;
#endif
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
optionsEnumArray[options++] = gps;
@@ -1605,8 +1995,12 @@ void menuHandler::FrameToggles_menu()
if (selected == Finish) {
screen->setFrames(Screen::FOCUS_DEFAULT);
} else if (selected == nodelist) {
screen->toggleFrameVisibility("nodelist");
} else if (selected == nodelist_nodes) {
screen->toggleFrameVisibility("nodelist_nodes");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_location) {
screen->toggleFrameVisibility("nodelist_location");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_lastheard) {
@@ -1722,6 +2116,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case position_base_menu:
positionBaseMenu();
break;
case node_base_menu:
nodeListMenu();
break;
#if !MESHTASTIC_EXCLUDE_GPS
case gps_toggle_menu:
GPSToggleMenu();
@@ -1802,6 +2199,18 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case throttle_message:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break;
case message_response_menu:
messageResponseMenu();
break;
case reply_menu:
replyMenu();
break;
case delete_messages_menu:
deleteMessagesMenu();
break;
case message_viewmode_menu:
messageViewModeMenu();
break;
}
menuQueue = menu_none;
}
@@ -1813,4 +2222,4 @@ void menuHandler::saveUIConfig()
} // namespace graphics
#endif
#endif

View File

@@ -19,6 +19,7 @@ class menuHandler
clock_face_picker,
clock_menu,
position_base_menu,
node_base_menu,
gps_toggle_menu,
gps_format_menu,
compass_point_north_menu,
@@ -43,6 +44,10 @@ class menuHandler
key_verification_final_prompt,
trace_route_menu,
throttle_message,
message_response_menu,
message_viewmode_menu,
reply_menu,
delete_messages_menu,
node_name_length_menu,
FrameToggles,
DisplayUnits
@@ -61,6 +66,9 @@ class menuHandler
static void TwelveHourPicker();
static void ClockFacePicker();
static void messageResponseMenu();
static void messageViewModeMenu();
static void replyMenu();
static void deleteMessagesMenu();
static void homeBaseMenu();
static void textMessageBaseMenu();
static void systemBaseMenu();
@@ -119,4 +127,4 @@ template <typename T> struct MenuOption {
using RadioPresetOption = MenuOption<meshtastic_Config_LoRaConfig_ModemPreset>;
} // namespace graphics
#endif
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,11 @@
#pragma once
#include "MessageStore.h" // for StoredMessage
#if HAS_SCREEN
#include "OLEDDisplay.h"
#include "OLEDDisplayUi.h"
#include "graphics/emotes.h"
#include "mesh/generated/meshtastic/mesh.pb.h" // for meshtastic_MeshPacket
#include <cstdint>
#include <string>
#include <vector>
@@ -10,6 +14,27 @@ namespace graphics
namespace MessageRenderer
{
// Thread filter modes
enum class ThreadMode { ALL, CHANNEL, DIRECT };
// Setter for switching thread mode
void setThreadMode(ThreadMode mode, int channel = -1, uint32_t peer = 0);
// Getter for current mode
ThreadMode getThreadMode();
// Getter for current channel (valid if mode == CHANNEL)
int getThreadChannel();
// Getter for current peer (valid if mode == DIRECT)
uint32_t getThreadPeer();
// Registry accessors for menuHandler
const std::vector<int> &getSeenChannels();
const std::vector<uint32_t> &getSeenPeers();
void clearThreadRegistries();
// Text and emote rendering
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount);
@@ -20,11 +45,27 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
// Function to calculate heights for each line
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes);
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes,
const std::vector<bool> &isHeaderVec);
// Function to render the message content
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,
int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold);
// Reset scroll state when new messages arrive
void resetScrollState();
// Manual scroll control for encoder-style inputs
void nudgeScroll(int8_t direction);
// Helper to auto-select the correct thread mode from a message
void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet);
// Handles a new incoming/outgoing message: banner, wake, thread select, scroll reset
void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet);
// Clear Message Line Cache from Message Renderer
void clearMessageCache();
void scrollUp();
void scrollDown();
} // namespace MessageRenderer
} // namespace graphics
#endif

View File

@@ -23,7 +23,6 @@ extern graphics::Screen *screen;
#if defined(M5STACK_UNITC6L)
static uint32_t lastSwitchTime = 0;
#else
#endif
namespace graphics
{
@@ -46,79 +45,119 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *
}
// Static variables for dynamic cycling
static NodeListMode currentMode = MODE_LAST_HEARD;
static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD;
static ListMode_Location currentMode_Location = MODE_DISTANCE;
static int scrollIndex = 0;
// Popup overlay state
static uint32_t popupTime = 0;
static int popupTotal = 0;
static int popupStart = 0;
static int popupEnd = 0;
static int popupPage = 1;
static int popupMaxPage = 1;
static const uint32_t POPUP_DURATION_MS = 1000; // 1 second visible
// =============================
// Scrolling Logic
// =============================
void scrollUp()
{
if (scrollIndex > 0)
scrollIndex--;
popupTime = millis(); // show popup
}
void scrollDown()
{
scrollIndex++;
popupTime = millis();
}
// =============================
// Utility Functions
// =============================
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node)
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth)
{
const char *name = NULL;
static char nodeName[16] = "?";
if (config.display.use_long_node_name == true) {
if (node->has_user && strlen(node->user.long_name) > 0) {
name = node->user.long_name;
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
}
} else {
if (node->has_user && strlen(node->user.short_name) > 0) {
name = node->user.short_name;
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
}
static char nodeName[25]; // single static buffer we return
nodeName[0] = '\0';
auto writeFallbackId = [&] {
std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
};
// 1) Choose target candidate (long vs short) only if present
const char *raw = nullptr;
if (node && node->has_user) {
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
}
// Use sanitizeString() function and copy directly into nodeName
std::string sanitized_name = sanitizeString(name ? name : "");
// 2) Sanitize (empty if raw is null/empty)
std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{};
if (!sanitized_name.empty()) {
strncpy(nodeName, sanitized_name.c_str(), sizeof(nodeName) - 1);
nodeName[sizeof(nodeName) - 1] = '\0';
// 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed)
if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) {
writeFallbackId();
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
// %.*s ensures null-termination and safe truncation to buffer size - 1
std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast<int>(sizeof(nodeName) - 1), s.c_str());
}
if (config.display.use_long_node_name == true) {
int availWidth = (SCREEN_WIDTH / 2) - 65;
// 4) Width-based truncation + ellipsis (long-name mode only)
if (config.display.use_long_node_name && display) {
int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38);
if (availWidth < 0)
availWidth = 0;
size_t origLen = strlen(nodeName);
while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) {
nodeName[strlen(nodeName) - 1] = '\0';
const size_t beforeLen = std::strlen(nodeName);
// Trim from the end until it fits or is empty
size_t len = beforeLen;
while (len && display->getStringWidth(nodeName) > availWidth) {
nodeName[--len] = '\0';
}
// If we actually truncated, append "..." (ensure space remains in buffer)
if (strlen(nodeName) < origLen) {
size_t len = strlen(nodeName);
size_t maxLen = sizeof(nodeName) - 4; // 3 for "..." and 1 for '\0'
if (len > maxLen) {
nodeName[maxLen] = '\0';
len = maxLen;
// If truncated, append "..." (respect buffer size)
if (len < beforeLen) {
// Make sure there's room for "..." and '\0'
const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0'
const size_t needed = 3; // "..."
if (len > capForText - needed) {
len = capForText - needed;
nodeName[len] = '\0';
}
strcat(nodeName, "...");
std::strcat(nodeName, "...");
}
}
return nodeName;
}
const char *getCurrentModeTitle(int screenWidth)
const char *getCurrentModeTitle_Nodes(int screenWidth)
{
switch (currentMode) {
switch (currentMode_Nodes) {
case MODE_LAST_HEARD:
return "Last Heard";
case MODE_HOP_SIGNAL:
#ifdef USE_EINK
return "Hops/Sig";
#else
return (isHighResolution) ? "Hops/Signal" : "Hops/Sig";
return (currentResolution == ScreenResolution::High) ? "Hops/Signal" : "Hops/Sig";
#endif
default:
return "Nodes";
}
}
const char *getCurrentModeTitle_Location(int screenWidth)
{
switch (currentMode_Location) {
case MODE_DISTANCE:
return "Distance";
case MODE_BEARING:
return "Bearings";
default:
return "Nodes";
}
@@ -137,10 +176,8 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
{
int columnWidth = display->getWidth() / 2;
int separatorX = x + columnWidth - 2;
for (int y = yStart; y <= yEnd; y += 2) {
display->setPixel(separatorX, y);
display->setPixel(x, y);
}
}
@@ -152,7 +189,8 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
int scrollbarX = display->getWidth() - 2;
int scrollbarHeight = display->getHeight() - scrollStartY - 10;
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows);
int perPage = visibleNodeRows * columns;
int maxScroll = std::max(0, (totalEntries - 1) / perPage);
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
for (int i = 0; i < thumbHeight; i++) {
@@ -167,9 +205,9 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{
bool isLeftCol = (x < SCREEN_WIDTH / 2);
int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
const char *nodeName = getSafeNodeName(display, node);
const char *nodeName = getSafeNodeName(display, node, columnWidth);
char timeStr[10];
uint32_t seconds = sinceLastSeen(node);
@@ -188,9 +226,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawString(x + ((isHighResolution) ? 6 : 3), y, nodeName);
display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName);
if (node->is_favorite) {
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -209,19 +247,19 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = columnWidth - 25;
int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
int barsXOffset = columnWidth - barsOffset;
const char *nodeName = getSafeNodeName(display, node);
const char *nodeName = getSafeNodeName(display, node, columnWidth);
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) {
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -256,9 +294,10 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{
bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
int nameMaxWidth =
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(display, node);
const char *nodeName = getSafeNodeName(display, node, columnWidth);
char distStr[10] = "";
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
@@ -311,9 +350,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) {
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -321,26 +360,24 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
}
if (strlen(distStr) > 0) {
int offset = (isHighResolution) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
int offset = (currentResolution == ScreenResolution::High)
? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
int rightEdge = x + columnWidth - offset;
int textWidth = display->getStringWidth(distStr);
display->drawString(rightEdge - textWidth, y, distStr);
}
}
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{
switch (currentMode) {
switch (currentMode_Nodes) {
case MODE_LAST_HEARD:
drawEntryLastHeard(display, node, x, y, columnWidth);
break;
case MODE_HOP_SIGNAL:
drawEntryHopSignal(display, node, x, y, columnWidth);
break;
case MODE_DISTANCE:
drawNodeDistance(display, node, x, y, columnWidth);
break;
default:
break;
}
@@ -351,15 +388,16 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
bool isLeftCol = (x < SCREEN_WIDTH / 2);
// Adjust max text width depending on column and screen width
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
int nameMaxWidth =
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(display, node);
const char *nodeName = getSafeNodeName(display, node, columnWidth);
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) {
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -374,7 +412,7 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
return;
bool isLeftCol = (x < SCREEN_WIDTH / 2);
int arrowXOffset = (isHighResolution) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
int centerX = x + columnWidth - arrowXOffset;
int centerY = y + FONT_HEIGHT_SMALL / 2;
@@ -431,11 +469,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
locationScreen = true;
else if (strcmp(title, "Distance") == 0)
locationScreen = true;
#if defined(M5STACK_UNITC6L)
int columnWidth = display->getWidth();
#else
int columnWidth = display->getWidth() / 2;
#endif
display->clear();
// Draw the battery/time header
@@ -444,39 +477,74 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// Space below header
y += COMMON_HEADER_HEIGHT;
int totalColumns = 1; // Default to 1 column
if (config.display.use_long_node_name) {
if (SCREEN_WIDTH <= 240) {
totalColumns = 1;
} else if (SCREEN_WIDTH > 240) {
totalColumns = 2;
}
} else {
if (SCREEN_WIDTH <= 64) {
totalColumns = 1;
} else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) {
totalColumns = 2;
} else {
totalColumns = 3;
}
}
int columnWidth = display->getWidth() / totalColumns;
int totalEntries = nodeDB->getNumMeshNodes();
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
int numskipped = 0;
int visibleNodeRows = totalRowsAvailable;
#if defined(M5STACK_UNITC6L)
int totalColumns = 1;
#else
int totalColumns = 2;
#endif
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) {
startIndex++; // skip own node
}
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
// Build filtered + ordered list
std::vector<int> drawList;
drawList.reserve(totalEntries);
for (int i = 0; i < totalEntries; i++) {
auto *n = nodeDB->getMeshNodeByIndex(i);
if (!n)
continue;
if (n->num == nodeDB->getNodeNum())
continue;
if (locationScreen && !n->has_position)
continue;
drawList.push_back(n->num);
}
totalEntries = drawList.size();
int perPage = visibleNodeRows * totalColumns;
int maxScroll = 0;
if (perPage > 0) {
maxScroll = std::max(0, (totalEntries - 1) / perPage);
}
if (scrollIndex > maxScroll)
scrollIndex = maxScroll;
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
int yOffset = 0;
int col = 0;
int lastNodeY = y;
int shownCount = 0;
int rowCount = 0;
for (int i = startIndex; i < endIndex; ++i) {
if (locationScreen && !nodeDB->getMeshNodeByIndex(i)->has_position) {
numskipped++;
continue;
}
for (int idx = startIndex; idx < endIndex; idx++) {
uint32_t nodeNum = drawList[idx];
auto *node = nodeDB->getMeshNode(nodeNum);
int xPos = x + (col * columnWidth);
int yPos = y + yOffset;
renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
if (extras) {
extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon);
}
renderer(display, node, xPos, yPos, columnWidth);
if (extras)
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
yOffset += rowYOffset;
@@ -495,17 +563,73 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// This should correct the scrollbar
totalEntries -= numskipped;
#if !defined(M5STACK_UNITC6L)
// Draw column separator
if (shownCount > 0) {
if (currentResolution != ScreenResolution::UltraLow && shownCount > 0) {
const int firstNodeY = y + 3;
drawColumnSeparator(display, x, firstNodeY, lastNodeY);
for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) {
drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY);
}
}
#endif
const int scrollStartY = y + 3;
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY);
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY);
graphics::drawCommonFooter(display, x, y);
// Scroll Popup Overlay
if (millis() - popupTime < POPUP_DURATION_MS) {
popupTotal = totalEntries;
int perPage = visibleNodeRows * totalColumns;
popupStart = startIndex + 1;
popupEnd = std::min(startIndex + perPage, totalEntries);
popupPage = (scrollIndex + 1);
popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage);
char buf[32];
snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage);
display->setTextAlignment(TEXT_ALIGN_LEFT);
// Box padding
int padding = 2;
int textW = display->getStringWidth(buf);
int textH = FONT_HEIGHT_SMALL;
int boxWidth = textW + padding * 3;
int boxHeight = textH + padding * 2;
// Center of usable screen area:
int headerHeight = FONT_HEIGHT_SMALL - 1;
int footerHeight = FONT_HEIGHT_SMALL + 2;
int usableTop = headerHeight;
int usableBottom = display->getHeight() - footerHeight;
int usableHeight = usableBottom - usableTop;
// Center point inside usable area
int boxLeft = (display->getWidth() - boxWidth) / 2;
int boxTop = usableTop + (usableHeight - boxHeight) / 2;
// Draw Box
display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
display->setColor(WHITE);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
// Text
display->drawString(boxLeft + padding, boxTop + padding, buf);
}
}
// =============================
@@ -513,10 +637,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// =============================
#ifndef USE_EINK
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
// Node list for Last Heard and Hop Signal views
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// Static variables to track mode and duration
static NodeListMode lastRenderedMode = MODE_COUNT;
static ListMode_Node lastRenderedMode = MODE_COUNT_NODE;
static unsigned long modeStartTime = 0;
unsigned long now = millis();
@@ -529,23 +654,65 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state,
}
#endif
// On very first call (on boot or state enter)
if (lastRenderedMode == MODE_COUNT) {
currentMode = MODE_LAST_HEARD;
if (lastRenderedMode == MODE_COUNT_NODE) {
currentMode_Nodes = MODE_LAST_HEARD;
modeStartTime = now;
}
// Time to switch to next mode?
if (now - modeStartTime >= getModeCycleIntervalMs()) {
currentMode = static_cast<NodeListMode>((currentMode + 1) % MODE_COUNT);
currentMode_Nodes = static_cast<ListMode_Node>((currentMode_Nodes + 1) % MODE_COUNT_NODE);
modeStartTime = now;
}
// Render screen based on currentMode
const char *title = getCurrentModeTitle(display->getWidth());
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic);
const char *title = getCurrentModeTitle_Nodes(display->getWidth());
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes);
// Track the last mode to avoid reinitializing modeStartTime
lastRenderedMode = currentMode;
lastRenderedMode = currentMode_Nodes;
}
// Node list for Distance and Bearings views
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// Static variables to track mode and duration
static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION;
static unsigned long modeStartTime = 0;
unsigned long now = millis();
#if defined(M5STACK_UNITC6L)
display->clear();
if (now - lastSwitchTime >= 3000) {
display->display();
lastSwitchTime = now;
}
#endif
// On very first call (on boot or state enter)
if (lastRenderedMode == MODE_COUNT_LOCATION) {
currentMode_Location = MODE_DISTANCE;
modeStartTime = now;
}
// Time to switch to next mode?
if (now - modeStartTime >= getModeCycleIntervalMs()) {
currentMode_Location = static_cast<ListMode_Location>((currentMode_Location + 1) % MODE_COUNT_LOCATION);
modeStartTime = now;
}
// Render screen based on currentMode
const char *title = getCurrentModeTitle_Location(display->getWidth());
// Render screen based on currentMode_Location
if (currentMode_Location == MODE_DISTANCE) {
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
} else if (currentMode_Location == MODE_BEARING) {
drawNodeListWithCompasses(display, state, x, y);
}
// Track the last mode to avoid reinitializing modeStartTime
lastRenderedMode = currentMode_Location;
}
#endif
@@ -566,14 +733,12 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
#endif
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
}
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
const char *title = "Distance";
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
}
#endif
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
float heading = 0;

View File

@@ -23,8 +23,11 @@ namespace NodeListRenderer
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
// Node list mode enumeration
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
// Node list mode enumeration for Last Heard and Hop Signal views
enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = 2 };
// Node list mode enumeration for Distance and Bearings views
enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 };
// Main node list screen function
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
@@ -35,7 +38,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
// Extras renderers
@@ -46,14 +49,20 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Utility functions
const char *getCurrentModeTitle(int screenWidth);
const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
const char *getCurrentModeTitle_Nodes(int screenWidth);
const char *getCurrentModeTitle_Location(int screenWidth);
const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth);
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
// Scrolling controls
void scrollUp();
void scrollDown();
// Bitmap drawing function
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display);

View File

@@ -1,6 +1,6 @@
#include "configuration.h"
#if HAS_SCREEN
#if HAS_SCREEN
#include "DisplayFormatters.h"
#include "NodeDB.h"
#include "NotificationRenderer.h"
@@ -38,7 +38,7 @@ extern bool hasUnreadMessage;
namespace graphics
{
int bannerSignalBars = -1;
InputEvent NotificationRenderer::inEvent;
int8_t NotificationRenderer::curSelected = 0;
char NotificationRenderer::alertBannerMessage[256] = {0};
@@ -321,7 +321,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
}
if (i == curSelected) {
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
strncpy(scratchLineBuffer[scratchLineNum], "> ", 3);
strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36);
strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3);
@@ -449,7 +449,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
if (i == curSelected) {
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
strncpy(lineBuffer, "> ", 3);
strncpy(lineBuffer + 2, optionsArrayPtr[i], 36);
strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3);
@@ -477,7 +477,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
bool is_picker = false;
uint16_t lineCount = 0;
// === Layout Configuration ===
// Layout Configuration
constexpr uint16_t hPadding = 5;
constexpr uint16_t vPadding = 2;
bool needs_bell = false;
@@ -491,13 +491,32 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
// Track widest line INCLUDING bars (but don't change per-line widths)
uint16_t widestLineWithBars = 0;
while (lines[lineCount] != nullptr) {
auto newlinePointer = strchr(lines[lineCount], '\n');
if (newlinePointer)
lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first
else // if the newline wasn't found, then pull string length from strlen
lineLengths[lineCount] = strlen(lines[lineCount]);
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
// Consider extra width for signal bars on lines that contain "Signal:"
uint16_t potentialWidth = lineWidths[lineCount];
if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) {
const int totalBars = 5;
const int barWidth = 3;
const int barSpacing = 2;
const int gap = 6; // space between text and bars
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
potentialWidth += barsWidth;
}
if (potentialWidth > widestLineWithBars)
widestLineWithBars = potentialWidth;
if (!is_picker) {
needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr);
if (lineWidths[lineCount] > maxWidth)
@@ -507,12 +526,16 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
}
// count lines
// Ensure box accounts for signal bars if present
if (widestLineWithBars > maxWidth)
maxWidth = widestLineWithBars;
uint16_t boxWidth = hPadding * 2 + maxWidth;
#if defined(M5STACK_UNITC6L)
if (needs_bell) {
if (isHighResolution && boxWidth <= 150)
if ((currentResolution == ScreenResolution::High) && boxWidth <= 150)
boxWidth += 26;
if (!isHighResolution && boxWidth <= 100)
if ((currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) && boxWidth <= 100)
boxWidth += 20;
}
@@ -521,14 +544,17 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
uint16_t boxHeight = contentHeight + vPadding * 2;
if (visibleTotalLines == 1)
boxHeight += (isHighResolution ? 4 : 3);
if (visibleTotalLines == 1) {
boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3;
}
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
if (totalLines > visibleTotalLines)
boxWidth += (isHighResolution ? 4 : 2);
if (totalLines > visibleTotalLines) {
boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2;
}
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1;
#if defined(M5STACK_UNITC6L)
if (visibleTotalLines == 1) {
boxTop += 25;
}
@@ -539,127 +565,9 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
if (boxTop < 0)
boxTop = 0;
}
#endif
// === Draw Box ===
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(WHITE);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
int16_t lineY = boxTop + vPadding;
int swingRange = 8;
static int swingOffset = 0;
static bool swingRight = true;
static unsigned long lastSwingTime = 0;
unsigned long now = millis();
int swingSpeedMs = 10 / (swingRange * 2);
if (now - lastSwingTime >= (unsigned long)swingSpeedMs) {
lastSwingTime = now;
if (swingRight) {
swingOffset++;
if (swingOffset >= swingRange)
swingRight = false;
} else {
swingOffset--;
if (swingOffset <= 0)
swingRight = true;
}
}
for (int i = 0; i < lineCount; i++) {
bool isTitle = (i == 0);
int globalOptionIndex = (i - 1) + firstOptionToShow;
bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected);
uint16_t visibleWidth = 64 - hPadding * 2;
if (totalLines > visibleTotalLines)
visibleWidth -= 6;
char lineBuffer[lineLengths[i] + 1];
strncpy(lineBuffer, lines[i], lineLengths[i]);
lineBuffer[lineLengths[i]] = '\0';
if (isTitle) {
if (visibleTotalLines == 1) {
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
display->setColor(WHITE);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
} else {
display->setColor(WHITE);
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
display->setColor(BLACK);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
display->setColor(WHITE);
if (needs_bell) {
int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2;
display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert);
display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert);
}
}
lineY = boxTop + effectiveLineHeight + 1;
} else if (isSelectedOption) {
display->setColor(WHITE);
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
display->setColor(BLACK);
if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) {
int textX = boxLeft + hPadding + swingOffset;
display->drawString(textX, lineY - 1, lineBuffer);
} else {
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer);
}
display->setColor(WHITE);
lineY += effectiveLineHeight;
} else {
display->setColor(BLACK);
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
display->setColor(WHITE);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer);
lineY += effectiveLineHeight;
}
}
if (totalLines > visibleTotalLines) {
const uint8_t scrollBarWidth = 5;
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
float ratio = (float)visibleTotalLines / totalLines;
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
}
#else
if (needs_bell) {
if (isHighResolution && boxWidth <= 150)
boxWidth += 26;
if (!isHighResolution && boxWidth <= 100)
boxWidth += 20;
}
uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
uint16_t boxHeight = contentHeight + vPadding * 2;
if (visibleTotalLines == 1) {
boxHeight += (isHighResolution) ? 4 : 3;
}
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
if (totalLines > visibleTotalLines) {
boxWidth += (isHighResolution) ? 4 : 2;
}
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
// === Draw Box ===
// Draw Box
display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
@@ -675,7 +583,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
// === Draw Content ===
// Draw Content
int16_t lineY = boxTop + vPadding;
for (int i = 0; i < lineCount; i++) {
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
@@ -704,17 +612,47 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
lineY += (effectiveLineHeight - 2 - background_yOffset);
} else {
// Pop-up
display->drawString(textX, lineY, lineBuffer);
// If this is the Signal line, center text + bars as one group
bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr);
if (isSignalLine) {
const int totalBars = 5;
const int barWidth = 3;
const int barSpacing = 2;
const int barHeightStep = 2;
const int gap = 6;
int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true);
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
int totalWidth = textWidth + barsWidth;
int groupStartX = boxLeft + (boxWidth - totalWidth) / 2;
display->drawString(groupStartX, lineY, lineBuffer);
int baseX = groupStartX + textWidth + gap;
int baseY = lineY + effectiveLineHeight - 1;
for (int b = 0; b < totalBars; b++) {
int barHeight = (b + 1) * barHeightStep;
int x = baseX + b * (barWidth + barSpacing);
int y = baseY - barHeight;
if (b < graphics::bannerSignalBars) {
display->fillRect(x, y, barWidth, barHeight);
} else {
display->drawRect(x, y, barWidth, barHeight);
}
}
} else {
display->drawString(textX, lineY, lineBuffer);
}
lineY += (effectiveLineHeight);
}
}
// === Scroll Bar (Thicker, inside box, not over title) ===
// Scroll Bar (Thicker, inside box, not over title)
if (totalLines > visibleTotalLines) {
const uint8_t scrollBarWidth = 5;
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
float ratio = (float)visibleTotalLines / totalLines;
@@ -725,7 +663,6 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
}
#endif
}
/// Draw the last text message we received

View File

@@ -6,10 +6,7 @@
#include "NodeListRenderer.h"
#include "UIRenderer.h"
#include "airtime.h"
#include "configuration.h"
#include "gps/GeoCoord.h"
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TimeFormatters.h"
#include "graphics/images.h"
@@ -29,6 +26,16 @@ namespace graphics
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
std::vector<meshtastic_NodeInfoLite *> graphics::UIRenderer::favoritedNodes;
static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y)
{
int yOffset = (currentResolution == ScreenResolution::High) ? -5 : 1;
if (currentResolution == ScreenResolution::High) {
NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite);
}
}
void graphics::UIRenderer::rebuildFavoritedNodes()
{
favoritedNodes.clear();
@@ -56,7 +63,7 @@ extern uint32_t dopThresholds[5];
void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
{
// Draw satellite image
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite);
@@ -76,7 +83,7 @@ void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const mesht
} else {
snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites());
}
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
display->drawString(x + 18, y, textString);
} else {
display->drawString(x + 11, y, textString);
@@ -244,16 +251,16 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
// Draw nodes status
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset,
bool show_total, String additional_words)
bool show_total, const char *additional_words)
{
char usersString[20];
int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0;
snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words.c_str());
snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words);
if (show_total) {
int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0;
snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words.c_str());
snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words);
}
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
@@ -261,19 +268,19 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else {
display->drawFastImage(x, y + 3, 8, 8, imgUser);
}
#else
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else {
display->drawFastImage(x, y + 1, 8, 8, imgUser);
}
#endif
int string_offset = (isHighResolution) ? 9 : 0;
int string_offset = (currentResolution == ScreenResolution::High) ? 9 : 0;
display->drawString(x + 10 + string_offset, y - 2, usersString);
}
@@ -321,11 +328,12 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
int line = 1; // which slot to use next
std::string usernameStr;
// === 1. Long Name (always try to show first) ===
#if defined(M5STACK_UNITC6L)
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr;
#else
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
#endif
const char *username;
if (currentResolution == ScreenResolution::UltraLow) {
username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr;
} else {
username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
}
if (username) {
usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
@@ -501,7 +509,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
const int margin = 4;
// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) -----------
#if defined(USE_EINK)
const int iconSize = (isHighResolution) ? 16 : 8;
const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8;
const int navBarHeight = iconSize + 6;
#else
const int navBarHeight = 0;
@@ -559,11 +567,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// === Header ===
#if defined(M5STACK_UNITC6L)
graphics::drawCommonHeader(display, x, y, "Home");
#else
graphics::drawCommonHeader(display, x, y, "");
#endif
if (currentResolution == ScreenResolution::UltraLow) {
graphics::drawCommonHeader(display, x, y, "Home");
} else {
graphics::drawCommonHeader(display, x, y, "");
}
// === Content below header ===
@@ -578,15 +586,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
config.display.heading_bold = false;
// Display Region and Channel Utilization
#if defined(M5STACK_UNITC6L)
drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
#else
drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
#endif
if (currentResolution == ScreenResolution::UltraLow) {
drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
} else {
drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
}
char uptimeStr[32] = "";
#if !defined(M5STACK_UNITC6L)
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
#endif
if (currentResolution != ScreenResolution::UltraLow) {
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
}
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr);
// === Second Row: Satellites and Voltage ===
@@ -600,15 +608,8 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
} else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
}
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (isHighResolution) ? 6 : 0;
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
} else {
UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus);
@@ -647,21 +648,22 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10
: display->getStringWidth(chUtil) + 5;
int chUtil_y = getTextPositions(display)[line] + 3;
int chutil_bar_width = (isHighResolution) ? 100 : 50;
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50;
if (!config.bluetooth.enabled) {
#if defined(USE_EINK)
chutil_bar_width = (isHighResolution) ? 50 : 30;
chutil_bar_width = (currentResolution == ScreenResolution::High) ? 50 : 30;
#else
chutil_bar_width = (isHighResolution) ? 80 : 40;
chutil_bar_width = (currentResolution == ScreenResolution::High) ? 80 : 40;
#endif
}
int chutil_bar_height = (isHighResolution) ? 12 : 7;
int extraoffset = (isHighResolution) ? 6 : 3;
int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7;
int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3;
if (!config.bluetooth.enabled) {
extraoffset = (isHighResolution) ? 6 : 1;
extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 1;
}
int chutil_percent = airTime->channelUtilizationPercent();
@@ -721,7 +723,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === Fourth & Fifth Rows: Node Identity ===
int textWidth = 0;
int nameX = 0;
int yOffset = (isHighResolution) ? 0 : 5;
int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5;
std::string longNameStr;
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
@@ -759,7 +761,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// Start Functions to write date/time to the screen
// Helper function to check if a year is a leap year
bool isLeapYear(int year)
constexpr bool isLeapYear(int year)
{
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
}
@@ -990,15 +992,8 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
} else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
}
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (isHighResolution) ? 6 : 0;
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else {
// Onboard GPS
@@ -1156,7 +1151,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA;
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH,
USERPREFS_OEM_IMAGE_HEIGHT, xbm);
@@ -1181,7 +1176,7 @@ void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, O
display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *title = USERPREFS_OEM_TEXT;
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title);
}
display->setFont(FONT_SMALL);
@@ -1225,15 +1220,15 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
lastFrameChangeTime = millis();
}
const int iconSize = isHighResolution ? 16 : 8;
const int spacing = isHighResolution ? 8 : 4;
const int bigOffset = isHighResolution ? 1 : 0;
const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8;
const int spacing = (currentResolution == ScreenResolution::High) ? 8 : 4;
const int bigOffset = (currentResolution == ScreenResolution::High) ? 1 : 0;
const size_t totalIcons = screen->indicatorIcons.size();
if (totalIcons == 0)
return;
const int navPadding = isHighResolution ? 24 : 12; // padding per side
const int navPadding = (currentResolution == ScreenResolution::High) ? 24 : 12; // padding per side
int usableWidth = SCREEN_WIDTH - (navPadding * 2);
if (usableWidth < iconSize)
@@ -1300,7 +1295,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setColor(BLACK);
}
if (isHighResolution) {
if (currentResolution == ScreenResolution::High) {
NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display);
} else {
display->drawXbm(x, y, iconSize, iconSize, icon);
@@ -1315,7 +1310,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
auto drawArrow = [&](bool rightSide) {
display->setColor(WHITE);
const int offset = isHighResolution ? 3 : 1;
const int offset = (currentResolution == ScreenResolution::High) ? 3 : 1;
const int halfH = rectHeight / 2;
const int top = (y - 2) + (rectHeight - halfH) / 2;

View File

@@ -34,7 +34,7 @@ class UIRenderer
public:
// Common UI elements
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
int node_offset = 0, bool show_total = true, String additional_words = "");
int node_offset = 0, bool show_total = true, const char *additional_words = "");
// GPS status functions
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
@@ -43,9 +43,6 @@ class UIRenderer
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
// Layout and utility functions
static void drawScrollbar(OLEDDisplay *display, int visibleItems, int totalItems, int scrollIndex, int x, int startY);
// Overlay and special screens
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
@@ -83,8 +80,6 @@ class UIRenderer
static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds);
static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime);
// Message filtering
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet);
// Check if the display can render a string (detect special chars; emoji)
static bool haveGlyphs(const char *str);
}; // namespace UIRenderer

View File

@@ -13,41 +13,81 @@ const Emote emotes[] = {
{"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down
// --- Smileys (Multiple Unicode Aliases) ---
{"\U0001F60A", Smiling_Eyes, Smiling_Eyes_width, Smiling_Eyes_height}, // 😊 Smiling Eyes
{"\U0001F600", Grinning, Grinning_width, Grinning_height}, // 😀 Grinning Face
{"\U0001F642", Slightly_Smiling, Slightly_Smiling_width, Slightly_Smiling_height}, // 🙂 Slightly Smiling Face
{"\U0001F609", Winking_Face, Winking_Face_width, Winking_Face_height}, // 😉 Winking Face
{"\U0001F601", Grinning_Smiling_Eyes, Grinning_Smiling_Eyes_width, Grinning_Smiling_Eyes_height}, // 😁 Grinning Smiling Eyes
{"\U0001F60D", Heart_eyes, Heart_eyes_width, Heart_eyes_height}, // 😍 Heart Eyes
{"\U0001F60A", smiling_eyes, smiling_eyes_width, smiling_eyes_height}, // 😊 Smiling Eyes
{"\U0001F600", grinning, grinning_width, grinning_height}, // 😀 Grinning Face
{"\U0001F642", slightly_smiling, slightly_smiling_width, slightly_smiling_height}, // 🙂 Slightly Smiling Face
{"\U0001F609", winking_face, winking_face_width, winking_face_height}, // 😉 Winking Face
{"\U0001F601", grinning_smiling_eyes, grinning_smiling_eyes_width, grinning_smiling_eyes_height}, // 😁 Grinning Smiling Eyes
{"\U0001F60D", heart_eyes, heart_eyes_width, heart_eyes_height}, // 😍 Heart Eyes
{"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // 🥰 Smiling Face with Hearts
// --- Question/Alert ---
{"\u2753", question, question_width, question_height}, // ❓ Question Mark
{"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark
{"\u2753", question, question_width, question_height}, // ❓ Question Mark
{"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark
{"\u26A0\uFE0F", caution, caution_width, caution_height}, // ⚠️ Warning Sign
// --- Laughing Faces ---
{"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy
{"\U0001F923", ROFL, ROFL_width, ROFL_height}, // 🤣 Rolling on the Floor Laughing
{"\U0001F606", Smiling_Closed_Eyes, Smiling_Closed_Eyes_width, Smiling_Closed_Eyes_height}, // 😆 Smiling Closed Eyes
{"\U0001F923", rofl, rofl_width, rofl_height}, // 🤣 Rolling on the Floor Laughing
{"\U0001F606", smiling_closed_eyes, smiling_closed_eyes_width, smiling_closed_eyes_height}, // 😆 Smiling Closed Eyes
{"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat
{"\U0001F604", Grinning_SmilingEyes2, Grinning_SmilingEyes2_width,
Grinning_SmilingEyes2_height}, // 😄 Grinning Face with Smiling Eyes
{"\U0001F62D", Loudly_Crying_Face, Loudly_Crying_Face_width, Loudly_Crying_Face_height}, // 😭 Loudly Crying Face
{"\U0001F604", grinning_smiling_eyes_2, grinning_smiling_eyes_2_width,
grinning_smiling_eyes_2_height}, // 😄 Grinning Face with Smiling Eyes
{"\U0001F62D", loudly_crying_face, loudly_crying_face_width, loudly_crying_face_height}, // 😭 Loudly Crying Face
{"\U0001F92E", vomiting, vomiting_width, vomiting_height}, // 🤮 Face Vomiting
{"\U0001F60E", cool, cool_width, cool_height}, // 😎 Smiling Face with Sunglasses
{"\U0001F440", eyes, eyes_width, eyes_height}, // 👀 Eyes
{"\U0001F441\uFE0F", eye, eye_width, eye_height}, // 👁️ Eye
// --- Gestures and People ---
{"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand
{"\u270C\uFE0F", peace_sign, peace_sign_width, peace_sign_height}, // ✌️ Victory Hand
{"\U0001F596", vulcan_salute, vulcan_salute_width, vulcan_salute_height}, // 🖖 Vulcan Salute
{"\U0001F64F", Praying, Praying_width, Praying_height}, // 🙏 Praying Hands
{"\U0001F64F", praying, praying_width, praying_height}, // 🙏 Praying Hands
{"\U0001F4AA", strong, strong_width, strong_height}, // 💪 Flexed Biceps
{"\U0001F937", shrug, shrug_width, shrug_height}, // 🤷 Person Shrugging
{"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🤠 Cowboy Hat Face
{"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones
// --- Symbols ---
{"\u2714\uFE0F", check_mark, check_mark_width, check_mark_height}, // ✔️ Check Mark
{"\u2705", check_mark, check_mark_width, check_mark_height}, // ✅ Check Mark Button
{"\u2611\uFE0F", check_mark, check_mark_width, check_mark_height}, // ☑️ Check Box with Check
{"\U0001F3E0", house, house_width, house_height}, // 🏠 House
// --- Weather ---
{"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector)
{"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector)
{"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain
{"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud
{"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog
{"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector)
{"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector)
{"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain
{"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud
{"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog
{"\u2744\uFE0F", snowflake, snowflake_width, snowflake_height}, // ❄️ Snowflake
{"\U0001F4A7", drop, drop_width, drop_height}, // 💧 Droplet
{"\U0001F321\uFE0F", thermometer, thermometer_width, thermometer_height}, // 🌡️ Thermometer
{"\U0001F326\uFE0F", sun_behind_raincloud, sun_behind_raincloud_width,
sun_behind_raincloud_height}, // 🌦️ Sun Behind Rain Cloud
{"\u26C5", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅ Sun Behind Cloud
{"\u26C5\uFE0F", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅️ Sun Behind Cloud
{"\U0001F328\uFE0F", cloud_with_snow, cloud_with_snow_width, cloud_with_snow_height}, // 🌨️ Cloud with Snow
{"\U0001F329\uFE0F", cloud_with_lightning, cloud_with_lightning_width,
cloud_with_lightning_height}, // 🌩️ Cloud with Lightning
{"\u26C8", cloud_with_lightning_rain, cloud_with_lightning_rain_width,
cloud_with_lightning_rain_height}, // ⛈ Cloud with Lightning and Rain
{"\u26C8\uFE0F", cloud_with_lightning_rain, cloud_with_lightning_rain_width,
cloud_with_lightning_rain_height}, // ⛈️ Cloud with Lightning and Rain
{"\U0001F32C\uFE0F", wind_face, wind_face_width, wind_face_height}, // 🌬️ Wind Face
// --- Moon Phases ---
{"\U0001F311", new_moon, new_moon_width, new_moon_height}, // 🌑 New Moon
{"\U0001F312", waxing_crescent_moon, waxing_crescent_moon_width, waxing_crescent_moon_height}, // 🌒 Waxing Crescent Moon
{"\U0001F313", first_quarter_moon, first_quarter_moon_width, first_quarter_moon_height}, // 🌓 First Quarter Moon
{"\U0001F314", waxing_gibbous_moon, waxing_gibbous_moon_width, waxing_gibbous_moon_height}, // 🌔 Waxing Gibbous Moon
{"\U0001F315", full_moon, full_moon_width, full_moon_height}, // 🌕 Full Moon
{"\U0001F316", waning_gibbous_moon, waning_gibbous_moon_width, waning_gibbous_moon_height}, // 🌖 Waning Gibbous Moon
{"\U0001F317", last_quarter_moon, last_quarter_moon_width, last_quarter_moon_height}, // 🌗 Last Quarter Moon
{"\U0001F318", waning_crescent_moon, waning_crescent_moon_width, waning_crescent_moon_height}, // 🌘 Waning Crescent Moon
{"\U0001F31B", first_quarter_moon_face, first_quarter_moon_face_width,
first_quarter_moon_face_height}, // 🌛 First Quarter Moon Face
// --- Misc Faces ---
{"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns
@@ -67,13 +107,49 @@ const Emote emotes[] = {
{"\U0001F498", heart, heart_width, heart_height}, // 💘 Heart with Arrow
// --- Objects ---
{"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo
{"\U0001F514", bell_icon, bell_icon_width, bell_icon_height}, // 🔔 Bell
{"\U0001F36A", cookie, cookie_width, cookie_height}, // 🍪 Cookie
{"\U0001F525", Fire, Fire_width, Fire_height}, // 🔥 Fire
{"\u2728", Sparkles, Sparkles_width, Sparkles_height}, // Sparkles
{"\U0001F573\uFE0F", hole, hole_width, hole_height}, // 🕳️ Hole
{"\U0001F3B3", bowling, bowling_width, bowling_height} // 🎳 Bowling
{"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo
{"\U0001F514", bell_icon, bell_icon_width, bell_icon_height}, // 🔔 Bell
{"\U0001F4CB", clipboard, clipboard_width, clipboard_height}, // 📋 Clipboard
{"\U0001F36A", cookie, cookie_width, cookie_height}, // 🍪 Cookie
{"\U0001F370", shortcake, shortcake_width, shortcake_height}, // 🍰 Shortcake
{"\U0001F351", peach, peach_width, peach_height}, // 🍑 Peach
{"\U0001F983", turkey, turkey_width, turkey_height}, // 🦃 Turkey
{"\U0001F357", turkey_leg, turkey_leg_width, turkey_leg_height}, // 🍗 Poultry Leg
{"\U0001F525", fire, fire_width, fire_height}, // 🔥 Fire
{"\u2728", sparkles, sparkles_width, sparkles_height}, // ✨ Sparkles
{"\U0001F573\uFE0F", hole, hole_width, hole_height}, // 🕳️ Hole
{"\U0001F3B3", bowling, bowling_width, bowling_height}, // 🎳 Bowling
// --- Arrows ---
{"\u2193", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓ Downwards Arrow
{"\u2193\uFE0E", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓︎ Downwards Arrow (text)
{"\u2193\uFE0F", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓️ Downwards Arrow (emoji)
{"\u2199", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙ South West Arrow
{"\u2199\uFE0E", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙︎ South West Arrow (text)
{"\u2199\uFE0F", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙️ South West Arrow (emoji)
{"\u2190", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // ← Leftwards Arrow
{"\u2190\uFE0E", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // ←︎ Leftwards Arrow (text)
{"\u2190\uFE0F", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // ←️ Leftwards Arrow (emoji)
{"\u2196", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖ North West Arrow
{"\u2196\uFE0E", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖︎ North West Arrow (text)
{"\u2196\uFE0F", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖️ North West Arrow (emoji)
{"\u2191", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑ Upwards Arrow
{"\u2191\uFE0E", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑︎ Upwards Arrow (text)
{"\u2191\uFE0F", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑️ Upwards Arrow (emoji)
{"\u2197", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗ North East Arrow
{"\u2197\uFE0E", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗︎ North East Arrow (text)
{"\u2197\uFE0F", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗️ North East Arrow (emoji)
{"\u2192", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // → Rightwards Arrow
{"\u2192\uFE0E", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // →︎ Rightwards Arrow (text)
{"\u2192\uFE0F", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // →️ Rightwards Arrow (emoji)
{"\u2198", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘ South East Arrow
{"\u2198\uFE0E", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘︎ South East Arrow (text)
{"\u2198\uFE0F", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘️ South East Arrow (emoji)
// --- Halloween ---
{"\U0001F383", jack_o_lantern, jack_o_lantern_width, jack_o_lantern_height}, // 🎃 Jack-O-Lantern
{"\U0001F47B", ghost, ghost_width, ghost_height}, // 👻 Ghost
{"\U0001F480", skull, skull_width, skull_height} // 💀 Skull
#endif
};
@@ -88,23 +164,23 @@ const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0
0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x3F, 0x18, 0x02, 0x20, 0x02,
0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00};
const unsigned char Smiling_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
const unsigned char smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
0x4A, 0x02, 0x40, 0x02, 0x40, 0x22, 0x44, 0x22, 0x44, 0xC2, 0x43,
0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
const unsigned char grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
0x42, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Slightly_Smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
const unsigned char slightly_smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
0x42, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Winking_Face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42,
const unsigned char winking_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42,
0x46, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Grinning_Smiling_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
const unsigned char grinning_smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
0x4A, 0x02, 0x40, 0xFA, 0x5F, 0x0A, 0x50, 0x0A, 0x50, 0x12, 0x48,
0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
@@ -112,7 +188,7 @@ const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18,
0x0A, 0x02, 0xD8, 0x02, 0xF8, 0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23,
0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00};
const unsigned char Heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA,
const unsigned char heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA,
0x5F, 0x72, 0x4E, 0x22, 0x44, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48,
0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
@@ -128,19 +204,19 @@ const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04,
0x4A, 0x0A, 0x50, 0x0E, 0x70, 0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char ROFL[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02,
const unsigned char rofl[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02,
0x4C, 0x02, 0x4A, 0x1A, 0x49, 0x8A, 0x48, 0x42, 0x48, 0x22, 0x44,
0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Smiling_Closed_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42,
const unsigned char smiling_closed_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42,
0x42, 0x22, 0x44, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Grinning_SmilingEyes2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char grinning_smiling_eyes_2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Loudly_Crying_Face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A,
const unsigned char loudly_crying_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A,
0x52, 0x12, 0x48, 0x12, 0x48, 0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A,
0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00};
@@ -192,7 +268,7 @@ const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04
0x40, 0x02, 0x58, 0x82, 0x5B, 0x92, 0x43, 0x82, 0x43, 0x02, 0x40,
0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC,
const unsigned char fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC,
0x1F, 0xFE, 0x3E, 0x7E, 0x3E, 0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70,
0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C};
@@ -200,11 +276,11 @@ const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25,
0x0A, 0x54, 0x68, 0x54, 0x58, 0x54, 0x44, 0x3C, 0x22, 0x04, 0x22,
0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90,
const unsigned char praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90,
0x09, 0x98, 0x19, 0x94, 0x29, 0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21,
0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC};
const unsigned char Sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00,
const unsigned char sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00,
0x1F, 0x80, 0x3F, 0xE0, 0xFF, 0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E,
0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00};
@@ -227,7 +303,179 @@ const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x0
const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A,
0x0A, 0x02, 0x38, 0x04, 0x48, 0x04, 0x44, 0x04, 0x22, 0x04, 0x22,
0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00};
const unsigned char jack_o_lantern[] PROGMEM = {0xC0, 0x00, 0x80, 0x01, 0xB8, 0x1D, 0xC4, 0x23, 0x22, 0x44, 0x05,
0xA0, 0x31, 0x8C, 0x51, 0x8A, 0x61, 0x86, 0x09, 0x90, 0xB9, 0x9D,
0x49, 0x92, 0xB2, 0x4D, 0x42, 0x42, 0x04, 0x20, 0xF8, 0x1F};
const unsigned char ghost[] PROGMEM = {0xC0, 0x03, 0xF0, 0x0F, 0xF8, 0x1F, 0xDC, 0x3B, 0xBC, 0x3D, 0xDF,
0xFB, 0xFF, 0xFF, 0x1F, 0xF8, 0x1E, 0x78, 0x1C, 0x38, 0x3C, 0x3C,
0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0x8C, 0x31};
const unsigned char skull[] PROGMEM = {0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xC7,
0xE3, 0x87, 0xE1, 0x87, 0xE1, 0x8F, 0xF1, 0xFE, 0x7F, 0x7C, 0x3E,
0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xB0, 0x0D};
const unsigned char vomiting[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x22,
0x44, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, 0x02, 0x40, 0xC2, 0x43,
0x64, 0x26, 0x64, 0x26, 0x68, 0x16, 0x50, 0x0A, 0xF8, 0x1F};
const unsigned char cool[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0xFC, 0x3F, 0xFA,
0x5F, 0x72, 0x4E, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char shortcake[] PROGMEM = {0x00, 0x00, 0x00, 0x0F, 0x80, 0x3F, 0xE0, 0xFC, 0xE0, 0xE1, 0xF0,
0xB8, 0x10, 0x87, 0xC8, 0x80, 0x3C, 0xE0, 0x06, 0x98, 0x02, 0xC7,
0xE2, 0x30, 0x1A, 0x0E, 0xC6, 0x01, 0x32, 0x00, 0x0E, 0x00};
const unsigned char caution[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60,
0x06, 0x70, 0x0E, 0x70, 0x0E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E,
0xFC, 0x3F, 0x7E, 0x7E, 0x7E, 0x7E, 0xFC, 0x3F, 0x00, 0x00};
const unsigned char clipboard[] PROGMEM = {0xC0, 0x03, 0x7E, 0x7E, 0xC2, 0x43, 0xFA, 0x5F, 0x0A, 0x5B, 0xFA,
0x5F, 0x8A, 0x54, 0xFA, 0x5F, 0x4A, 0x58, 0xFA, 0x5F, 0x2A, 0x51,
0xFA, 0x5F, 0x0A, 0x59, 0xFA, 0x5F, 0x02, 0x40, 0xFE, 0x7F};
const unsigned char snowflake[] PROGMEM = {0x00, 0x00, 0x40, 0x01, 0x88, 0x08, 0x8C, 0x18, 0xD0, 0x05, 0x60,
0x03, 0x32, 0x26, 0x1C, 0x1C, 0x32, 0x26, 0x60, 0x03, 0xD0, 0x05,
0x8C, 0x18, 0x88, 0x08, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00};
const unsigned char drop[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0,
0x0F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F,
0xF8, 0x3F, 0xF0, 0x1F, 0xE0, 0x0F, 0x80, 0x03, 0x00, 0x00};
const unsigned char thermometer[] PROGMEM = {0x00, 0x00, 0x0C, 0x00, 0x16, 0x00, 0x2E, 0x00, 0x5C, 0x00, 0xB8,
0x00, 0x70, 0x01, 0xE0, 0x02, 0xC0, 0x05, 0x80, 0x3B, 0x00, 0x47,
0x00, 0xBE, 0x00, 0x9E, 0x00, 0xBE, 0x00, 0x7C, 0x00, 0x38};
const unsigned char sun_behind_raincloud[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x0E, 0x38, 0x1F, 0xFC, 0x37, 0xEE,
0x77, 0xDE, 0x7B, 0x3E, 0x7C, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12,
0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00};
const unsigned char sun_behind_cloud[] PROGMEM = {0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x04, 0x0E, 0x3C, 0x1B, 0xFC,
0x3B, 0xFE, 0x7B, 0xFA, 0x7B, 0xF6, 0x7D, 0x0C, 0x3E, 0xF8, 0x1F,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
const unsigned char cloud_with_snow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE,
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x08, 0x02,
0x40, 0x10, 0x00, 0x00, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00};
const unsigned char cloud_with_lightning[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE,
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x80, 0x01,
0x80, 0x01, 0xC0, 0x07, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01};
const unsigned char cloud_with_lightning_rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE,
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x90, 0x21,
0x90, 0x21, 0xC8, 0x17, 0x08, 0x13, 0x00, 0x03, 0x00, 0x01};
const unsigned char wind_face[] PROGMEM = {0xFF, 0x00, 0x01, 0x01, 0x01, 0x01, 0xF9, 0x00, 0xF9, 0x01, 0xD9,
0x01, 0x99, 0x01, 0xF9, 0x01, 0xF9, 0x33, 0xFD, 0x4B, 0xFD, 0x85,
0xFD, 0x9A, 0xFD, 0x75, 0xFD, 0x09, 0xFD, 0x01, 0xFF, 0x00};
const unsigned char new_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x02,
0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40,
0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char waxing_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3E, 0x04, 0x3C, 0x02,
0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78,
0x04, 0x3C, 0x04, 0x3E, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
const unsigned char first_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3F, 0x04, 0x3F, 0x02,
0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F,
0x04, 0x3F, 0x04, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
const unsigned char waxing_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x84, 0x3F, 0xC4, 0x3F, 0xC2,
0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F,
0xC4, 0x3F, 0x84, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
const unsigned char full_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE,
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F,
0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00};
const unsigned char waning_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x21, 0xFC, 0x23, 0xFE,
0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43,
0xFC, 0x23, 0xFC, 0x21, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char last_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x20, 0xFC, 0x20, 0xFE,
0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40,
0xFC, 0x20, 0xFC, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char waning_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0x7C, 0x20, 0x3C, 0x20, 0x1E,
0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40,
0x3C, 0x20, 0x7C, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char first_quarter_moon_face[] PROGMEM = {0x00, 0x0F, 0x00, 0x12, 0x00, 0x24, 0x00, 0x44, 0x00, 0x48, 0x00,
0x88, 0x00, 0x84, 0x80, 0x93, 0x80, 0x80, 0x03, 0x81, 0x8D, 0x80,
0x71, 0x40, 0x82, 0x41, 0x02, 0x20, 0x0C, 0x18, 0xF0, 0x07};
const unsigned char peach[] PROGMEM = {0x70, 0x0F, 0x88, 0x10, 0x78, 0x1F, 0x88, 0x11, 0x04, 0x22, 0x02,
0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x42, 0x02, 0x40,
0x04, 0x20, 0x04, 0x20, 0x08, 0x10, 0x30, 0x0C, 0xC0, 0x03};
const unsigned char turkey[] PROGMEM = {0x00, 0x00, 0x38, 0x00, 0x44, 0x38, 0x56, 0x54, 0x45, 0x52, 0xE2,
0x21, 0x2C, 0x56, 0x14, 0x58, 0x0A, 0x37, 0x86, 0x68, 0x82, 0x50,
0x82, 0x20, 0x04, 0x41, 0xF8, 0x7F, 0x40, 0x02, 0xF0, 0x07};
const unsigned char turkey_leg[] PROGMEM = {0x0C, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x46, 0x00, 0x88,
0x01, 0x10, 0x0E, 0x20, 0x30, 0x20, 0x40, 0x40, 0x40, 0x40, 0x80,
0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x43, 0x00, 0x3C};
const unsigned char south_west_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x3E, 0x00,
0x1F, 0x80, 0x0F, 0xC2, 0x07, 0xE6, 0x03, 0xFE, 0x01, 0xFE, 0x00,
0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0x00, 0x00};
const unsigned char south_east_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xF8,
0x00, 0xF0, 0x01, 0xE0, 0x43, 0xC0, 0x67, 0x80, 0x7F, 0x00, 0x7F,
0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x00};
const unsigned char north_west_arrow[] PROGMEM = {0x00, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE,
0x00, 0xFE, 0x01, 0xE6, 0x03, 0xC2, 0x07, 0x80, 0x0F, 0x00, 0x1F,
0x00, 0x3E, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00};
const unsigned char north_east_arrow[] PROGMEM = {0x00, 0x00, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00,
0x7F, 0x80, 0x7F, 0xC0, 0x67, 0xE0, 0x43, 0xF0, 0x01, 0xF8, 0x00,
0x7C, 0x00, 0x38, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00};
const unsigned char downwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0,
0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xFC, 0x3F,
0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01};
const unsigned char leftwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3C,
0x00, 0xFE, 0x3F, 0xFF, 0x3F, 0xFF, 0x3F, 0xFE, 0x3F, 0x3C, 0x00,
0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00};
const unsigned char upwards_arrow[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC,
0x3F, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03,
0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00};
const unsigned char rightwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x1C, 0x00,
0x3C, 0xFC, 0x7F, 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0x7F, 0x00, 0x3C,
0x00, 0x1C, 0x00, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00};
const unsigned char strong[] PROGMEM = {0x38, 0x00, 0x44, 0x00, 0x62, 0x00, 0x42, 0x00, 0x42, 0x00, 0x3A,
0x00, 0x11, 0x3C, 0x11, 0x42, 0xD1, 0x81, 0x31, 0x82, 0x11, 0x82,
0x21, 0x80, 0x01, 0x80, 0x01, 0x80, 0x02, 0x40, 0xFC, 0x3F};
const unsigned char check_mark[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x3C, 0x00,
0x1E, 0x00, 0x0F, 0x80, 0x07, 0xC3, 0x03, 0xEE, 0x03, 0xFC, 0x01,
0xF8, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x60, 0x00, 0x20, 0x00};
const unsigned char house[] PROGMEM = {0x80, 0x01, 0x5C, 0x02, 0x34, 0x04, 0x14, 0x08, 0x0C, 0x10, 0x04,
0x20, 0x02, 0x40, 0xFF, 0xFF, 0x02, 0x40, 0x7A, 0x5F, 0x4A, 0x55,
0x4A, 0x5F, 0x6A, 0x55, 0x4A, 0x5F, 0x4A, 0x40, 0xFE, 0x7F};
const unsigned char shrug[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x50, 0x0A, 0x10, 0x08, 0x90,
0x09, 0x27, 0xE4, 0x49, 0x92, 0xAA, 0x55, 0x16, 0x68, 0x12, 0x48,
0x02, 0x40, 0x02, 0x40, 0x0C, 0x30, 0x08, 0x10, 0xF8, 0x1F};
const unsigned char eyes[] PROGMEM = {0x00, 0x00, 0x3C, 0x3C, 0x42, 0x42, 0x81, 0x81, 0x85, 0x85, 0x8F,
0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F,
0x85, 0x85, 0x81, 0x81, 0x42, 0x42, 0x3C, 0x3C, 0x00, 0x00};
const unsigned char eye[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xF4,
0x2F, 0x7A, 0x5E, 0x39, 0x9C, 0x39, 0x9C, 0x7A, 0x5E, 0xF4, 0x2F,
0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
#endif
} // namespace graphics
#endif
#endif

View File

@@ -22,33 +22,33 @@ extern const int numEmotes;
extern const unsigned char thumbup[] PROGMEM;
extern const unsigned char thumbdown[] PROGMEM;
#define Smiling_Eyes_height 16
#define Smiling_Eyes_width 16
extern const unsigned char Smiling_Eyes[] PROGMEM;
#define smiling_eyes_height 16
#define smiling_eyes_width 16
extern const unsigned char smiling_eyes[] PROGMEM;
#define Grinning_height 16
#define Grinning_width 16
extern const unsigned char Grinning[] PROGMEM;
#define grinning_height 16
#define grinning_width 16
extern const unsigned char grinning[] PROGMEM;
#define Slightly_Smiling_height 16
#define Slightly_Smiling_width 16
extern const unsigned char Slightly_Smiling[] PROGMEM;
#define slightly_smiling_height 16
#define slightly_smiling_width 16
extern const unsigned char slightly_smiling[] PROGMEM;
#define Winking_Face_height 16
#define Winking_Face_width 16
extern const unsigned char Winking_Face[] PROGMEM;
#define winking_face_height 16
#define winking_face_width 16
extern const unsigned char winking_face[] PROGMEM;
#define Grinning_Smiling_Eyes_height 16
#define Grinning_Smiling_Eyes_width 16
extern const unsigned char Grinning_Smiling_Eyes[] PROGMEM;
#define grinning_smiling_eyes_height 16
#define grinning_smiling_eyes_width 16
extern const unsigned char grinning_smiling_eyes[] PROGMEM;
#define heart_smile_height 16
#define heart_smile_width 16
extern const unsigned char heart_smile[] PROGMEM;
#define Heart_eyes_height 16
#define Heart_eyes_width 16
extern const unsigned char Heart_eyes[] PROGMEM;
#define heart_eyes_height 16
#define heart_eyes_width 16
extern const unsigned char heart_eyes[] PROGMEM;
#define question_height 16
#define question_width 16
@@ -62,21 +62,21 @@ extern const unsigned char bang[] PROGMEM;
#define haha_width 16
extern const unsigned char haha[] PROGMEM;
#define ROFL_height 16
#define ROFL_width 16
extern const unsigned char ROFL[] PROGMEM;
#define rofl_height 16
#define rofl_width 16
extern const unsigned char rofl[] PROGMEM;
#define Smiling_Closed_Eyes_height 16
#define Smiling_Closed_Eyes_width 16
extern const unsigned char Smiling_Closed_Eyes[] PROGMEM;
#define smiling_closed_eyes_height 16
#define smiling_closed_eyes_width 16
extern const unsigned char smiling_closed_eyes[] PROGMEM;
#define Grinning_SmilingEyes2_height 16
#define Grinning_SmilingEyes2_width 16
extern const unsigned char Grinning_SmilingEyes2[] PROGMEM;
#define grinning_smiling_eyes_2_height 16
#define grinning_smiling_eyes_2_width 16
extern const unsigned char grinning_smiling_eyes_2[] PROGMEM;
#define Loudly_Crying_Face_height 16
#define Loudly_Crying_Face_width 16
extern const unsigned char Loudly_Crying_Face[] PROGMEM;
#define loudly_crying_face_height 16
#define loudly_crying_face_width 16
extern const unsigned char loudly_crying_face[] PROGMEM;
#define wave_icon_height 16
#define wave_icon_width 16
@@ -126,21 +126,21 @@ extern const unsigned char bell_icon[] PROGMEM;
#define cookie_height 16
extern const unsigned char cookie[] PROGMEM;
#define Fire_width 16
#define Fire_height 16
extern const unsigned char Fire[] PROGMEM;
#define fire_width 16
#define fire_height 16
extern const unsigned char fire[] PROGMEM;
#define peace_sign_width 16
#define peace_sign_height 16
extern const unsigned char peace_sign[] PROGMEM;
#define Praying_width 16
#define Praying_height 16
extern const unsigned char Praying[] PROGMEM;
#define praying_width 16
#define praying_height 16
extern const unsigned char praying[] PROGMEM;
#define Sparkles_width 16
#define Sparkles_height 16
extern const unsigned char Sparkles[] PROGMEM;
#define sparkles_width 16
#define sparkles_height 16
extern const unsigned char sparkles[] PROGMEM;
#define clown_width 16
#define clown_height 16
@@ -161,6 +161,178 @@ extern const unsigned char bowling[] PROGMEM;
#define vulcan_salute_width 16
#define vulcan_salute_height 16
extern const unsigned char vulcan_salute[] PROGMEM;
#define jack_o_lantern_width 16
#define jack_o_lantern_height 16
extern const unsigned char jack_o_lantern[] PROGMEM;
#define ghost_width 16
#define ghost_height 16
extern const unsigned char ghost[] PROGMEM;
#define skull_width 16
#define skull_height 16
extern const unsigned char skull[] PROGMEM;
#define vomiting_width 16
#define vomiting_height 16
extern const unsigned char vomiting[] PROGMEM;
#define cool_width 16
#define cool_height 16
extern const unsigned char cool[] PROGMEM;
#define shortcake_width 16
#define shortcake_height 16
extern const unsigned char shortcake[] PROGMEM;
#define caution_width 16
#define caution_height 16
extern const unsigned char caution[] PROGMEM;
#define clipboard_width 16
#define clipboard_height 16
extern const unsigned char clipboard[] PROGMEM;
#define snowflake_width 16
#define snowflake_height 16
extern const unsigned char snowflake[] PROGMEM;
#define drop_width 16
#define drop_height 16
extern const unsigned char drop[] PROGMEM;
#define thermometer_width 16
#define thermometer_height 16
extern const unsigned char thermometer[] PROGMEM;
#define sun_behind_raincloud_width 16
#define sun_behind_raincloud_height 16
extern const unsigned char sun_behind_raincloud[] PROGMEM;
#define sun_behind_cloud_width 16
#define sun_behind_cloud_height 16
extern const unsigned char sun_behind_cloud[] PROGMEM;
#define cloud_with_snow_width 16
#define cloud_with_snow_height 16
extern const unsigned char cloud_with_snow[] PROGMEM;
#define cloud_with_lightning_width 16
#define cloud_with_lightning_height 16
extern const unsigned char cloud_with_lightning[] PROGMEM;
#define cloud_with_lightning_rain_width 16
#define cloud_with_lightning_rain_height 16
extern const unsigned char cloud_with_lightning_rain[] PROGMEM;
#define wind_face_width 16
#define wind_face_height 16
extern const unsigned char wind_face[] PROGMEM;
#define new_moon_width 16
#define new_moon_height 16
extern const unsigned char new_moon[] PROGMEM;
#define waxing_crescent_moon_width 16
#define waxing_crescent_moon_height 16
extern const unsigned char waxing_crescent_moon[] PROGMEM;
#define first_quarter_moon_width 16
#define first_quarter_moon_height 16
extern const unsigned char first_quarter_moon[] PROGMEM;
#define waxing_gibbous_moon_width 16
#define waxing_gibbous_moon_height 16
extern const unsigned char waxing_gibbous_moon[] PROGMEM;
#define full_moon_width 16
#define full_moon_height 16
extern const unsigned char full_moon[] PROGMEM;
#define waning_gibbous_moon_width 16
#define waning_gibbous_moon_height 16
extern const unsigned char waning_gibbous_moon[] PROGMEM;
#define last_quarter_moon_width 16
#define last_quarter_moon_height 16
extern const unsigned char last_quarter_moon[] PROGMEM;
#define waning_crescent_moon_width 16
#define waning_crescent_moon_height 16
extern const unsigned char waning_crescent_moon[] PROGMEM;
#define first_quarter_moon_face_width 16
#define first_quarter_moon_face_height 16
extern const unsigned char first_quarter_moon_face[] PROGMEM;
#define peach_width 16
#define peach_height 16
extern const unsigned char peach[] PROGMEM;
#define turkey_width 16
#define turkey_height 16
extern const unsigned char turkey[] PROGMEM;
#define turkey_leg_width 16
#define turkey_leg_height 16
extern const unsigned char turkey_leg[] PROGMEM;
#define south_west_arrow_width 16
#define south_west_arrow_height 16
extern const unsigned char south_west_arrow[] PROGMEM;
#define south_east_arrow_width 16
#define south_east_arrow_height 16
extern const unsigned char south_east_arrow[] PROGMEM;
#define north_west_arrow_width 16
#define north_west_arrow_height 16
extern const unsigned char north_west_arrow[] PROGMEM;
#define north_east_arrow_width 16
#define north_east_arrow_height 16
extern const unsigned char north_east_arrow[] PROGMEM;
#define downwards_arrow_width 16
#define downwards_arrow_height 16
extern const unsigned char downwards_arrow[] PROGMEM;
#define leftwards_arrow_width 16
#define leftwards_arrow_height 16
extern const unsigned char leftwards_arrow[] PROGMEM;
#define upwards_arrow_width 16
#define upwards_arrow_height 16
extern const unsigned char upwards_arrow[] PROGMEM;
#define rightwards_arrow_width 16
#define rightwards_arrow_height 16
extern const unsigned char rightwards_arrow[] PROGMEM;
#define strong_width 16
#define strong_height 16
extern const unsigned char strong[] PROGMEM;
#define check_mark_width 16
#define check_mark_height 16
extern const unsigned char check_mark[] PROGMEM;
#define house_width 16
#define house_height 16
extern const unsigned char house[] PROGMEM;
#define shrug_width 16
#define shrug_height 16
extern const unsigned char shrug[] PROGMEM;
#define eyes_width 16
#define eyes_height 16
extern const unsigned char eyes[] PROGMEM;
#define eye_width 16
#define eye_height 16
extern const unsigned char eye[] PROGMEM;
#endif // EXCLUDE_EMOJI
} // namespace graphics
} // namespace graphics

View File

@@ -304,58 +304,6 @@ const uint8_t chirpy[] = {
0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01,
0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf};
#define chirpy_width_hirez 76
#define chirpy_height_hirez 100
const uint8_t chirpy_hirez[] = {
0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00,
0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc,
0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03,
0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0,
0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff,
0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f,
0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0,
0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff,
0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00,
0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc,
0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03,
0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0,
0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff,
0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f,
0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc,
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03,
0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0,
0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00,
0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f,
0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c,
0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00,
0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00,
0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc,
0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03,
0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00,
0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0,
0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3};
#define chirpy_small_image_width 8
#define chirpy_small_image_height 8
const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};

View File

@@ -88,8 +88,14 @@ class Applet : public GFX
virtual void onForeground() {}
virtual void onBackground() {}
virtual void onShutdown() {}
virtual void onButtonShortPress() {} // (System Applets only)
virtual void onButtonLongPress() {} // (System Applets only)
virtual void onButtonShortPress() {}
virtual void onButtonLongPress() {}
virtual void onExitShort() {}
virtual void onExitLong() {}
virtual void onNavUp() {}
virtual void onNavDown() {}
virtual void onNavLeft() {}
virtual void onNavRight() {}
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification

View File

@@ -0,0 +1,205 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./AlignStickApplet.h"
using namespace NicheGraphics;
InkHUD::AlignStickApplet::AlignStickApplet()
{
if (!settings->joystick.aligned)
bringToForeground();
}
void InkHUD::AlignStickApplet::onRender()
{
setFont(fontMedium);
printAt(0, 0, "Align Joystick:");
setFont(fontSmall);
std::string instructions = "Move joystick in the direction indicated";
printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions);
// Size of the region in which the joystick graphic should fit
uint16_t joyXLimit = X(0.8);
uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1;
if (getTextWidth(instructions) > width())
contentH += fontSmall.lineHeight();
uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2;
uint16_t joyYLimit = freeY * 0.8;
// Use the shorter of the two
uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit;
// Center the joystick graphic
uint16_t centerX = X(0.5);
uint16_t centerY = contentH + freeY * 0.5;
// Draw joystick graphic
drawStick(centerX, centerY, joyWidth);
setFont(fontSmall);
printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM);
}
// Draw a scalable joystick graphic
void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width)
{
if (width < 9) // too small to draw
return;
else if (width < 40) { // only draw up arrow
uint16_t chamfer = width < 20 ? 1 : 2;
// Draw filled up arrow
drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK);
} else { // large enough to draw the full thing
uint16_t chamfer = width < 80 ? 1 : 2;
uint16_t stroke = 3; // pixels
uint16_t arrowW = width * 0.22;
uint16_t hollowW = arrowW - stroke * 2;
// Draw center circle
fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK);
fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE);
// Draw filled up arrow
drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK);
// Draw down arrow
drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK);
drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE);
// Draw left arrow
drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK);
drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE);
// Draw right arrow
drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK);
drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE);
}
}
// Draw a scalable joystick direction arrow
// a right-triangle with blunted tips
/*
_ <--point
^ / \
| / \
size / \
| / \
v |_________|
*/
void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size,
uint16_t chamfer, Color color)
{
uint16_t chamferW = chamfer * 2 + 1;
uint16_t triangleW = size - chamferW;
// Draw arrow
switch (direction) {
case Direction::UP:
fillRect(pointX - chamfer, pointY, chamferW, triangleW, color);
fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color);
fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer,
pointY + triangleW, color);
fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer,
pointY + triangleW, color);
break;
case Direction::DOWN:
fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color);
fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color);
fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer,
pointY - triangleW, color);
fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer,
pointY - triangleW, color);
break;
case Direction::LEFT:
fillRect(pointX, pointY - chamfer, triangleW, chamferW, color);
fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color);
fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW,
pointY - chamfer, color);
fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW,
pointY + chamfer, color);
break;
case Direction::RIGHT:
fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color);
fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color);
fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW,
pointY - chamfer, color);
fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW,
pointY + chamfer, color);
break;
}
}
void InkHUD::AlignStickApplet::onForeground()
{
// Prevent most other applets from requesting update, and skip their rendering entirely
// Another system applet with a higher precedence can potentially ignore this
SystemApplet::lockRendering = true;
SystemApplet::lockRequests = true;
handleInput = true; // Intercept the button input for our applet
}
void InkHUD::AlignStickApplet::onBackground()
{
// Allow normal update behavior to resume
SystemApplet::lockRendering = false;
SystemApplet::lockRequests = false;
SystemApplet::handleInput = false;
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onButtonLongPress()
{
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onExitLong()
{
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavUp()
{
settings->joystick.aligned = true;
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavDown()
{
inkhud->rotateJoystick(2); // 180 deg
settings->joystick.aligned = true;
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavLeft()
{
inkhud->rotateJoystick(3); // 270 deg
settings->joystick.aligned = true;
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavRight()
{
inkhud->rotateJoystick(1); // 90 deg
settings->joystick.aligned = true;
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
#endif

View File

@@ -0,0 +1,50 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
System Applet for manually aligning the joystick with the screen
should be run at startup if the joystick is enabled
and not aligned to the screen
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
namespace NicheGraphics::InkHUD
{
class AlignStickApplet : public SystemApplet
{
public:
AlignStickApplet();
void onRender() override;
void onForeground() override;
void onBackground() override;
void onButtonLongPress() override;
void onExitLong() override;
void onNavUp() override;
void onNavDown() override;
void onNavLeft() override;
void onNavRight() override;
protected:
enum Direction {
UP,
DOWN,
LEFT,
RIGHT,
};
void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width);
void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color);
};
} // namespace NicheGraphics::InkHUD
#endif

View File

@@ -30,6 +30,7 @@ enum MenuAction {
TOGGLE_AUTOSHOW_APPLET,
SET_RECENTS,
ROTATE,
ALIGN_JOYSTICK,
LAYOUT,
TOGGLE_BATTERY_ICON,
TOGGLE_NOTIFICATIONS,

View File

@@ -178,6 +178,10 @@ void InkHUD::MenuApplet::execute(MenuItem item)
inkhud->rotate();
break;
case ALIGN_JOYSTICK:
inkhud->openAlignStick();
break;
case LAYOUT:
// Todo: smarter incrementing of tile count
settings->userTiles.count++;
@@ -287,14 +291,17 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
// items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO
items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::EXIT;
break;
case SEND:
populateSendPage();
previousPage = MenuPage::ROOT;
break;
case CANNEDMESSAGE_RECIPIENT:
populateRecipientPage();
previousPage = MenuPage::OPTIONS;
break;
case OPTIONS:
@@ -321,6 +328,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
if (settings->userTiles.maxCount > 1)
items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS));
items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS));
if (settings->joystick.enabled)
items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT));
items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS,
&settings->optionalFeatures.notifications));
items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS,
@@ -332,20 +341,24 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
items.push_back(
MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::ROOT;
break;
case APPLETS:
populateAppletPage();
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::OPTIONS;
break;
case AUTOSHOW:
populateAutoshowPage();
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::OPTIONS;
break;
case RECENTS:
populateRecentsPage();
previousPage = MenuPage::OPTIONS;
break;
case EXIT:
@@ -479,12 +492,21 @@ void InkHUD::MenuApplet::onButtonShortPress()
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
// Move menu cursor to next entry, then update
if (cursorShown)
cursor = (cursor + 1) % items.size();
else
cursorShown = true;
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
if (!settings->joystick.enabled) {
// Move menu cursor to next entry, then update
if (cursorShown)
cursor = (cursor + 1) % items.size();
else
cursorShown = true;
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
} else {
if (cursorShown)
execute(items.at(cursor));
else
showPage(MenuPage::EXIT);
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
}
void InkHUD::MenuApplet::onButtonLongPress()
@@ -504,6 +526,62 @@ void InkHUD::MenuApplet::onButtonLongPress()
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onExitShort()
{
// Exit the menu
showPage(MenuPage::EXIT);
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onNavUp()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
// Move menu cursor to previous entry, then update
if (cursor == 0)
cursor = items.size() - 1;
else
cursor--;
if (!cursorShown)
cursorShown = true;
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onNavDown()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
// Move menu cursor to next entry, then update
if (cursorShown)
cursor = (cursor + 1) % items.size();
else
cursorShown = true;
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onNavLeft()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
// Go to the previous menu page
showPage(previousPage);
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onNavRight()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (cursorShown)
execute(items.at(cursor));
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
void InkHUD::MenuApplet::populateAppletPage()
{
@@ -796,4 +874,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources()
cm.recipientItems.clear();
}
#endif
#endif

View File

@@ -27,6 +27,11 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void onBackground() override;
void onButtonShortPress() override;
void onButtonLongPress() override;
void onExitShort() override;
void onNavUp() override;
void onNavDown() override;
void onNavLeft() override;
void onNavRight() override;
void onRender() override;
void show(Tile *t); // Open the menu, onto a user tile
@@ -52,6 +57,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data
MenuPage currentPage = MenuPage::ROOT;
MenuPage previousPage = MenuPage::EXIT;
uint8_t cursor = 0; // Which menu item is currently highlighted
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
@@ -97,4 +103,4 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
} // namespace NicheGraphics::InkHUD
#endif
#endif

View File

@@ -153,6 +153,42 @@ void InkHUD::NotificationApplet::onButtonLongPress()
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onExitShort()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onExitLong()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onNavUp()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onNavDown()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onNavLeft()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onNavRight()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
// Called internally when we first get a "notifiable event", and then again before render,
// in case autoshow swapped which applet was displayed

View File

@@ -31,6 +31,12 @@ class NotificationApplet : public SystemApplet
void onBackground() override;
void onButtonShortPress() override;
void onButtonLongPress() override;
void onExitShort() override;
void onExitLong() override;
void onNavUp() override;
void onNavDown() override;
void onNavLeft() override;
void onNavRight() override;
int onReceiveTextMessage(const meshtastic_MeshPacket *p);

View File

@@ -112,12 +112,21 @@ void InkHUD::TipsApplet::onRender()
setFont(fontSmall);
int16_t cursorY = fontMedium.lineHeight() * 1.5;
printAt(0, cursorY, "User Button");
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- short press: next");
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- long press: select / open menu");
cursorY += fontSmall.lineHeight() * 1.5;
if (!settings->joystick.enabled) {
printAt(0, cursorY, "User Button");
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- short press: next");
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- long press: select / open menu");
} else {
printAt(0, cursorY, "Joystick");
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- open menu / select");
cursorY += fontSmall.lineHeight() * 1.5;
printAt(0, cursorY, "Exit Button");
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- switch tile / close menu");
}
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
} break;
@@ -127,8 +136,13 @@ void InkHUD::TipsApplet::onRender()
printAt(0, 0, "Tip: Rotation");
setFont(fontSmall);
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
"To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate.");
if (!settings->joystick.enabled) {
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
"To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate.");
} else {
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
"To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate.");
}
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
@@ -232,4 +246,10 @@ void InkHUD::TipsApplet::onButtonShortPress()
requestUpdate();
}
// Functions the same as the user button in this instance
void InkHUD::TipsApplet::onExitShort()
{
onButtonShortPress();
}
#endif

View File

@@ -36,6 +36,7 @@ class TipsApplet : public SystemApplet
void onForeground() override;
void onBackground() override;
void onButtonShortPress() override;
void onExitShort() override;
protected:
void renderWelcome(); // Very first screen of tutorial

View File

@@ -55,10 +55,15 @@ void InkHUD::Events::onButtonShort()
}
// If no system applet is handling input, default behavior instead is to cycle applets
if (consumer)
// or open menu if joystick is enabled
if (consumer) {
consumer->onButtonShortPress();
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
inkhud->nextApplet();
} else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
if (!settings->joystick.enabled)
inkhud->nextApplet();
else
inkhud->openMenu();
}
}
void InkHUD::Events::onButtonLong()
@@ -83,6 +88,156 @@ void InkHUD::Events::onButtonLong()
inkhud->openMenu();
}
void InkHUD::Events::onExitShort()
{
if (settings->joystick.enabled) {
// Audio feedback (via buzzer)
// Short tone
playChirp();
// Cancel any beeping, buzzing, blinking
// Some button handling suppressed if we are dismissing an external notification (see below)
bool dismissedExt = dismissExternalNotification();
// Check which system applet wants to handle the button press (if any)
SystemApplet *consumer = nullptr;
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleInput) {
consumer = sa;
break;
}
}
// If no system applet is handling input, default behavior instead is change tiles
if (consumer)
consumer->onExitShort();
else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module
inkhud->nextTile();
}
}
void InkHUD::Events::onExitLong()
{
if (settings->joystick.enabled) {
// Audio feedback (via buzzer)
// Slightly longer than playChirp
playBoop();
// Check which system applet wants to handle the button press (if any)
SystemApplet *consumer = nullptr;
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleInput) {
consumer = sa;
break;
}
}
if (consumer)
consumer->onExitLong();
}
}
void InkHUD::Events::onNavUp()
{
if (settings->joystick.enabled) {
// Audio feedback (via buzzer)
// Short tone
playChirp();
// Cancel any beeping, buzzing, blinking
// Some button handling suppressed if we are dismissing an external notification (see below)
bool dismissedExt = dismissExternalNotification();
// Check which system applet wants to handle the button press (if any)
SystemApplet *consumer = nullptr;
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleInput) {
consumer = sa;
break;
}
}
if (consumer)
consumer->onNavUp();
}
}
void InkHUD::Events::onNavDown()
{
if (settings->joystick.enabled) {
// Audio feedback (via buzzer)
// Short tone
playChirp();
// Cancel any beeping, buzzing, blinking
// Some button handling suppressed if we are dismissing an external notification (see below)
bool dismissedExt = dismissExternalNotification();
// Check which system applet wants to handle the button press (if any)
SystemApplet *consumer = nullptr;
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleInput) {
consumer = sa;
break;
}
}
if (consumer)
consumer->onNavDown();
}
}
void InkHUD::Events::onNavLeft()
{
if (settings->joystick.enabled) {
// Audio feedback (via buzzer)
// Short tone
playChirp();
// Cancel any beeping, buzzing, blinking
// Some button handling suppressed if we are dismissing an external notification (see below)
bool dismissedExt = dismissExternalNotification();
// Check which system applet wants to handle the button press (if any)
SystemApplet *consumer = nullptr;
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleInput) {
consumer = sa;
break;
}
}
// If no system applet is handling input, default behavior instead is to cycle applets
if (consumer)
consumer->onNavLeft();
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
inkhud->prevApplet();
}
}
void InkHUD::Events::onNavRight()
{
if (settings->joystick.enabled) {
// Audio feedback (via buzzer)
// Short tone
playChirp();
// Cancel any beeping, buzzing, blinking
// Some button handling suppressed if we are dismissing an external notification (see below)
bool dismissedExt = dismissExternalNotification();
// Check which system applet wants to handle the button press (if any)
SystemApplet *consumer = nullptr;
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleInput) {
consumer = sa;
break;
}
}
// If no system applet is handling input, default behavior instead is to cycle applets
if (consumer)
consumer->onNavRight();
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
inkhud->nextApplet();
}
}
// Callback for deepSleepObserver
// Returns 0 to signal that we agree to sleep now
int InkHUD::Events::beforeDeepSleep(void *unused)

View File

@@ -29,6 +29,12 @@ class Events
void onButtonShort(); // User button: short press
void onButtonLong(); // User button: long press
void onExitShort(); // Exit button: short press
void onExitLong(); // Exit button: long press
void onNavUp(); // Navigate up
void onNavDown(); // Navigate down
void onNavLeft(); // Navigate left
void onNavRight(); // Navigate right
int beforeDeepSleep(void *unused); // Prepare for shutdown
int beforeReboot(void *unused); // Prepare for reboot

View File

@@ -80,6 +80,94 @@ void InkHUD::InkHUD::longpress()
events->onButtonLong();
}
// Call this when your exit button gets a short press
void InkHUD::InkHUD::exitShort()
{
events->onExitShort();
}
// Call this when your exit button gets a long press
void InkHUD::InkHUD::exitLong()
{
events->onExitLong();
}
// Call this when your joystick gets an up input
void InkHUD::InkHUD::navUp()
{
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
case 1: // 90 deg
events->onNavLeft();
break;
case 2: // 180 deg
events->onNavDown();
break;
case 3: // 270 deg
events->onNavRight();
break;
default: // 0 deg
events->onNavUp();
break;
}
}
// Call this when your joystick gets a down input
void InkHUD::InkHUD::navDown()
{
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
case 1: // 90 deg
events->onNavRight();
break;
case 2: // 180 deg
events->onNavUp();
break;
case 3: // 270 deg
events->onNavLeft();
break;
default: // 0 deg
events->onNavDown();
break;
}
}
// Call this when your joystick gets a left input
void InkHUD::InkHUD::navLeft()
{
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
case 1: // 90 deg
events->onNavDown();
break;
case 2: // 180 deg
events->onNavRight();
break;
case 3: // 270 deg
events->onNavUp();
break;
default: // 0 deg
events->onNavLeft();
break;
}
}
// Call this when your joystick gets a right input
void InkHUD::InkHUD::navRight()
{
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
case 1: // 90 deg
events->onNavUp();
break;
case 2: // 180 deg
events->onNavLeft();
break;
case 3: // 270 deg
events->onNavDown();
break;
default: // 0 deg
events->onNavRight();
break;
}
}
// Cycle the next user applet to the foreground
// Only activated applets are cycled
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
@@ -88,6 +176,14 @@ void InkHUD::InkHUD::nextApplet()
windowManager->nextApplet();
}
// Cycle the previous user applet to the foreground
// Only activated applets are cycled
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
void InkHUD::InkHUD::prevApplet()
{
windowManager->prevApplet();
}
// Show the menu (on the the focused tile)
// The applet previously displayed there will be restored once the menu closes
void InkHUD::InkHUD::openMenu()
@@ -95,6 +191,12 @@ void InkHUD::InkHUD::openMenu()
windowManager->openMenu();
}
// Bring AlignStick applet to the foreground
void InkHUD::InkHUD::openAlignStick()
{
windowManager->openAlignStick();
}
// In layouts where multiple applets are shown at once, change which tile is focused
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
void InkHUD::InkHUD::nextTile()
@@ -102,12 +204,26 @@ void InkHUD::InkHUD::nextTile()
windowManager->nextTile();
}
// In layouts where multiple applets are shown at once, change which tile is focused
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
void InkHUD::InkHUD::prevTile()
{
windowManager->prevTile();
}
// Rotate the display image by 90 degrees
void InkHUD::InkHUD::rotate()
{
windowManager->rotate();
}
// rotate the joystick in 90 degree increments
void InkHUD::InkHUD::rotateJoystick(uint8_t angle)
{
persistence->settings.joystick.alignment += angle;
persistence->settings.joystick.alignment %= 4;
}
// Show / hide the battery indicator in top-right
void InkHUD::InkHUD::toggleBatteryIcon()
{

View File

@@ -55,15 +55,25 @@ class InkHUD
void shortpress();
void longpress();
void exitShort();
void exitLong();
void navUp();
void navDown();
void navLeft();
void navRight();
// Trigger UI changes
// - called by various InkHUD components
// - suitable(?) for use by aux button, connected in variant nicheGraphics.h
void nextApplet();
void prevApplet();
void openMenu();
void openAlignStick();
void nextTile();
void prevTile();
void rotate();
void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default
void toggleBatteryIcon();
// Updating the display

View File

@@ -29,7 +29,7 @@ class Persistence
// Used to invalidate old settings, if needed
// Version 0 is reserved for testing, and will always load defaults
static constexpr uint32_t SETTINGS_VERSION = 2;
static constexpr uint32_t SETTINGS_VERSION = 3;
struct Settings {
struct Meta {
@@ -96,6 +96,19 @@ class Persistence
bool safeShutdownSeen = false;
} tips;
// Joystick settings for enabling and aligning to the screen
struct Joystick {
// Modifies the UI for joystick use
bool enabled = false;
// gets set to true when AlignStick applet is completed
bool aligned = false;
// Rotation of the joystick
// Multiples of 90 degrees clockwise
uint8_t alignment = 0;
} joystick;
// Rotation of the display
// Multiples of 90 degrees clockwise
// Most commonly: rotation is 0 when flex connector is oriented below display

View File

@@ -8,4 +8,5 @@ build_flags =
-D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling
-D HAS_BUTTON=0 ; Suppress default ButtonThread
lib_deps =
https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX
# TODO renovate
https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX

View File

@@ -2,6 +2,7 @@
#include "./WindowManager.h"
#include "./Applets/System/AlignStick/AlignStickApplet.h"
#include "./Applets/System/BatteryIcon/BatteryIconApplet.h"
#include "./Applets/System/Logo/LogoApplet.h"
#include "./Applets/System/Menu/MenuApplet.h"
@@ -98,6 +99,38 @@ void InkHUD::WindowManager::nextTile()
userTiles.at(settings->userTiles.focused)->requestHighlight();
}
// Focus on a different tile but decrement index
void InkHUD::WindowManager::prevTile()
{
// Close the menu applet if open
// We don't *really* want to do this, but it simplifies handling *a lot*
MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu");
bool menuWasOpen = false;
if (menu->isForeground()) {
menu->sendToBackground();
menuWasOpen = true;
}
// Swap to next tile
if (settings->userTiles.focused == 0)
settings->userTiles.focused = settings->userTiles.count - 1;
else
settings->userTiles.focused--;
// Make sure that we don't get stuck on the placeholder tile
refocusTile();
if (menuWasOpen)
menu->show(userTiles.at(settings->userTiles.focused));
// Ask the tile to draw an indicator showing which tile is now focused
// Requests a render
// We only draw this indicator if the device uses an aux button to switch tiles.
// Assume aux button is used to switch tiles if the "next tile" menu item is hidden
if (!settings->optionalMenuItems.nextTile)
userTiles.at(settings->userTiles.focused)->requestHighlight();
}
// Show the menu (on the the focused tile)
// The applet previously displayed there will be restored once the menu closes
void InkHUD::WindowManager::openMenu()
@@ -106,6 +139,15 @@ void InkHUD::WindowManager::openMenu()
menu->show(userTiles.at(settings->userTiles.focused));
}
// Bring the AlignStick applet to the foreground
void InkHUD::WindowManager::openAlignStick()
{
if (settings->joystick.enabled) {
AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick");
alignStick->bringToForeground();
}
}
// On the currently focussed tile: cycle to the next available user applet
// Applets available for this must be activated, and not already displayed on another tile
void InkHUD::WindowManager::nextApplet()
@@ -155,6 +197,59 @@ void InkHUD::WindowManager::nextApplet()
inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
}
// On the currently focussed tile: cycle to the previous available user applet
// Applets available for this must be activated, and not already displayed on another tile
void InkHUD::WindowManager::prevApplet()
{
Tile *t = userTiles.at(settings->userTiles.focused);
// Abort if zero applets available
// nullptr means WindowManager::refocusTile determined that there were no available applets
if (!t->getAssignedApplet())
return;
// Find the index of the applet currently shown on the tile
uint8_t appletIndex = -1;
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
if (inkhud->userApplets.at(i) == t->getAssignedApplet()) {
appletIndex = i;
break;
}
}
// Confirm that we did find the applet
assert(appletIndex != (uint8_t)-1);
// Iterate forward through the WindowManager::applets, looking for the previous valid applet
Applet *prevValidApplet = nullptr;
for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) {
uint8_t newAppletIndex = 0;
if (i > appletIndex)
newAppletIndex = inkhud->userApplets.size() + appletIndex - i;
else
newAppletIndex = (appletIndex - i);
Applet *a = inkhud->userApplets.at(newAppletIndex);
// Looking for an applet which is active (enabled by user), but currently in background
if (a->isActive() && !a->isForeground()) {
prevValidApplet = a;
settings->userTiles.displayedUserApplet[settings->userTiles.focused] =
newAppletIndex; // Remember this setting between boots!
break;
}
}
// Confirm that we found another applet
if (!prevValidApplet)
return;
// Hide old applet, show new applet
t->getAssignedApplet()->sendToBackground();
t->assignApplet(prevValidApplet);
prevValidApplet->bringToForeground();
inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
}
// Rotate the display image by 90 degrees
void InkHUD::WindowManager::rotate()
{
@@ -338,6 +433,8 @@ void InkHUD::WindowManager::createSystemApplets()
addSystemApplet("Logo", new LogoApplet, new Tile);
addSystemApplet("Pairing", new PairingApplet, new Tile);
addSystemApplet("Tips", new TipsApplet, new Tile);
if (settings->joystick.enabled)
addSystemApplet("AlignStick", new AlignStickApplet, new Tile);
addSystemApplet("Menu", new MenuApplet, nullptr);
@@ -360,6 +457,8 @@ void InkHUD::WindowManager::placeSystemTiles()
inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
if (settings->joystick.enabled)
inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20);

View File

@@ -28,8 +28,11 @@ class WindowManager
// - call these to make stuff change
void nextTile();
void prevTile();
void openMenu();
void openAlignStick();
void nextApplet();
void prevApplet();
void rotate();
void toggleBatteryIcon();

View File

@@ -0,0 +1,523 @@
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./TwoButtonExtended.h"
#include "NodeDB.h" // For the helper function TwoButtonExtended::getUserButtonPin
#include "PowerFSM.h"
#include "sleep.h"
using namespace NicheGraphics::Inputs;
TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended")
{
// Don't start polling buttons for release immediately
// Assume they are in a "released" state at boot
OSThread::disable();
#ifdef ARCH_ESP32
// Register callbacks for before and after lightsleep
lsObserver.observe(&notifyLightSleep);
lsEndObserver.observe(&notifyLightSleepEnd);
#endif
// Explicitly initialize these, just to keep cppcheck quiet..
buttons[0] = Button();
buttons[1] = Button();
joystick[Direction::UP] = SimpleButton();
joystick[Direction::DOWN] = SimpleButton();
joystick[Direction::LEFT] = SimpleButton();
joystick[Direction::RIGHT] = SimpleButton();
}
// Get access to (or create) the singleton instance of this class
// Accessible inside the ISRs, even though we maybe shouldn't
TwoButtonExtended *TwoButtonExtended::getInstance()
{
// Instantiate the class the first time this method is called
static TwoButtonExtended *const singletonInstance = new TwoButtonExtended;
return singletonInstance;
}
// Begin receiving button input
// We probably need to do this after sleep, as well as at boot
void TwoButtonExtended::start()
{
if (buttons[0].pin != 0xFF)
attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING);
if (buttons[1].pin != 0xFF)
attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING);
if (joystick[Direction::UP].pin != 0xFF)
attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp,
joystickActiveLogic == LOW ? FALLING : RISING);
if (joystick[Direction::DOWN].pin != 0xFF)
attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown,
joystickActiveLogic == LOW ? FALLING : RISING);
if (joystick[Direction::LEFT].pin != 0xFF)
attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft,
joystickActiveLogic == LOW ? FALLING : RISING);
if (joystick[Direction::RIGHT].pin != 0xFF)
attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight,
joystickActiveLogic == LOW ? FALLING : RISING);
}
// Stop receiving button input, and run custom sleep code
// Called before device sleeps. This might be power-off, or just ESP32 light sleep
// Some devices will want to attach interrupts here, for the user button to wake from sleep
void TwoButtonExtended::stop()
{
if (buttons[0].pin != 0xFF)
detachInterrupt(buttons[0].pin);
if (buttons[1].pin != 0xFF)
detachInterrupt(buttons[1].pin);
if (joystick[Direction::UP].pin != 0xFF)
detachInterrupt(joystick[Direction::UP].pin);
if (joystick[Direction::DOWN].pin != 0xFF)
detachInterrupt(joystick[Direction::DOWN].pin);
if (joystick[Direction::LEFT].pin != 0xFF)
detachInterrupt(joystick[Direction::LEFT].pin);
if (joystick[Direction::RIGHT].pin != 0xFF)
detachInterrupt(joystick[Direction::RIGHT].pin);
}
// Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings
// This helper method isn't used by the TwoButtonExtended class itself, it could be moved elsewhere.
// Intention is to pass this value to TwoButtonExtended::setWiring in the setupNicheGraphics method.
uint8_t TwoButtonExtended::getUserButtonPin()
{
uint8_t pin = 0xFF; // Unset
// Use default pin for variant, if no better source
#ifdef BUTTON_PIN
pin = BUTTON_PIN;
#endif
// From userPrefs.jsonc, if set
#ifdef USERPREFS_BUTTON_PIN
pin = USERPREFS_BUTTON_PIN;
#endif
// From user's override in device settings, if set
if (config.device.button_gpio)
pin = config.device.button_gpio;
return pin;
}
// Configures the wiring and logic of either button
// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp
void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup)
{
// Prevent the same GPIO being assigned to multiple buttons
// Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button
for (uint8_t i = 0; i < whichButton; i++) {
if (buttons[i].pin == pin) {
LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton);
return;
}
}
assert(whichButton < 2);
buttons[whichButton].pin = pin;
buttons[whichButton].activeLogic = LOW;
pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT);
}
// Configures the wiring and logic of the joystick buttons
// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp
void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup)
{
if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin ||
joystick[Direction::RIGHT].pin == rPin) {
LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment");
return;
}
joystick[Direction::UP].pin = uPin;
joystick[Direction::DOWN].pin = dPin;
joystick[Direction::LEFT].pin = lPin;
joystick[Direction::RIGHT].pin = rPin;
joystickActiveLogic = LOW;
pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT);
pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT);
pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT);
pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT);
}
void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs)
{
assert(whichButton < 2);
buttons[whichButton].debounceLength = debounceMs;
buttons[whichButton].longpressLength = longpressMs;
}
void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs)
{
joystickDebounceLength = debounceMs;
}
// Set what should happen when a button becomes pressed
// Use this to implement a "while held" behavior
void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown)
{
assert(whichButton < 2);
buttons[whichButton].onDown = onDown;
}
// Set what should happen when a button becomes unpressed
// Use this to implement a "While held" behavior
void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp)
{
assert(whichButton < 2);
buttons[whichButton].onUp = onUp;
}
// Set what should happen when a "short press" event has occurred
void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress)
{
assert(whichButton < 2);
buttons[whichButton].onPress = onPress;
}
// Set what should happen when a "long press" event has fired
// Note: this will occur while the button is still held
void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress)
{
assert(whichButton < 2);
buttons[whichButton].onLongPress = onLongPress;
}
// Set what should happen when a joystick button becomes pressed
// Use this to implement a "while held" behavior
void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown)
{
joystick[Direction::UP].onDown = uDown;
joystick[Direction::DOWN].onDown = dDown;
joystick[Direction::LEFT].onDown = lDown;
joystick[Direction::RIGHT].onDown = rDown;
}
// Set what should happen when a joystick button becomes unpressed
// Use this to implement a "while held" behavior
void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp)
{
joystick[Direction::UP].onUp = uUp;
joystick[Direction::DOWN].onUp = dUp;
joystick[Direction::LEFT].onUp = lUp;
joystick[Direction::RIGHT].onUp = rUp;
}
// Set what should happen when a "press" event has fired
// Note: this will occur while the joystick button is still held
void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress)
{
joystick[Direction::UP].onPress = uPress;
joystick[Direction::DOWN].onPress = dPress;
joystick[Direction::LEFT].onPress = lPress;
joystick[Direction::RIGHT].onPress = rPress;
}
// Handle the start of a press to the primary button
// Wakes our button thread
void TwoButtonExtended::isrPrimary()
{
static volatile bool isrRunning = false;
if (!isrRunning) {
isrRunning = true;
TwoButtonExtended *b = TwoButtonExtended::getInstance();
if (b->buttons[0].state == State::REST) {
b->buttons[0].state = State::IRQ;
b->buttons[0].irqAtMillis = millis();
b->startThread();
}
isrRunning = false;
}
}
// Handle the start of a press to the secondary button
// Wakes our button thread
void TwoButtonExtended::isrSecondary()
{
static volatile bool isrRunning = false;
if (!isrRunning) {
isrRunning = true;
TwoButtonExtended *b = TwoButtonExtended::getInstance();
if (b->buttons[1].state == State::REST) {
b->buttons[1].state = State::IRQ;
b->buttons[1].irqAtMillis = millis();
b->startThread();
}
isrRunning = false;
}
}
// Handle the start of a press to the joystick buttons
// Also wakes our button thread
void TwoButtonExtended::isrJoystickUp()
{
static volatile bool isrRunning = false;
if (!isrRunning) {
isrRunning = true;
TwoButtonExtended *b = TwoButtonExtended::getInstance();
if (b->joystick[Direction::UP].state == State::REST) {
b->joystick[Direction::UP].state = State::IRQ;
b->joystick[Direction::UP].irqAtMillis = millis();
b->startThread();
}
isrRunning = false;
}
}
void TwoButtonExtended::isrJoystickDown()
{
static volatile bool isrRunning = false;
if (!isrRunning) {
isrRunning = true;
TwoButtonExtended *b = TwoButtonExtended::getInstance();
if (b->joystick[Direction::DOWN].state == State::REST) {
b->joystick[Direction::DOWN].state = State::IRQ;
b->joystick[Direction::DOWN].irqAtMillis = millis();
b->startThread();
}
isrRunning = false;
}
}
void TwoButtonExtended::isrJoystickLeft()
{
static volatile bool isrRunning = false;
if (!isrRunning) {
isrRunning = true;
TwoButtonExtended *b = TwoButtonExtended::getInstance();
if (b->joystick[Direction::LEFT].state == State::REST) {
b->joystick[Direction::LEFT].state = State::IRQ;
b->joystick[Direction::LEFT].irqAtMillis = millis();
b->startThread();
}
isrRunning = false;
}
}
void TwoButtonExtended::isrJoystickRight()
{
static volatile bool isrRunning = false;
if (!isrRunning) {
isrRunning = true;
TwoButtonExtended *b = TwoButtonExtended::getInstance();
if (b->joystick[Direction::RIGHT].state == State::REST) {
b->joystick[Direction::RIGHT].state = State::IRQ;
b->joystick[Direction::RIGHT].irqAtMillis = millis();
b->startThread();
}
isrRunning = false;
}
}
// Concise method to start our button thread
// Follows an ISR, listening for button release
void TwoButtonExtended::startThread()
{
if (!OSThread::enabled) {
OSThread::setInterval(10);
OSThread::enabled = true;
}
}
// Concise method to stop our button thread
// Called when we no longer need to poll for button release
void TwoButtonExtended::stopThread()
{
if (OSThread::enabled) {
OSThread::disable();
}
// Reset both buttons manually
// Just in case an IRQ fires during the process of resetting the system
// Can occur with super rapid presses?
buttons[0].state = REST;
buttons[1].state = REST;
joystick[Direction::UP].state = REST;
joystick[Direction::DOWN].state = REST;
joystick[Direction::LEFT].state = REST;
joystick[Direction::RIGHT].state = REST;
}
// Our button thread
// Started by an IRQ, on either button
// Polls for button releases
// Stops when both buttons released
int32_t TwoButtonExtended::runOnce()
{
constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button);
constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton);
// Allow either button to request that our thread should continue polling
bool awaitingRelease = false;
// Check both primary and secondary buttons
for (uint8_t i = 0; i < BUTTON_COUNT; i++) {
switch (buttons[i].state) {
// No action: button has not been pressed
case REST:
break;
// New press detected by interrupt
case IRQ:
powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
buttons[i].onDown(); // Run callback: press has begun (possible hold behavior)
buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
awaitingRelease = true; // Mark that polling-for-release should continue
break;
// An existing press continues
// Not held long enough to register as longpress
case POLLING_UNFIRED: {
uint32_t length = millis() - buttons[i].irqAtMillis;
// If button released since last thread tick,
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
buttons[i].onUp(); // Run callback: press has ended (possible release of a hold)
buttons[i].state = State::REST; // Mark that the button has reset
if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress,
buttons[i].onPress(); // Run callback: press
}
// If button not yet released
else {
awaitingRelease = true; // Mark that polling-for-release should continue
if (length >= buttons[i].longpressLength) {
// Run callback: long press (once)
// Then continue waiting for release, to rearm
buttons[i].state = State::POLLING_FIRED;
buttons[i].onLongPress();
}
}
break;
}
// Button still held, but duration long enough that longpress event already fired
// Just waiting for release
case POLLING_FIRED:
// Release detected
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
buttons[i].state = State::REST;
buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired)
}
// Not yet released, keep polling
else
awaitingRelease = true;
break;
}
}
// Check all the joystick directions
for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) {
switch (joystick[i].state) {
// No action: button has not been pressed
case REST:
break;
// New press detected by interrupt
case IRQ:
powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
joystick[i].onDown(); // Run callback: press has begun (possible hold behavior)
joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
awaitingRelease = true; // Mark that polling-for-release should continue
break;
// An existing press continues
// Not held long enough to register as press
case POLLING_UNFIRED: {
uint32_t length = millis() - joystick[i].irqAtMillis;
// If button released since last thread tick,
if (digitalRead(joystick[i].pin) != joystickActiveLogic) {
joystick[i].onUp(); // Run callback: press has ended (possible release of a hold)
joystick[i].state = State::REST; // Mark that the button has reset
}
// If button not yet released
else {
awaitingRelease = true; // Mark that polling-for-release should continue
if (length >= joystickDebounceLength) {
// Run callback: long press (once)
// Then continue waiting for release, to rearm
joystick[i].state = State::POLLING_FIRED;
joystick[i].onPress();
}
}
break;
}
// Button still held after press
// Just waiting for release
case POLLING_FIRED:
// Release detected
if (digitalRead(joystick[i].pin) != joystickActiveLogic) {
joystick[i].state = State::REST;
joystick[i].onUp(); // Callback: release of hold
}
// Not yet released, keep polling
else
awaitingRelease = true;
break;
}
}
// If all buttons are now released
// we don't need to waste cpu resources polling
// IRQ will restart this thread when we next need it
if (!awaitingRelease)
stopThread();
// Run this method again, or don't..
// Use whatever behavior was previously set by stopThread() or startThread()
return OSThread::interval;
}
#ifdef ARCH_ESP32
// Detach our class' interrupts before lightsleep
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
int TwoButtonExtended::beforeLightSleep(void *unused)
{
stop();
return 0; // Indicates success
}
// Reconfigure our interrupts
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
int TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause)
{
start();
// Manually trigger the button-down ISR
// - during light sleep, our ISR is disabled
// - if light sleep ends by button press, pretend our own ISR caught it
// - need to manually confirm by reading pin ourselves, to avoid occasional false positives
// (false positive only when using internal pullup resistors?)
if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic)
isrPrimary();
return 0; // Indicates success
}
#endif
#endif

View File

@@ -0,0 +1,136 @@
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
/*
Re-usable NicheGraphics input source
Short and Long press for up to two buttons
Interrupt driven
*/
/*
This expansion adds support for four more buttons
These buttons are single-action only, no long press
Interrupt driven
*/
#pragma once
#include "configuration.h"
#include "assert.h"
#include "functional"
#ifdef ARCH_ESP32
#include "esp_sleep.h" // For light-sleep handling
#endif
#include "Observer.h"
namespace NicheGraphics::Inputs
{
class TwoButtonExtended : protected concurrency::OSThread
{
public:
typedef std::function<void()> Callback;
static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition
static TwoButtonExtended *getInstance(); // Create or get the singleton instance
void start(); // Start handling button input
void stop(); // Stop handling button input (disconnect ISRs for sleep)
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false);
void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false);
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
void setJoystickDebounce(uint32_t debounceMs);
void setHandlerDown(uint8_t whichButton, Callback onDown);
void setHandlerUp(uint8_t whichButton, Callback onUp);
void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
void setHandlerLongPress(uint8_t whichButton, Callback onLongPress);
void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown);
void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp);
void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress);
// Disconnect and reconnect interrupts for light sleep
#ifdef ARCH_ESP32
int beforeLightSleep(void *unused);
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
#endif
private:
// Internal state of a specific button
enum State {
REST, // Up, no activity
IRQ, // Down detected, not yet handled
POLLING_UNFIRED, // Down handled, polling for release
POLLING_FIRED, // Longpress fired, button still held
};
// Joystick Directions
enum Direction { UP = 0, DOWN, LEFT, RIGHT };
// Data used for direction (single-action) buttons
class SimpleButton
{
public:
// Per-button config
uint8_t pin = 0xFF; // 0xFF: unset
volatile State state = State::REST; // Internal state
volatile uint32_t irqAtMillis; // millis() when button went down
// Per-button event callbacks
static void noop(){};
std::function<void()> onDown = noop;
std::function<void()> onUp = noop;
std::function<void()> onPress = noop;
};
// Data used for double-action buttons
class Button : public SimpleButton
{
public:
// Per-button extended config
bool activeLogic = LOW; // Active LOW by default.
uint32_t debounceLength = 50; // Minimum length for shortpress in ms
uint32_t longpressLength = 500; // Time until longpress in ms
// Per-button event callbacks
std::function<void()> onLongPress = noop;
};
#ifdef ARCH_ESP32
// Get notified when lightsleep begins and ends
CallbackObserver<TwoButtonExtended, void *> lsObserver =
CallbackObserver<TwoButtonExtended, void *>(this, &TwoButtonExtended::beforeLightSleep);
CallbackObserver<TwoButtonExtended, esp_sleep_wakeup_cause_t> lsEndObserver =
CallbackObserver<TwoButtonExtended, esp_sleep_wakeup_cause_t>(this, &TwoButtonExtended::afterLightSleep);
#endif
int32_t runOnce() override; // Timer method. Polls for button release
void startThread(); // Start polling for release
void stopThread(); // Stop polling for release
static void isrPrimary(); // User Button ISR
static void isrSecondary(); // optional aux button or joystick center
static void isrJoystickUp();
static void isrJoystickDown();
static void isrJoystickLeft();
static void isrJoystickRight();
TwoButtonExtended(); // Constructor made private: force use of Button::instance()
// Info about both buttons
Button buttons[2];
bool joystickActiveLogic = LOW; // Active LOW by default
uint32_t joystickDebounceLength = 50; // time until press in ms
SimpleButton joystick[4];
};
}; // namespace NicheGraphics::Inputs
#endif

View File

@@ -489,8 +489,6 @@ int32_t KbI2cBase::runOnce()
case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT
case 0x91: // fn+t
case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE
case 0x8b: // fn+del INPUT_BROKEN_MSG_DISMISS_FRAME
case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE
case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST
// just pass those unmodified

View File

@@ -428,10 +428,17 @@ void setup()
#endif
#if ARCH_PORTDUINO
RTCQuality ourQuality = RTCQualityDevice;
std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c");
if (timeCommandResult[0] == '1') {
ourQuality = RTCQualityNTP;
}
struct timeval tv;
tv.tv_sec = time(NULL);
tv.tv_usec = 0;
perhapsSetRTC(RTCQualityDevice, &tv);
perhapsSetRTC(ourQuality, &tv);
#endif
powerMonInit();

View File

@@ -96,6 +96,8 @@ class Channels
bool setDefaultPresetCryptoForHash(ChannelHash channelHash);
int16_t getHash(ChannelIndex i) { return hashes[i]; }
private:
/** Given a channel index, change to use the crypto key specified by that index
*
@@ -113,8 +115,6 @@ class Channels
*/
int16_t generateHash(ChannelIndex channelNum);
int16_t getHash(ChannelIndex i) { return hashes[i]; }
/**
* Validate a channel, fixing any errors as needed
*/

View File

@@ -44,6 +44,7 @@ struct UIFrameEvent {
REDRAW_ONLY, // Don't change which frames are show, just redraw, asap
REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus()
REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout
SWITCH_TO_TEXTMESSAGE // Jump directly to the Text Message screen
} action = REDRAW_ONLY;
// We might want to pass additional data inside this struct at some point

View File

@@ -7,10 +7,12 @@
#include "../concurrency/Periodic.h"
#include "BluetoothCommon.h" // needed for updateBatteryLevel, FIXME, eventually when we pull mesh out into a lib we shouldn't be whacking bluetooth from here
#include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "RTC.h"
#include "TypeConversions.h"
#include "graphics/draw/MessageRenderer.h"
#include "main.h"
#include "mesh-pb-constants.h"
#include "meshUtils.h"
@@ -192,8 +194,16 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
p.id = generatePacketId(); // If the phone didn't supply one, then pick one
p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone
// (so we update our nodedb for the local node)
#if HAS_SCREEN
if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && p.to != NODENUM_BROADCAST &&
p.to != 0) // DM only
{
perhapsDecode(&p);
const StoredMessage &sm = messageStore.addFromPacket(p);
graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI
}
#endif
// Send the packet into the mesh
DEBUG_HEAP_BEFORE;
auto a = packetPool.allocCopy(p);

View File

@@ -692,7 +692,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
// Store a copy of encrypted packet for MQTT
DEBUG_HEAP_BEFORE;
meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
p_encrypted = packetPool.allocCopy(*p);
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
// Take those raw bytes and convert them back into a well structured protobuf we can understand
@@ -758,6 +758,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
}
packetPool.release(p_encrypted); // Release the encrypted packet
p_encrypted = nullptr;
}
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)

View File

@@ -91,6 +91,9 @@ class Router : protected concurrency::OSThread, protected PacketHistory
before us */
uint32_t rxDupe = 0, txRelayCanceled = 0;
// pointer to the encrypted packet
meshtastic_MeshPacket *p_encrypted = nullptr;
protected:
friend class RoutingModule;

View File

@@ -33,3 +33,5 @@ PB_BIND(meshtastic_KeyVerificationAdmin, meshtastic_KeyVerificationAdmin, AUTO)

View File

@@ -16,6 +16,16 @@
#endif
/* Enum definitions */
/* Firmware update mode for OTA updates */
typedef enum _meshtastic_OTAMode {
/* Do not reboot into OTA mode */
meshtastic_OTAMode_NO_REBOOT_OTA = 0,
/* Reboot into OTA mode for BLE firmware update */
meshtastic_OTAMode_OTA_BLE = 1,
/* Reboot into OTA mode for WiFi firmware update */
meshtastic_OTAMode_OTA_WIFI = 2
} meshtastic_OTAMode;
/* TODO: REPLACE */
typedef enum _meshtastic_AdminMessage_ConfigType {
/* TODO: REPLACE */
@@ -258,10 +268,13 @@ typedef struct _meshtastic_AdminMessage {
meshtastic_SharedContact add_contact;
/* Initiate or respond to a key verification request */
meshtastic_KeyVerificationAdmin key_verification;
/* Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now) */
meshtastic_OTAMode reboot_ota_mode;
/* Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. */
int32_t factory_reset_device;
/* Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot)
Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. */
Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth.
Deprecated in favor of reboot_ota_mode in 2.7.17 */
int32_t reboot_ota_seconds;
/* This message is only supported for the simulator Portduino build.
If received the simulator will exit successfully. */
@@ -288,6 +301,10 @@ extern "C" {
#endif
/* Helper constants for enums */
#define _meshtastic_OTAMode_MIN meshtastic_OTAMode_NO_REBOOT_OTA
#define _meshtastic_OTAMode_MAX meshtastic_OTAMode_OTA_WIFI
#define _meshtastic_OTAMode_ARRAYSIZE ((meshtastic_OTAMode)(meshtastic_OTAMode_OTA_WIFI+1))
#define _meshtastic_AdminMessage_ConfigType_MIN meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG
#define _meshtastic_AdminMessage_ConfigType_MAX meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG
#define _meshtastic_AdminMessage_ConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ConfigType)(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG+1))
@@ -309,6 +326,7 @@ extern "C" {
#define meshtastic_AdminMessage_payload_variant_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation
#define meshtastic_AdminMessage_payload_variant_restore_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation
#define meshtastic_AdminMessage_payload_variant_remove_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation
#define meshtastic_AdminMessage_payload_variant_reboot_ota_mode_ENUMTYPE meshtastic_OTAMode
@@ -396,6 +414,7 @@ extern "C" {
#define meshtastic_AdminMessage_commit_edit_settings_tag 65
#define meshtastic_AdminMessage_add_contact_tag 66
#define meshtastic_AdminMessage_key_verification_tag 67
#define meshtastic_AdminMessage_reboot_ota_mode_tag 68
#define meshtastic_AdminMessage_factory_reset_device_tag 94
#define meshtastic_AdminMessage_reboot_ota_seconds_tag 95
#define meshtastic_AdminMessage_exit_simulator_tag 96
@@ -454,6 +473,7 @@ X(a, STATIC, ONEOF, BOOL, (payload_variant,begin_edit_settings,begin_ed
X(a, STATIC, ONEOF, BOOL, (payload_variant,commit_edit_settings,commit_edit_settings), 65) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,add_contact,add_contact), 66) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification,key_verification), 67) \
X(a, STATIC, ONEOF, UENUM, (payload_variant,reboot_ota_mode,reboot_ota_mode), 68) \
X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_device,factory_reset_device), 94) \
X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_ota_seconds,reboot_ota_seconds), 95) \
X(a, STATIC, ONEOF, BOOL, (payload_variant,exit_simulator,exit_simulator), 96) \

View File

@@ -24,6 +24,9 @@ PB_BIND(meshtastic_Data, meshtastic_Data, 2)
PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO)
PB_BIND(meshtastic_StoreForwardPlusPlus, meshtastic_StoreForwardPlusPlus, 2)
PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO)
@@ -121,6 +124,8 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU

View File

@@ -478,6 +478,22 @@ typedef enum _meshtastic_Routing_Error {
meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38
} meshtastic_Routing_Error;
/* Enum of message types */
typedef enum _meshtastic_StoreForwardPlusPlus_SFPP_message_type {
/* Send an announcement of the canonical tip of a chain */
meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE = 0,
/* Query whether a specific link is on the chain */
meshtastic_StoreForwardPlusPlus_SFPP_message_type_CHAIN_QUERY = 1,
/* Request the next link in the chain */
meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST = 3,
/* Provide a link to add to the chain */
meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE = 4,
/* If we must fragment, send the first half */
meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_FIRSTHALF = 5,
/* If we must fragment, send the second half */
meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF = 6
} meshtastic_StoreForwardPlusPlus_SFPP_message_type;
/* The priority of this message for sending.
Higher priorities are sent first (when managing the transmit queue).
This field is never sent over the air, it is only used internally inside of a local device node.
@@ -782,6 +798,32 @@ typedef struct _meshtastic_KeyVerification {
meshtastic_KeyVerification_hash2_t hash2;
} meshtastic_KeyVerification;
typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_message_hash_t;
typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_commit_hash_t;
typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_root_hash_t;
typedef PB_BYTES_ARRAY_T(240) meshtastic_StoreForwardPlusPlus_message_t;
/* The actual over-the-mesh message doing store and forward++ */
typedef struct _meshtastic_StoreForwardPlusPlus {
/* Which message type is this */
meshtastic_StoreForwardPlusPlus_SFPP_message_type sfpp_message_type;
/* The hash of the specific message */
meshtastic_StoreForwardPlusPlus_message_hash_t message_hash;
/* The hash of a link on a chain */
meshtastic_StoreForwardPlusPlus_commit_hash_t commit_hash;
/* the root hash of a chain */
meshtastic_StoreForwardPlusPlus_root_hash_t root_hash;
/* The encrypted bytes from a message */
meshtastic_StoreForwardPlusPlus_message_t message;
/* Message ID of the contained message */
uint32_t encapsulated_id;
/* Destination of the contained message */
uint32_t encapsulated_to;
/* Sender of the contained message */
uint32_t encapsulated_from;
/* The receive time of the message in question */
uint32_t encapsulated_rxtime;
} meshtastic_StoreForwardPlusPlus;
/* Waypoint message, used to share arbitrary locations across the mesh */
typedef struct _meshtastic_Waypoint {
/* Id of the waypoint */
@@ -1310,6 +1352,10 @@ extern "C" {
#define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED
#define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED+1))
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+1))
#define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET
#define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX
#define _meshtastic_MeshPacket_Priority_ARRAYSIZE ((meshtastic_MeshPacket_Priority)(meshtastic_MeshPacket_Priority_MAX+1))
@@ -1338,6 +1384,8 @@ extern "C" {
#define meshtastic_Data_portnum_ENUMTYPE meshtastic_PortNum
#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_ENUMTYPE meshtastic_StoreForwardPlusPlus_SFPP_message_type
#define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority
@@ -1380,6 +1428,7 @@ extern "C" {
#define meshtastic_Routing_init_default {0, {meshtastic_RouteDiscovery_init_default}}
#define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
#define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}}
#define meshtastic_StoreForwardPlusPlus_init_default {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0}
#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0}
#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0}
#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
@@ -1411,6 +1460,7 @@ extern "C" {
#define meshtastic_Routing_init_zero {0, {meshtastic_RouteDiscovery_init_zero}}
#define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
#define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}}
#define meshtastic_StoreForwardPlusPlus_init_zero {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0}
#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0}
#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0}
#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
@@ -1489,6 +1539,15 @@ extern "C" {
#define meshtastic_KeyVerification_nonce_tag 1
#define meshtastic_KeyVerification_hash1_tag 2
#define meshtastic_KeyVerification_hash2_tag 3
#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_tag 1
#define meshtastic_StoreForwardPlusPlus_message_hash_tag 2
#define meshtastic_StoreForwardPlusPlus_commit_hash_tag 3
#define meshtastic_StoreForwardPlusPlus_root_hash_tag 4
#define meshtastic_StoreForwardPlusPlus_message_tag 5
#define meshtastic_StoreForwardPlusPlus_encapsulated_id_tag 6
#define meshtastic_StoreForwardPlusPlus_encapsulated_to_tag 7
#define meshtastic_StoreForwardPlusPlus_encapsulated_from_tag 8
#define meshtastic_StoreForwardPlusPlus_encapsulated_rxtime_tag 9
#define meshtastic_Waypoint_id_tag 1
#define meshtastic_Waypoint_latitude_i_tag 2
#define meshtastic_Waypoint_longitude_i_tag 3
@@ -1705,6 +1764,19 @@ X(a, STATIC, SINGULAR, BYTES, hash2, 3)
#define meshtastic_KeyVerification_CALLBACK NULL
#define meshtastic_KeyVerification_DEFAULT NULL
#define meshtastic_StoreForwardPlusPlus_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UENUM, sfpp_message_type, 1) \
X(a, STATIC, SINGULAR, BYTES, message_hash, 2) \
X(a, STATIC, SINGULAR, BYTES, commit_hash, 3) \
X(a, STATIC, SINGULAR, BYTES, root_hash, 4) \
X(a, STATIC, SINGULAR, BYTES, message, 5) \
X(a, STATIC, SINGULAR, UINT32, encapsulated_id, 6) \
X(a, STATIC, SINGULAR, UINT32, encapsulated_to, 7) \
X(a, STATIC, SINGULAR, UINT32, encapsulated_from, 8) \
X(a, STATIC, SINGULAR, UINT32, encapsulated_rxtime, 9)
#define meshtastic_StoreForwardPlusPlus_CALLBACK NULL
#define meshtastic_StoreForwardPlusPlus_DEFAULT NULL
#define meshtastic_Waypoint_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, id, 1) \
X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \
@@ -1980,6 +2052,7 @@ extern const pb_msgdesc_t meshtastic_RouteDiscovery_msg;
extern const pb_msgdesc_t meshtastic_Routing_msg;
extern const pb_msgdesc_t meshtastic_Data_msg;
extern const pb_msgdesc_t meshtastic_KeyVerification_msg;
extern const pb_msgdesc_t meshtastic_StoreForwardPlusPlus_msg;
extern const pb_msgdesc_t meshtastic_Waypoint_msg;
extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg;
extern const pb_msgdesc_t meshtastic_MeshPacket_msg;
@@ -2013,6 +2086,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
#define meshtastic_Routing_fields &meshtastic_Routing_msg
#define meshtastic_Data_fields &meshtastic_Data_msg
#define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg
#define meshtastic_StoreForwardPlusPlus_fields &meshtastic_StoreForwardPlusPlus_msg
#define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg
#define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg
#define meshtastic_MeshPacket_fields &meshtastic_MeshPacket_msg
@@ -2069,6 +2143,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
#define meshtastic_QueueStatus_size 23
#define meshtastic_RouteDiscovery_size 256
#define meshtastic_Routing_size 259
#define meshtastic_StoreForwardPlusPlus_size 371
#define meshtastic_ToRadio_size 504
#define meshtastic_User_size 115
#define meshtastic_Waypoint_size 165

View File

@@ -86,6 +86,11 @@ typedef enum _meshtastic_PortNum {
/* Paxcounter lib included in the firmware
ENCODING: protobuf */
meshtastic_PortNum_PAXCOUNTER_APP = 34,
/* Store and Forward++ module included in the firmware
ENCODING: protobuf
This module is specifically for Native Linux nodes, and provides a Git-style
chain of messages. */
meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP = 35,
/* Provides a hardware serial interface to send and receive from the Meshtastic network.
Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic
network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network.

File diff suppressed because it is too large Load Diff

View File

@@ -75,7 +75,6 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
// === State/UI ===
bool shouldDraw();
bool hasMessages();
void showTemporaryMessage(const String &message);
void resetSearch();
void updateDestinationSelectionList();
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
@@ -153,10 +152,9 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
unsigned long lastUpdateMillis = 0;
String searchQuery;
String freetext;
String temporaryMessage;
// === Message Storage ===
char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT];
int messagesCount = 0;
int currentMessageIndex = -1;
@@ -167,14 +165,11 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display)
ChannelIndex channel = 0; // Channel index used when sending a message
bool ack = false; // True = ACK received, False = NACK or failed
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
bool lastAckWasRelayed = false; // True if the ACK was relayed through intermediate nodes
uint8_t lastAckHopStart = 0; // Hop start value from the received ACK packet
uint8_t lastAckHopLimit = 0; // Hop limit value from the received ACK packet
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
bool ack = false; // True = ACK received, False = NACK or failed
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
uint32_t lastRequestId = 0; // tracks the request_id of our last sent packet
// === State Tracking ===
cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;

View File

@@ -60,9 +60,7 @@ meshtastic_MeshPacket *DropzoneModule::sendConditions()
long hms = rtc_sec % SEC_PER_DAY;
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
hour = hms / SEC_PER_HOUR;
min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN;
graphics::decomposeTime(rtc_sec, hour, min, sec);
}
// Check if the dropzone is open or closed by reading the analog pin

View File

@@ -429,9 +429,7 @@ int32_t PositionModule::runOnce()
if (lastGpsSend == 0 || msSinceLastSend >= intervalMs) {
if (waitingForFreshPosition) {
#ifdef GPS_DEBUG
LOG_DEBUG("Skip initial position send; no fresh position since boot");
#endif
} else if (nodeDB->hasValidPosition(node)) {
lastGpsSend = now;

View File

@@ -20,7 +20,7 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
switch (arg->getStatusType()) {
case STATUS_TYPE_POWER: {
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)arg;
if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) {
if (powerStatus->getHasUSB()) {
power_state = charging;
if (powerStatus->getBatteryChargePercent() >= 100) {
power_state = charged;

View File

@@ -1,10 +1,13 @@
#include "SystemCommandsModule.h"
#include "input/InputBroker.h"
#include "meshUtils.h"
#if HAS_SCREEN
#include "MessageStore.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#endif
#include "GPS.h"
#include "MeshService.h"
#include "Module.h"
@@ -28,10 +31,7 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
switch (event->kbchar) {
// Fn key symbols
case INPUT_BROKER_MSG_FN_SYMBOL_ON:
IF_SCREEN(screen->setFunctionSymbol("Fn"));
return 0;
case INPUT_BROKER_MSG_FN_SYMBOL_OFF:
IF_SCREEN(screen->removeFunctionSymbol("Fn"));
return 0;
// Brightness
case INPUT_BROKER_MSG_BRIGHTNESS_UP:
@@ -78,6 +78,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
case INPUT_BROKER_MSG_REBOOT:
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
nodeDB->saveToDisk();
#if HAS_SCREEN
messageStore.saveToFlash();
#endif
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
return true;

View File

@@ -378,7 +378,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
int line = 1;
// === Set Title
const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env.";
const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Environment" : "Env.";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);

View File

@@ -117,7 +117,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
int line = 1;
// === Set Title
const char *titleStr = (graphics::isHighResolution) ? "Power Telem." : "Power";
const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Power Telem." : "Power";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);

View File

@@ -1,10 +1,14 @@
#include "TextMessageModule.h"
#include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "buzz.h"
#include "configuration.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "main.h"
TextMessageModule *textMessageModule;
ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
@@ -15,14 +19,26 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp
#endif
// We only store/display messages destined for us.
// Keep a copy of the most recent text message.
devicestate.rx_text_message = mp;
devicestate.has_rx_text_message = true;
#if HAS_SCREEN
// Guard against running in MeshtasticUI
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
// Store in the central message history
const StoredMessage &sm = messageStore.addFromPacket(mp);
// Pass message to renderer (banner + thread switching + scroll reset)
// Use the global Screen singleton to retrieve the current OLED display
auto *display = screen ? screen->getDisplayDevice() : nullptr;
graphics::MessageRenderer::handleNewMessage(display, sm, mp);
}
#endif
// Only trigger screen wake if configuration allows it
if (shouldWakeOnReceivedMessage()) {
powerFSM.trigger(EVENT_RECEIVED_MSG);
}
// Notify any observers (e.g. external modules that care about packets)
notifyObservers(&mp);
return ProcessMessage::CONTINUE; // Let others look at this message also if they want

View File

@@ -3,7 +3,13 @@
#include "SinglePortModule.h"
/**
* Text message handling for meshtastic - draws on the OLED display the most recent received message
* Text message handling for Meshtastic.
*
* This module is responsible for receiving and storing incoming text messages
* from the mesh. It updates device state and notifies observers so that other
* components (such as the MessageRenderer) can later display or process them.
*
* Rendering of messages on screen is no longer done here.
*/
class TextMessageModule : public SinglePortModule, public Observable<const meshtastic_MeshPacket *>
{
@@ -15,10 +21,10 @@ class TextMessageModule : public SinglePortModule, public Observable<const mesht
protected:
/** Called to handle a particular incoming message
@return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for
it
*/
*
* @return ProcessMessage::STOP if you've guaranteed you've handled this
* message and no other handlers should be considered for it.
*/
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
};

View File

@@ -2,6 +2,7 @@
#include "NodeDB.h"
#include "PowerFSM.h"
#include "configuration.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/CompassRenderer.h"
#if HAS_SCREEN
@@ -14,6 +15,15 @@
WaypointModule *waypointModule;
static inline float degToRad(float deg)
{
return deg * PI / 180.0f;
}
static inline float radToDeg(float rad)
{
return rad * 180.0f / PI;
}
ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
{
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
@@ -52,31 +62,15 @@ ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
bool WaypointModule::shouldDraw()
{
#if !MESHTASTIC_EXCLUDE_WAYPOINT
if (screen == nullptr)
return false;
// If no waypoint to show
if (!devicestate.has_rx_waypoint)
if (!screen || !devicestate.has_rx_waypoint)
return false;
// Decode the message, to find the expiration time (is waypoint still valid)
// This handles "deletion" as well as expiration
meshtastic_Waypoint wp;
memset(&wp, 0, sizeof(wp));
meshtastic_Waypoint wp{}; // <- replaces memset
if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size,
&meshtastic_Waypoint_msg, &wp)) {
// Valid waypoint
if (wp.expire > getTime())
return devicestate.has_rx_waypoint = true;
// Expired, or deleted
else
return devicestate.has_rx_waypoint = false;
return wp.expire > getTime();
}
// If decoding failed
LOG_ERROR("Failed to decode waypoint");
devicestate.has_rx_waypoint = false;
return false;
return false; // no LOG_ERROR, no flag writes
#else
return false;
#endif
@@ -85,53 +79,46 @@ bool WaypointModule::shouldDraw()
/// Draw the last waypoint we received
void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
if (screen == nullptr)
if (!screen)
return;
// Prepare to draw
display->setFont(FONT_SMALL);
display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
int line = 1;
// Handle inverted display
// Unsure of expected behavior: for now, copy drawNodeInfo
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED)
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
// === Set Title
const char *titleStr = "Waypoint";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
const int w = display->getWidth();
const int h = display->getHeight();
// Decode the waypoint
const meshtastic_MeshPacket &mp = devicestate.rx_waypoint;
meshtastic_Waypoint wp;
memset(&wp, 0, sizeof(wp));
meshtastic_Waypoint wp{};
if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) {
// This *should* be caught by shouldDrawWaypoint, but we'll short-circuit here just in case
display->drawStringMaxWidth(0 + x, 0 + y, x + display->getWidth(), "Couldn't decode waypoint");
devicestate.has_rx_waypoint = false;
return;
}
// Get timestamp info. Will pass as a field to drawColumns
static char lastStr[20];
char lastStr[20];
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
// Will contain distance information, passed as a field to drawColumns
static char distStr[20];
char distStr[20];
// Get our node, to use our own position
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// Text fields to draw (left of compass)
// Last element must be NULL. This signals the end of the char*[] to drawColumns
const char *fields[] = {"Waypoint", lastStr, wp.name, distStr, NULL};
// Dimensions / co-ordinates for the compass/circle
int16_t compassX = 0, compassY = 0;
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight());
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
compassX = x + display->getWidth() - compassDiam / 2 - 5;
compassY = y + display->getHeight() / 2;
} else {
compassX = x + display->getWidth() - compassDiam / 2 - 5;
compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2;
}
const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h);
const int16_t compassX = x + w - (compassDiam / 2) - 5;
const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT)
? y + h / 2
: y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2;
// If our node has a position:
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
@@ -141,7 +128,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
myHeading = 0;
} else {
if (screen->hasHeading())
myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians
myHeading = degToRad(screen->getHeading());
else
myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
}
@@ -157,46 +144,35 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther;
bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI;
bearingToOtherDegrees = radToDeg(bearingToOtherDegrees);
// Distance to Waypoint
float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
if (d < (2 * MILES_TO_FEET))
snprintf(distStr, sizeof(distStr), "%.0fft %.0f°", d * METERS_TO_FEET, bearingToOtherDegrees);
else
snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, bearingToOtherDegrees);
float feet = d * METERS_TO_FEET;
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°",
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees);
} else {
if (d < 2000)
snprintf(distStr, sizeof(distStr), "%.0fm %.0f°", d, bearingToOtherDegrees);
else
snprintf(distStr, sizeof(distStr), "%.1fkm %.0f°", d / 1000, bearingToOtherDegrees);
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000,
bearingToOtherDegrees);
}
}
// If our node doesn't have position
else {
// ? in the compass
display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?");
// ? in the distance field
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL)
strncpy(distStr, "? mi ?°", sizeof(distStr));
else
strncpy(distStr, "? km ?°", sizeof(distStr));
snprintf(distStr, sizeof(distStr), "? %s ?°",
(config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km");
}
// Draw compass circle
display->drawCircle(compassX, compassY, compassDiam / 2);
// Undo color-inversion, if set prior to drawing header
// Unsure of expected behavior? For now: copy drawNodeInfo
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
display->setColor(BLACK);
}
// Must be after distStr is populated
graphics::NodeListRenderer::drawColumns(display, x, y, fields);
display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here!
display->drawString(0, graphics::getTextPositions(display)[line++], lastStr);
display->drawString(0, graphics::getTextPositions(display)[line++], wp.name);
display->drawString(0, graphics::getTextPositions(display)[line++], wp.description);
display->drawString(0, graphics::getTextPositions(display)[line++], distStr);
}
#endif

View File

@@ -2,16 +2,14 @@
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMA423) && __has_include(<SensorBMA423.hpp>)
using namespace MotionSensorI2C;
BMA423Sensor::BMA423Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
bool BMA423Sensor::init()
{
if (sensor.begin(deviceAddress(), &MotionSensorI2C::readRegister, &MotionSensorI2C::writeRegister)) {
if (sensor.begin(Wire, deviceAddress())) {
sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE);
sensor.enableAccelerometer();
sensor.configInterrupt(BMA4_LEVEL_TRIGGER, BMA4_ACTIVE_HIGH, BMA4_PUSH_PULL, BMA4_OUTPUT_ENABLE, BMA4_INPUT_DISABLE);
sensor.configInterrupt();
#ifdef BMA423_INT
pinMode(BMA4XX_INT, INPUT);
@@ -26,9 +24,9 @@ bool BMA423Sensor::init()
#ifdef T_WATCH_S3
// Need to raise the wrist function, need to set the correct axis
sensor.setReampAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER);
sensor.setRemapAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER);
#else
sensor.setReampAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER);
sensor.setRemapAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER);
#endif
// sensor.enableFeature(sensor.FEATURE_STEP_CNTR, true);
sensor.enableFeature(sensor.FEATURE_TILT, true);
@@ -50,7 +48,7 @@ bool BMA423Sensor::init()
int32_t BMA423Sensor::runOnce()
{
if (sensor.readIrqStatus() != DEV_WIRE_NONE) {
if (sensor.readIrqStatus()) {
if (sensor.isTilt() || sensor.isDoubleTap()) {
wakeScreen();
return 500;

View File

@@ -61,32 +61,6 @@ class MotionSensor
uint32_t endCalibrationAt = 0;
};
namespace MotionSensorI2C
{
static inline int readRegister(uint8_t address, uint8_t reg, uint8_t *data, uint8_t len)
{
Wire.beginTransmission(address);
Wire.write(reg);
Wire.endTransmission();
Wire.requestFrom((uint8_t)address, (uint8_t)len);
uint8_t i = 0;
while (Wire.available()) {
data[i++] = Wire.read();
}
return 0; // Pass
}
static inline int writeRegister(uint8_t address, uint8_t reg, uint8_t *data, uint8_t len)
{
Wire.beginTransmission(address);
Wire.write(reg);
Wire.write(data, len);
return (0 != Wire.endTransmission());
}
} // namespace MotionSensorI2C
#endif
#endif

View File

@@ -26,16 +26,6 @@
#include "nimble/nimble/host/include/host/ble_gap.h"
#endif
#ifdef ARCH_ESP32
#if defined(CONFIG_NIMBLE_CPP_IDF)
#include "host/ble_store.h"
#else
#include "nimble/nimble/host/include/host/ble_store.h"
#endif
#include <nvs.h>
#include <nvs_flash.h>
#endif
namespace
{
constexpr uint16_t kPreferredBleMtu = 517;
@@ -43,72 +33,6 @@ constexpr uint16_t kPreferredBleTxOctets = 251;
constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
} // namespace
#ifdef ARCH_ESP32
// Credit: https://github.com/h2zero/NimBLE-Arduino/issues/740#issuecomment-2539923656
// Note: Despite the name, this function is invoked on every device boot (from setup()).
// It performs a routine startup check on the stored NimBLE bond data and deletes bonds
// only if a mismatch or migration condition is detected. It is not a one-time hook that
// runs only when the NimBLE version changes.
static void deleteBondsIfNimBLEVersionChanged()
{
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
err = nvs_flash_erase();
if (err != ESP_OK) {
LOG_ERROR("Failed to erase NVS for NimBLE migration, err=%d", err);
return;
}
err = nvs_flash_init();
if (err != ESP_OK) {
LOG_ERROR("Failed to re-init NVS after erase, err=%d", err);
return;
}
} else if (err != ESP_OK) {
LOG_ERROR("nvs_flash_init failed, err=%d", err);
return;
}
nvs_handle_t nimbleHandle = 0;
err = nvs_open("nimble_bond", NVS_READWRITE, &nimbleHandle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
return;
}
if (err != ESP_OK) {
LOG_ERROR("Failed to open nimble_bond namespace, err=%d", err);
return;
}
size_t requiredSize = 0;
bool bondExists = nvs_get_blob(nimbleHandle, "peer_sec_1", nullptr, &requiredSize) == ESP_OK;
bool rpaExists = nvs_get_blob(nimbleHandle, "rpa_rec_1", nullptr, &requiredSize) == ESP_OK;
bool irkExists = nvs_get_blob(nimbleHandle, "local_irk_1", nullptr, &requiredSize) == ESP_OK;
bool erasePartition = false;
#if defined(BLE_STORE_OBJ_TYPE_LOCAL_IRK)
erasePartition = bondExists && !rpaExists;
#else
erasePartition = rpaExists || irkExists;
#endif
bool restartRequired = false;
if (erasePartition) {
LOG_WARN("Clearing NimBLE bonds due to version migration");
if (nvs_erase_all(nimbleHandle) != ESP_OK || nvs_commit(nimbleHandle) != ESP_OK) {
LOG_ERROR("Failed to erase nimble_bond namespace");
} else {
restartRequired = true;
}
}
nvs_close(nimbleHandle);
if (restartRequired) {
LOG_INFO("Restarting after NimBLE bond cleanup");
ESP.restart();
}
}
#endif
// Debugging options: careful, they slow things down quite a bit!
// #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration
// #define DEBUG_NIMBLE_ON_WRITE_TIMING // uncomment to time onWrite duration
@@ -876,10 +800,6 @@ void NimbleBluetooth::setup()
// Uncomment for testing
// NimbleBluetooth::clearBonds();
#ifdef ARCH_ESP32
deleteBondsIfNimBLEVersionChanged();
#endif
LOG_INFO("Init the NimBLE bluetooth module");
NimBLEDevice::init(getDeviceName());

View File

@@ -427,11 +427,13 @@ void portduinoSetup()
}
getMacAddr(dmac);
#ifndef UNIT_TEST
if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) {
std::cout << "*** Blank MAC Address not allowed!" << std::endl;
std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl;
exit(EXIT_FAILURE);
}
#endif
printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]);
// Rather important to set this, if not running simulated.
randomSeed(time(NULL));

View File

@@ -15,4 +15,5 @@ upload_protocol = esptool
upload_speed = 460800
lib_deps =
${esp32_base.lib_deps}
adafruit/Adafruit NeoPixel @ ^1.12.0
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
adafruit/Adafruit NeoPixel@1.15.2

View File

@@ -13,5 +13,3 @@ board_build.f_cpu = 240000000L
upload_protocol = esptool
;upload_port = /dev/ttyUSB0
upload_speed = 460800
lib_deps =
${esp32_base.lib_deps}

View File

@@ -9,4 +9,5 @@ build_flags =
lib_deps =
${esp32_base.lib_deps}
lovyan03/LovyanGFX@^1.2.0
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7

View File

@@ -54,6 +54,7 @@ build_flags =
lib_deps =
${arduino_base.lib_deps}
${networking_base.lib_deps}
${networking_extra.lib_deps}
${environmental_base.lib_deps}
${environmental_extra.lib_deps}
${radiolib_base.lib_deps}
@@ -63,8 +64,8 @@ lib_deps =
h2zero/NimBLE-Arduino@^2.3.7
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master
https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip
# renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib
https://github.com/lewisxhe/XPowersLib/archive/v0.3.2.zip
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
lewisxhe/XPowersLib@0.3.2
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto

View File

@@ -25,4 +25,5 @@ lib_ignore =
m5stack-core
lib_deps =
${esp32_base.lib_deps}
lovyan03/LovyanGFX@^1.2.0
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7

View File

@@ -18,8 +18,10 @@ build_flags =
-DM5STACK
lib_deps =
${esp32_base.lib_deps}
zinggjm/GxEPD2@^1.6.2
lewisxhe/PCF8563_Library@^1.0.1
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
zinggjm/GxEPD2@1.6.5
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.2
lib_ignore =
m5stack-coreink
monitor_filters = esp32_exception_decoder

View File

@@ -13,7 +13,6 @@
#define LED_STATE_ON 1 // State when LED is lit
#define LED_PIN 10
#include "pcf8563.h"
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
#define HAS_RTC 1

View File

@@ -2,8 +2,6 @@
[env:nano-g1-explorer]
extends = esp32_base
board = ttgo-t-beam
lib_deps =
${esp32_base.lib_deps}
build_flags =
${esp32_base.build_flags}
-D NANO_G1_EXPLORER

View File

@@ -2,8 +2,6 @@
[env:nano-g1]
extends = esp32_base
board = ttgo-t-beam
lib_deps =
${esp32_base.lib_deps}
build_flags =
${esp32_base.build_flags}
-D NANO_G1

View File

@@ -13,4 +13,5 @@ board_build.f_cpu = 240000000L
upload_protocol = esptool
lib_deps =
${esp32_base.lib_deps}
# renovate: datasource=github-tags depName=STK8xxx-Accelerometer packageName=gjelsoe/STK8xxx-Accelerometer
https://github.com/gjelsoe/STK8xxx-Accelerometer/archive/v0.1.1.zip

View File

@@ -15,5 +15,3 @@ build_flags =
-I variants/esp32/radiomaster_900_bandit_nano
board_build.f_cpu = 240000000L
upload_protocol = esptool
lib_deps =
${esp32_base.lib_deps}

View File

@@ -10,5 +10,3 @@ build_flags =
-I variants/esp32/radiomaster_900_bandit_nano
board_build.f_cpu = 240000000L
upload_protocol = esptool
lib_deps =
${esp32_base.lib_deps}

View File

@@ -2,8 +2,6 @@
[env:station-g1]
extends = esp32_base
board = ttgo-t-beam
lib_deps =
${esp32_base.lib_deps}
build_flags =
${esp32_base.build_flags}
-D STATION_G1

Some files were not shown because too many files have changed in this diff Show More