Compare commits

...

311 Commits

Author SHA1 Message Date
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
Ben Meadors
8e0547e76d Implement Long Turbo preset (#8985)
* Implement Long_Turbo preset

* Oops

* Start to DRY up menu handler by actually using OO concepts instead of jank separate arrays

* Move the implementation back into the method

* Dummy comment

* Listen to copilot feedback and prevent dangling pointer

* Static and optional
2025-12-16 11:42:13 -06:00
github-actions[bot]
8a48321555 Upgrade trunk (#8989)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-16 06:17:03 -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
renovate[bot]
aa8bb6c6f1 Update meshtastic/device-ui digest to 862ed04 (#8980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 16:52:23 -06:00
github-actions[bot]
1952982896 Update protobufs (#8982)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-12-15 16:51:59 -06:00
Austin
024ac74f5c rp2xx0: Update to arduino-pico 5.4.4 (#8979) 2025-12-15 16:09:59 -06:00
renovate[bot]
de2b9632bb Update GitHub Artifact Actions (#8954)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 06:52:40 -06:00
github-actions[bot]
c2b7dc2641 Upgrade trunk (#8976)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-15 06:47:00 -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
Jonathan Bennett
b74238194b Add JSON packet recording option to native (#8930) 2025-12-12 18:30:43 -06:00
Jason P
0597ae42c9 Update naming of Frame Visibility toggles 2025-12-12 17:12:21 -06:00
Ben Meadors
5d5819b876 Skipp assertion on this test for now 2025-12-12 16:26:01 -06:00
Tom Fifield
f127702bef Fix GPS Buffer full issue on NRF52480 (Seeed T1000E) (#8956)
We set the buffer size to about a byte on NRF52480, less than
other platforms:

esp32.ini:  -DSERIAL_BUFFER_SIZE=4096
esp32c6.ini:  -DSERIAL_BUFFER_SIZE=4096
nrf52.ini:  -DSERIAL_BUFFER_SIZE=1024

However, 115200 baud, like the T1000e uses is about 12 times that
- almost 15 bytes per millisecond.
15 bytes * 200 millisecond (our GPS poll rate)  = 3000 bytes, which is longer than our buffer
on the nrf52 platform. This causes "GPS Buffer full" errors on the T1000e
and other devices based on NRF52480 with newer GPS chips.

This patch increases SERIAL_BUFFER_SIZE for nrf52480 to 4096 to align with
other platforms. It keeps the original 1024 for the nrf52832, which has
fewer resources.

Fixes https://github.com/meshtastic/firmware/issues/5767
2025-12-12 16:23:23 -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
Ben Meadors
cce8cbfe34 Mark implicit ACK for MQTT as MQTT transport (#8939) (#8947)
* Mark implicit ACK for MQTT as MQTT transport

* TRUNK

* Fix build

* Make sure implicit ACKs from MQTT do not stop retransmissions in ReliableRouter

---------

Co-authored-by: GUVWAF <78759985+GUVWAF@users.noreply.github.com>
2025-12-12 05:21:08 -06:00
github-actions[bot]
a4a6c3509a Upgrade trunk (#8946)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-12 05:20:12 -06:00
GUVWAF
68250dc937 Mark implicit ACK for MQTT as MQTT transport (#8939)
* Mark implicit ACK for MQTT as MQTT transport

* TRUNK

* Fix build

* Make sure implicit ACKs from MQTT do not stop retransmissions in ReliableRouter

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-12-12 05:19:32 -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
Igor Danilov
c8628b3422 Fix #8899 [Bug]: [TloraPager] RotaryEncoder crash (#8933)
* Fix #8899 [Bug]: [TloraPager] RotaryEncoder crash

* Apply Copilot review

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-12-11 19:04:15 -06:00
renovate[bot]
2ac74d6677 Update actions/cache action to v5 (#8944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 19:03:14 -06:00
Ben Meadors
9d487ddc0d Merge pull request #8945 from meshtastic/develop
Develop to master
2025-12-11 19:02:56 -06:00
Austin
bcfe069997 Optimize builds to reduce duplicate dependency checks (#8943)
'mtjson' will now build all required pieces when they don't exist
2025-12-11 19:01:31 -06: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
Austin
4fc96bdf83 Use 'gh-action-runner' action for "Check" jobs. (#8938)
Everything's pre-baked, 503 no more!
2025-12-11 12:26:21 -06:00
renovate[bot]
4ef943f204 Update meshtastic/device-ui digest to 2746a1c (#8936)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 10:32:28 -06:00
Jonathan Bennett
a8fa5f25cb Properly turn off power pins at shutdown for m3 (#8935) 2025-12-11 10:23:45 -06:00
Jason P
2634978e57 Revert "Set nodeName to maximum size"
This reverts commit e254f39925.
2025-12-11 09:29:37 -06:00
Ben Meadors
3b2a1547de More board_level extras 2025-12-11 06:23:08 -06:00
github-actions[bot]
6f725a1996 Upgrade trunk (#8932)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-11 05:25:46 -06:00
Jason P
7515123307 Merge branch 'develop' into multi-message-Storage 2025-12-10 20:49:00 -06:00
Ben Meadors
467c042bf7 Merge pull request #8929 from meshtastic/master
Master to dev
2025-12-10 20:48:03 -06:00
Jason P
bbfddea3af Merge branch 'develop' into multi-message-Storage 2025-12-10 20:44:01 -06:00
Ben Meadors
cc4c41167c Merge pull request #8928 from meshtastic/develop 2025-12-10 19:08:53 -06:00
Benjamin Faershtein
fff2bbf4a0 Use truncated position for smart position (#8906) 2025-12-10 19:05:26 -06:00
Jonathan Bennett
fba92229a6 Add I2C device check for seesaw device on native (#8927)
It turns out the logic here was attempting to access i2c without being told to do so. Not good, especially on desktops.
2025-12-10 18:01:52 -06:00
Jason P
4a35fd8317 Merge branch 'develop' into multi-message-Storage 2025-12-10 16:39:11 -06:00
Jason P
ff0a4ea320 Update System Frame for improved rendering on devices (#8923) 2025-12-10 16:30:26 -06:00
Jonathan Bennett
83b603827c Enable Muzi-base LED notification (#8925) 2025-12-10 16:29:50 -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
2032ff1c32 Create new screen colors for BaseUI (#8921)
* Create new colors for BaseUI

* Update Ice color
2025-12-10 11:09:37 -06:00
Alex Samorukov
5910cc2e26 Use PSRAM to reduce heap usage percentage on ESP32 with PSRAM (#8891)
* Use PSRAM for malloc > 256bytes to get more heap memory

* Use dynamic allocator on boards with PSRAM to free more heap

* Apply suggestion from @Copilot

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

* Move heap_caps_malloc_extmem_enable() to the top of the init

* Update src/main.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-10 06:23:23 -06:00
github-actions[bot]
ee80ec7b68 Upgrade trunk (#8922)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-10 06:14:00 -06:00
Jason P
9d5b69f9b4 Merge branch 'develop' into multi-message-Storage 2025-12-09 17:17:13 -06:00
Austin
aa72e397f2 PIO: Fix closedcube lib reference (#8920)
Fixes ClosedCube reinstalling on every build
2025-12-09 16:40:37 -06:00
renovate[bot]
ec0dfb7337 Update peter-evans/create-pull-request action to v8 (#8919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:56:27 -06:00
Austin
c55bea8460 ARCtastic (#8904) -- Do It Live!
Actions Runner Controller

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2025-12-09 15:11:07 -06:00
Austin
aa605fc4a2 Actions: Fix release manifest formating (#8918) 2025-12-09 14:27:13 -06:00
Igor Danilov
d75680a2dd Fix #8915 [Bug]: Exception Decoder does not recognize the backtrace (#8917) 2025-12-09 12:24:41 -06:00
Austin
817f3b9ec8 Update platformio/espressif32 to v6.12.0 (#7697) 2025-12-09 09:57:02 -06:00
Jason P
d4b44b1c63 Merge branch 'develop' into multi-message-Storage 2025-12-09 08:56:59 -06:00
Ben Meadors
decd58cd5c Merge pull request #8913 from meshtastic/revert-8858-nrf52-power-saving-1
Revert "Cut NRF52 bluetooth power usage by 300% - testers needed!"
2025-12-09 08:02:29 -06:00
Ben Meadors
e691bd9732 Revert "Cut NRF52 bluetooth power usage by 300% - testers needed! (#8858)"
This reverts commit ae8d3fbb3d.
2025-12-09 08:02:04 -06:00
Jason P
5ae962731b Merge branch 'develop' into multi-message-Storage 2025-12-09 07:41:02 -06:00
Ben Meadors
6bad81f8dd Merge pull request #8911 from vidplace7/fix-chmod
Fix apply device-install permissions
2025-12-09 06:50:19 -06:00
Austin Lane
69b9977fc1 Fix apply device-install permissions
device-install.sh doesn't exist for non-esp32 targets
2025-12-09 07:48:30 -05: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
Ben Meadors
0726bb4b56 Merge pull request #8910 from meshtastic/develop
Develop to master
2025-12-09 06:04:59 -06:00
github-actions[bot]
6b11991be0 Upgrade trunk (#8856)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-09 06:03:52 -06:00
Ben Meadors
8e63dcf59a Merge branch 'master' into develop 2025-12-09 05:59:15 -06:00
Lewis He
042543eb25 Fixed the issue where T-Echo did not completely shut down peripherals upon power-off. (#8524)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-12-09 05:39:27 -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
phaseloop
ae8d3fbb3d Cut NRF52 bluetooth power usage by 300% - testers needed! (#8858)
* Improve NRF52 bluetooth power efficiency

* test T114 bad LFXO

* T1000 test

* force BLE param negotiation

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-12-08 19:59:14 -06:00
Austin
928739e0fb Renovate: fix malformed comment for wollewald/BH1750_WE (#8767) 2025-12-08 19:31:28 -06:00
Ben Meadors
8be7915fc7 Fix wm111111110 2025-12-08 19:19:10 -06:00
Ben Meadors
c052963395 Guard 2M PHY mode for NimBLE (#8890)
* Guard 2M PHY mode for NimBLE

* Update src/nimble/NimbleBluetooth.cpp

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>

* Another #endif snuck in there

* Move endif

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 18:48:28 -06:00
Jonathan Bennett
65c418d4e1 Update protobuf name of FRIED_CHICKEN (#8903) 2025-12-09 11:13:59 +11:00
Jonathan Bennett
c3a69a2742 Fix backwards buttons on Thinknode-M1 (#8901) 2025-12-08 17:58:23 -06:00
Austin
66ff1536f3 Meshtastic build manifest (#8248) 2025-12-08 17:21:23 -06:00
Jason P
2bcf342d3d Merge branch 'develop' into multi-message-Storage 2025-12-08 14:47:34 -06:00
simon-muzi
5671e9d96f Improved R1 Neo & muzi-base buzzer beeps for GPS on/off (#8870)
Matched the resonant frequency of the hardware buzzer to maximize volume for the turn on beep.

Further distinguished ON beep from OFF beep, making it easier for users to understand the state change.
2025-12-08 13:50:05 -06:00
Manuel
bd4bcb94f0 tryfix eink parameters (#8898) 2025-12-08 13:14:24 -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
Igor Danilov
4b2f241478 Disable vibration if needed (#8895) 2025-12-08 06:03:20 -06:00
Wilson
eb087849c0 OnScreenKeyboard Improvement with Joystick and UpDown Encoder (#8379)
* Add mesh/Default.h include.

* Reflacter OnScreenKeyBoard Module, do not interrupt keyboard when new message comes.

* feat: Add long press scrolling for Joystick and upDown Encoder on baseUI frames and menus.

* refactor: Clean up code formatting and improve readability in Screen and OnScreenKeyboardModule

* Fix navigation on UpDownEncoder, default was RotaryEncoder while bringing the T_LORA_PAGER

* Update src/graphics/draw/MenuHandler.cpp

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

* Update src/modules/OnScreenKeyboardModule.h

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

* Update src/graphics/draw/NotificationRenderer.cpp

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

* Optimize the detection logic for repeated events of the arrow keys.

* Fixed parameter names in the OnScreenKeyboardModule::start

* Trunk fix

* Reflator OnScreenKeyboard Input checking, make it simple

* Simplify long press logic in OnScreenKeyboardModule.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 05:40:30 -06:00
Jason P
b86a4c0b43 Merge branch 'develop' into multi-message-Storage 2025-12-07 21:52:42 -06:00
Igor Danilov
94aedff6ae Resolve #8887 (T-LoRaPager Vibration on New Message Delivery) (#8888)
* Resolve #8887

* Update src/modules/ExternalNotificationModule.cpp

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

* Update src/modules/ExternalNotificationModule.cpp

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

* use canBuzz method

* trunk fmt

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-07 20:29:18 -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
github-actions[bot]
eeaafda62a Update protobufs (#8871)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-12-05 10:41:48 -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
renovate[bot]
6e9fd189b4 Update meshtastic/device-ui digest to 4fb5f24 (#8862)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 15:33:19 -06:00
renovate[bot]
3f40916223 Update alpine Docker tag to v3.23 (#8853)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 15:30:09 -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
github-actions[bot]
1b4925bd07 Upgrade trunk (#8849)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-03 07:50:50 -06:00
renovate[bot]
0828c445fb Update actions/stale action to v10.1.1 (#8848)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 05:39:31 -06:00
Jason P
0b43c5b52d Merge branch 'develop' into multi-message-Storage 2025-12-02 08:42:01 -06:00
github-actions[bot]
90584359e4 Upgrade trunk (#8836)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-02 05:48:36 -06:00
Jonathan Bennett
8a43741589 Add 'cleanup' to required PR labels (#8835) 2025-12-02 05:48:19 -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
247 changed files with 5348 additions and 2628 deletions

View File

@@ -51,7 +51,7 @@ for f in .clusterfuzzlite/*_fuzzer.cpp; do
fuzzer=$(basename "$f" .cpp) fuzzer=$(basename "$f" .cpp)
cp -f "$f" src/fuzzer.cpp cp -f "$f" src/fuzzer.cpp
pio run -vvv --environment "$PIO_ENV" pio run -vvv --environment "$PIO_ENV"
program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/program" program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/meshtasticd"
cp "$program" "$OUT/$fuzzer" cp "$program" "$OUT/$fuzzer"
# Copy shared libraries used by the fuzzer. # Copy shared libraries used by the fuzzer.

View File

@@ -2,4 +2,5 @@
self-hosted-runner: self-hosted-runner:
# Labels of self-hosted runner in array of strings. # Labels of self-hosted runner in array of strings.
labels: labels:
- arctastic
- test-runner - test-runner

View File

@@ -76,7 +76,7 @@ runs:
done done
- name: PlatformIO ${{ inputs.arch }} download cache - name: PlatformIO ${{ inputs.arch }} download cache
uses: actions/cache@v4 uses: actions/cache@v5
with: with:
path: ~/.platformio/.cache path: ~/.platformio/.cache
key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }} key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }}
@@ -100,9 +100,9 @@ runs:
id: version id: version
- name: Store binaries as an artifact - name: Store binaries as an artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}
overwrite: true overwrite: true
path: | path: |
${{ inputs.artifact-paths }} ${{ inputs.artifact-paths }}

View File

@@ -64,7 +64,7 @@ jobs:
PKG_VERSION: ${{ steps.version.outputs.deb }} PKG_VERSION: ${{ steps.version.outputs.deb }}
- name: Store binaries as an artifact - name: Store binaries as an artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
overwrite: true overwrite: true

View File

@@ -18,7 +18,8 @@ permissions: read-all
jobs: jobs:
pio-build: pio-build:
name: build-${{ inputs.platform }} name: build-${{ inputs.platform }}
runs-on: ubuntu-24.04 # Use 'arctastic' self-hosted runner pool when building in the main repo
runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }}
outputs: outputs:
artifact-id: ${{ steps.upload.outputs.artifact-id }} artifact-id: ${{ steps.upload.outputs.artifact-id }}
steps: steps:
@@ -55,15 +56,31 @@ jobs:
ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }} ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }} ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
- name: Job summary
env:
PIO_ENV: ${{ inputs.pio_env }}
run: |
echo "## $PIO_ENV" >> $GITHUB_STEP_SUMMARY
echo "<details><summary><strong>Manifest</strong></summary>" >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
cat release/firmware-*.mt.json >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Store binaries as an artifact - name: Store binaries as an artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
id: upload id: upload
with: with:
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
overwrite: true overwrite: true
path: | path: |
release/*.mt.json
release/*.bin release/*.bin
release/*.elf release/*.elf
release/*.uf2 release/*.uf2
release/*.hex release/*.hex
release/*-ota.zip release/*.zip
release/device-*.sh
release/device-*.bat

View File

@@ -98,7 +98,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v7
with: with:
path: ./ path: ./
pattern: firmware-*-* pattern: firmware-*-*
@@ -111,7 +111,7 @@ jobs:
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip - name: Repackage in single firmware zip
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
@@ -119,7 +119,7 @@ jobs:
./firmware-*.bin ./firmware-*.bin
./firmware-*.uf2 ./firmware-*.uf2
./firmware-*.hex ./firmware-*.hex
./firmware-*-ota.zip ./firmware-*.zip
./device-*.sh ./device-*.sh
./device-*.bat ./device-*.bat
./littlefs-*.bin ./littlefs-*.bin
@@ -127,7 +127,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2 ./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30 retention-days: 30
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v7
with: with:
pattern: firmware-*-${{ needs.version.outputs.long }} pattern: firmware-*-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -139,14 +139,14 @@ jobs:
- name: Device scripts permissions - name: Device scripts permissions
run: | run: |
chmod +x ./output/device-install.sh chmod +x ./output/device-install.sh || true
chmod +x ./output/device-update.sh chmod +x ./output/device-update.sh || true
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip - name: Repackage in single elfs zip
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
overwrite: true overwrite: true

View File

@@ -77,16 +77,21 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
check: ${{ fromJson(needs.setup.outputs.check) }} check: ${{ fromJson(needs.setup.outputs.check) }}
# Use 'arctastic' self-hosted runner pool when checking in the main repo
runs-on: ubuntu-latest runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }}
if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- name: Build base with:
id: base submodules: recursive
uses: ./.github/actions/setup-base ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Check ${{ matrix.check.board }} - name: Check ${{ matrix.check.board }}
run: bin/check-all.sh ${{ matrix.check.board }} uses: meshtastic/gh-action-firmware@main
with:
pio_platform: ${{ matrix.check.platform }}
pio_env: ${{ matrix.check.board }}
pio_target: check
build: build:
needs: [setup, version] needs: [setup, version]
@@ -168,7 +173,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v7
with: with:
path: ./ path: ./
pattern: firmware-${{matrix.arch}}-* pattern: firmware-${{matrix.arch}}-*
@@ -177,19 +182,17 @@ jobs:
- name: Display structure of downloaded files - name: Display structure of downloaded files
run: ls -R run: ls -R
- name: Move files up
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip - name: Repackage in single firmware zip
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
path: | path: |
./firmware-*.mt.json
./firmware-*.bin ./firmware-*.bin
./firmware-*.uf2 ./firmware-*.uf2
./firmware-*.hex ./firmware-*.hex
./firmware-*-ota.zip ./firmware-*.zip
./device-*.sh ./device-*.sh
./device-*.bat ./device-*.bat
./littlefs-*.bin ./littlefs-*.bin
@@ -197,7 +200,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2 ./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30 retention-days: 30
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v7
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -209,16 +212,16 @@ jobs:
- name: Device scripts permissions - name: Device scripts permissions
run: | run: |
chmod +x ./output/device-install.sh chmod +x ./output/device-install.sh || true
chmod +x ./output/device-update.sh chmod +x ./output/device-update.sh || true
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip - name: Repackage in single elfs zip
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
path: ./*.elf path: ./*.elf
retention-days: 30 retention-days: 30
@@ -236,6 +239,7 @@ jobs:
outputs: outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
needs: needs:
- setup
- version - version
- gather-artifacts - gather-artifacts
- build-debian-src - build-debian-src
@@ -244,11 +248,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Create release - name: Create release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
id: create_release id: create_release
@@ -261,14 +260,14 @@ jobs:
Autogenerated by github action, developer should edit as required before publishing... Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb - name: Download source deb
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true merge-multiple: true
path: ./output/debian-src path: ./output/debian-src
- name: Download `native-tft` pio deps - name: Download `native-tft` pio deps
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -284,10 +283,25 @@ jobs:
- name: Display structure of downloaded files - name: Display structure of downloaded files
run: ls -lR run: ls -lR
- name: Add Linux sources to GtiHub Release - name: Generate Release manifest
run: |
jq -n --arg ver "${{ needs.version.outputs.long }}" --argjson targets ${{ toJson(needs.setup.outputs.all) }} '{
"version": $ver,
"targets": $targets
}' > firmware-${{ needs.version.outputs.long }}.json
- name: Save Release manifest artifact
uses: actions/upload-artifact@v6
with:
name: manifest-${{ needs.version.outputs.long }}
overwrite: true
path: firmware-${{ needs.version.outputs.long }}.json
- name: Add sources to GitHub Release
# Only run when targeting master branch with workflow_dispatch # Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }} if: ${{ github.ref_name == 'master' }}
run: | run: |
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{ needs.version.outputs.long }}.json
gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip
gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip
env: env:
@@ -318,7 +332,7 @@ jobs:
with: with:
python-version: 3.x python-version: 3.x
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v7
with: with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -329,15 +343,15 @@ jobs:
- name: Device scripts permissions - name: Device scripts permissions
run: | run: |
chmod +x ./output/device-install.sh chmod +x ./output/device-install.sh || true
chmod +x ./output/device-update.sh chmod +x ./output/device-update.sh || true
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v7
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
path: ./elfs path: ./elfs
@@ -373,12 +387,19 @@ jobs:
with: with:
python-version: 3.x python-version: 3.x
- uses: actions/download-artifact@v6 - name: Get firmware artifacts
uses: actions/download-artifact@v7
with: with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
path: ./publish path: ./publish
- name: Get manifest artifact
uses: actions/download-artifact@v7
with:
pattern: manifest-${{ needs.version.outputs.long }}
path: ./publish
- name: Publish firmware to meshtastic.github.io - name: Publish firmware to meshtastic.github.io
uses: peaceiris/actions-gh-pages@v4 uses: peaceiris/actions-gh-pages@v4
env: env:

View File

@@ -147,7 +147,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v7
with: with:
path: ./ path: ./
pattern: firmware-${{matrix.arch}}-* pattern: firmware-${{matrix.arch}}-*
@@ -160,7 +160,7 @@ jobs:
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip - name: Repackage in single firmware zip
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
@@ -168,7 +168,7 @@ jobs:
./firmware-*.bin ./firmware-*.bin
./firmware-*.uf2 ./firmware-*.uf2
./firmware-*.hex ./firmware-*.hex
./firmware-*-ota.zip ./firmware-*.zip
./device-*.sh ./device-*.sh
./device-*.bat ./device-*.bat
./littlefs-*.bin ./littlefs-*.bin
@@ -176,7 +176,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2 ./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30 retention-days: 30
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v7
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -188,16 +188,16 @@ jobs:
- name: Device scripts permissions - name: Device scripts permissions
run: | run: |
chmod +x ./output/device-install.sh chmod +x ./output/device-install.sh || true
chmod +x ./output/device-update.sh chmod +x ./output/device-update.sh || true
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip - name: Repackage in single elfs zip
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
path: ./*.elf path: ./*.elf
retention-days: 30 retention-days: 30
@@ -223,11 +223,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Create release - name: Create release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
id: create_release id: create_release
@@ -240,14 +235,14 @@ jobs:
Autogenerated by github action, developer should edit as required before publishing... Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb - name: Download source deb
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true merge-multiple: true
path: ./output/debian-src path: ./output/debian-src
- name: Download `native-tft` pio deps - name: Download `native-tft` pio deps
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -297,7 +292,7 @@ jobs:
with: with:
python-version: 3.x python-version: 3.x
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v7
with: with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -308,15 +303,15 @@ jobs:
- name: Device scripts permissions - name: Device scripts permissions
run: | run: |
chmod +x ./output/device-install.sh chmod +x ./output/device-install.sh || true
chmod +x ./output/device-update.sh chmod +x ./output/device-update.sh || true
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v7
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
path: ./elfs path: ./elfs
@@ -352,7 +347,7 @@ jobs:
with: with:
python-version: 3.x python-version: 3.x
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v7
with: with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true

View File

@@ -58,7 +58,7 @@ jobs:
id: version id: version
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true merge-multiple: true

View File

@@ -56,7 +56,7 @@ jobs:
PLATFORMIO_CORE_DIR: pio/core PLATFORMIO_CORE_DIR: pio/core
- name: Store binaries as an artifact - name: Store binaries as an artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }} name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }}
overwrite: true overwrite: true

View File

@@ -60,7 +60,7 @@ jobs:
id: version id: version
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true merge-multiple: true

View File

@@ -17,7 +17,7 @@ jobs:
with: with:
script: | script: |
const labels = context.payload.pull_request.labels.map(label => label.name); const labels = context.payload.pull_request.labels.map(label => label.name);
const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk']; const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup'];
const hasRequiredLabel = labels.some(label => requiredLabels.includes(label)); const hasRequiredLabel = labels.some(label => requiredLabels.includes(label));
if (!hasRequiredLabel) { if (!hasRequiredLabel) {
core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`); core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`);

View File

@@ -50,9 +50,9 @@ jobs:
- name: Download test artifacts - name: Download test artifacts
if: needs.native-tests.result != 'skipped' if: needs.native-tests.result != 'skipped'
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
name: platformio-test-report-${{ steps.version.outputs.long }}.zip name: platformio-test-report-${{ steps.version.outputs.long }}
merge-multiple: true merge-multiple: true
- name: Parse test results and create detailed summary - name: Parse test results and create detailed summary

View File

@@ -102,7 +102,7 @@ jobs:
PIP_DISABLE_PIP_VERSION_CHECK: 1 PIP_DISABLE_PIP_VERSION_CHECK: 1
- name: Create Bumps pull request - name: Create Bumps pull request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v8
with: with:
base: ${{ github.event.repository.default_branch }} base: ${{ github.event.repository.default_branch }}
branch: create-pull-request/bump-version branch: create-pull-request/bump-version

View File

@@ -33,7 +33,7 @@ jobs:
# step 3 # step 3
- name: save report as pipeline artifact - name: save report as pipeline artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: report.sarif name: report.sarif
overwrite: true overwrite: true

View File

@@ -17,7 +17,7 @@ jobs:
steps: steps:
- name: Stale PR+Issues - name: Stale PR+Issues
uses: actions/stale@v10.1.0 uses: actions/stale@v10.1.1
with: with:
days-before-stale: 45 days-before-stale: 45
stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days. stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days.

View File

@@ -40,7 +40,7 @@ jobs:
- name: Integration test - name: Integration test
run: | run: |
.pio/build/coverage/program -s & .pio/build/coverage/meshtasticd -s &
PID=$! PID=$!
timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done" timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done"
echo "Simulator started, launching python test..." echo "Simulator started, launching python test..."
@@ -59,10 +59,10 @@ jobs:
id: version id: version
- name: Save coverage information - name: Save coverage information
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
if: always() # run this step even if previous step failed if: always() # run this step even if previous step failed
with: with:
name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}.zip name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}
overwrite: true overwrite: true
path: ./coverage_*.info path: ./coverage_*.info
@@ -94,9 +94,9 @@ jobs:
- name: Save test results - name: Save test results
if: always() # run this step even if previous step failed if: always() # run this step even if previous step failed
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: platformio-test-report-${{ steps.version.outputs.long }}.zip name: platformio-test-report-${{ steps.version.outputs.long }}
overwrite: true overwrite: true
path: ./testreport.xml path: ./testreport.xml
@@ -108,10 +108,10 @@ jobs:
sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative. sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative.
- name: Save coverage information - name: Save coverage information
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
if: always() # run this step even if previous step failed if: always() # run this step even if previous step failed
with: with:
name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}.zip name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}
overwrite: true overwrite: true
path: ./coverage_*.info path: ./coverage_*.info
@@ -137,9 +137,9 @@ jobs:
id: version id: version
- name: Download test artifacts - name: Download test artifacts
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
name: platformio-test-report-${{ steps.version.outputs.long }}.zip name: platformio-test-report-${{ steps.version.outputs.long }}
merge-multiple: true merge-multiple: true
- name: Test Report - name: Test Report
@@ -150,9 +150,9 @@ jobs:
reporter: java-junit reporter: java-junit
- name: Download coverage artifacts - name: Download coverage artifacts
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}
path: code-coverage-report path: code-coverage-report
merge-multiple: true merge-multiple: true
@@ -163,7 +163,7 @@ jobs:
genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report
- name: Save Code Coverage Report - name: Save Code Coverage Report
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: code-coverage-report-${{ steps.version.outputs.long }}.zip name: code-coverage-report-${{ steps.version.outputs.long }}
path: code-coverage-report path: code-coverage-report

View File

@@ -22,7 +22,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
# - uses: actions/setup-python@v5 # - uses: actions/setup-python@v6
# with: # with:
# python-version: '3.10' # python-version: '3.10'

View File

@@ -31,7 +31,7 @@ jobs:
./bin/regen-protos.sh ./bin/regen-protos.sh
- name: Create pull request - name: Create pull request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v8
with: with:
branch: create-pull-request/update-protobufs branch: create-pull-request/update-protobufs
labels: submodules labels: submodules

View File

@@ -9,24 +9,24 @@ plugins:
lint: lint:
enabled: enabled:
- checkov@3.2.495 - checkov@3.2.495
- renovate@42.27.1 - renovate@42.57.1
- prettier@3.7.3 - prettier@3.7.4
- trufflehog@3.91.1 - trufflehog@3.92.3
- yamllint@1.37.1 - yamllint@1.37.1
- bandit@1.9.2 - bandit@1.9.2
- trivy@0.67.2 - trivy@0.68.1
- taplo@0.10.0 - taplo@0.10.0
- ruff@0.14.7 - ruff@0.14.9
- isort@7.0.0 - isort@7.0.0
- markdownlint@0.46.0 - markdownlint@0.47.0
- oxipng@9.1.5 - oxipng@10.0.0
- svgo@4.0.0 - svgo@4.0.0
- actionlint@1.7.9 - actionlint@1.7.9
- flake8@7.3.0 - flake8@7.3.0
- hadolint@2.14.0 - hadolint@2.14.0
- shfmt@3.6.0 - shfmt@3.6.0
- shellcheck@0.11.0 - shellcheck@0.11.0
- black@25.11.0 - black@25.12.0
- git-diff-check - git-diff-check
- gitleaks@8.30.0 - gitleaks@8.30.0
- clang-format@16.0.3 - clang-format@16.0.3

View File

@@ -28,7 +28,7 @@ RUN bash ./bin/build-native.sh "$PIO_ENV" && \
# ##### PRODUCTION BUILD ############# # ##### PRODUCTION BUILD #############
FROM alpine:3.22 FROM alpine:3.23
LABEL org.opencontainers.image.title="Meshtastic" \ LABEL org.opencontainers.image.title="Meshtastic" \
org.opencontainers.image.description="Alpine Meshtastic daemon" \ org.opencontainers.image.description="Alpine Meshtastic daemon" \
org.opencontainers.image.url="https://meshtastic.org" \ org.opencontainers.image.url="https://meshtastic.org" \

View File

@@ -5,7 +5,8 @@ set -e
VERSION=`bin/buildinfo.py long` VERSION=`bin/buildinfo.py long`
SHORT_VERSION=`bin/buildinfo.py short` SHORT_VERSION=`bin/buildinfo.py short`
OUTDIR=release/ BUILDDIR=.pio/build/$1
OUTDIR=release
rm -f $OUTDIR/firmware* rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
@@ -14,33 +15,27 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1 platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f .pio/build/$1/firmware.* rm -f $BUILDDIR/firmware*
# The shell vars the build tool expects to find # The shell vars the build tool expects to find
export APP_VERSION=$VERSION export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION basename=firmware-$1-$VERSION
pio run --environment $1 # -v pio run --environment $1 -t mtjson # -v
SRCELF=.pio/build/$1/firmware.elf
cp $SRCELF $OUTDIR/$basename.elf cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
echo "Copying ESP32 bin file" echo "Copying ESP32 bin file"
SRCBIN=.pio/build/$1/firmware.factory.bin cp $BUILDDIR/$basename.factory.bin $OUTDIR/$basename.factory.bin
cp $SRCBIN $OUTDIR/$basename.bin
echo "Copying ESP32 update bin file" echo "Copying ESP32 update bin file"
SRCBIN=.pio/build/$1/firmware.bin cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
cp $SRCBIN $OUTDIR/$basename-update.bin
echo "Building Filesystem for ESP32 targets" echo "Copying Filesystem for ESP32 targets"
# If you want to build the webui, uncomment the following lines cp $BUILDDIR/littlefs-$1-$VERSION.bin $OUTDIR/littlefs-$1-$VERSION.bin
# pio run --environment $1 -t buildfs cp bin/device-install.* $OUTDIR/
# cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin cp bin/device-update.* $OUTDIR/
# # Remove webserver files from the filesystem and rebuild
# ls -l data/static # Diagnostic list of files echo "Copying manifest"
# rm -rf data/static cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
pio run --environment $1 -t buildfs
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin
cp bin/device-install.* $OUTDIR
cp bin/device-update.* $OUTDIR

View File

@@ -17,15 +17,19 @@ VERSION=$(bin/buildinfo.py long)
SHORT_VERSION=$(bin/buildinfo.py short) SHORT_VERSION=$(bin/buildinfo.py short)
PIO_ENV=${1:-native} PIO_ENV=${1:-native}
OUTDIR=release/ BUILDDIR=.pio/build/$PIO_ENV
OUTDIR=release
rm -f $OUTDIR/firmware* rm -f $OUTDIR/meshtasticd*
mkdir -p $OUTDIR/ mkdir -p $OUTDIR/
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
basename=meshtasticd-$1-$VERSION
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
pio pkg install --environment "$PIO_ENV" || platformioFailed pio pkg install --environment "$PIO_ENV" || platformioFailed
pio run --environment "$PIO_ENV" || platformioFailed pio run --environment "$PIO_ENV" || platformioFailed
cp ".pio/build/$PIO_ENV/program" "$OUTDIR/meshtasticd_linux_$(uname -m)"
cp bin/native-install.* $OUTDIR cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)"
cp bin/native-install.* $OUTDIR/

View File

@@ -5,7 +5,8 @@ set -e
VERSION=$(bin/buildinfo.py long) VERSION=$(bin/buildinfo.py long)
SHORT_VERSION=$(bin/buildinfo.py short) SHORT_VERSION=$(bin/buildinfo.py short)
OUTDIR=release/ BUILDDIR=.pio/build/$1
OUTDIR=release
rm -f $OUTDIR/firmware* rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
@@ -14,40 +15,37 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1 platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f .pio/build/$1/firmware.* rm -f $BUILDDIR/firmware*
# The shell vars the build tool expects to find # The shell vars the build tool expects to find
export APP_VERSION=$VERSION export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION basename=firmware-$1-$VERSION
pio run --environment $1 # -v pio run --environment $1 -t mtjson # -v
SRCELF=.pio/build/$1/firmware.elf
cp $SRCELF $OUTDIR/$basename.elf
echo "Generating NRF52 dfu file" cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
DFUPKG=.pio/build/$1/firmware.zip
cp $DFUPKG $OUTDIR/$basename-ota.zip
echo "Generating NRF52 uf2 file" echo "Copying NRF52 dfu (OTA) file"
SRCHEX=.pio/build/$1/firmware.hex cp $BUILDDIR/$basename.zip $OUTDIR/$basename.zip
# if WM1110 target, merge hex with softdevice 7.3.0 echo "Copying NRF52 UF2 file"
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
cp bin/*.uf2 $OUTDIR/
SRCHEX=$BUILDDIR/$basename.hex
# if WM1110 target, copy the merged.hex
if (echo $1 | grep -q "wio-sdk-wm1110"); then if (echo $1 | grep -q "wio-sdk-wm1110"); then
echo "Merging with softdevice" echo "Copying .merged.hex file"
bin/mergehex -m bin/s140_nrf52_7.3.0_softdevice.hex $SRCHEX -o .pio/build/$1/$basename.hex SRCHEX=$BUILDDIR/$basename.merged.hex
SRCHEX=.pio/build/$1/$basename.hex cp $SRCHEX $OUTDIR/
bin/uf2conv.py $SRCHEX -c -o $OUTDIR/$basename.uf2 -f 0xADA52840
cp $SRCHEX $OUTDIR
cp bin/*.uf2 $OUTDIR
else
bin/uf2conv.py $SRCHEX -c -o $OUTDIR/$basename.uf2 -f 0xADA52840
cp bin/device-install.* $OUTDIR
cp bin/device-update.* $OUTDIR
cp bin/*.uf2 $OUTDIR
fi fi
if (echo $1 | grep -q "rak4631"); then if (echo $1 | grep -q "rak4631"); then
echo "Copying hex file" echo "Copying .hex file"
cp .pio/build/$1/firmware.hex $OUTDIR/$basename.hex cp $SRCHEX $OUTDIR/
fi fi
echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json

View File

@@ -5,7 +5,8 @@ set -e
VERSION=`bin/buildinfo.py long` VERSION=`bin/buildinfo.py long`
SHORT_VERSION=`bin/buildinfo.py short` SHORT_VERSION=`bin/buildinfo.py short`
OUTDIR=release/ BUILDDIR=.pio/build/$1
OUTDIR=release
rm -f $OUTDIR/firmware* rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
@@ -14,20 +15,19 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1 platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f .pio/build/$1/firmware.* rm -f $BUILDDIR/firmware*
# The shell vars the build tool expects to find # The shell vars the build tool expects to find
export APP_VERSION=$VERSION export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION basename=firmware-$1-$VERSION
pio run --environment $1 # -v pio run --environment $1 -t mtjson # -v
SRCELF=.pio/build/$1/firmware.elf
cp $SRCELF $OUTDIR/$basename.elf cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
echo "Copying uf2 file" echo "Copying uf2 file"
SRCBIN=.pio/build/$1/firmware.uf2 cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
cp $SRCBIN $OUTDIR/$basename.uf2
cp bin/device-install.* $OUTDIR echo "Copying manifest"
cp bin/device-update.* $OUTDIR cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json

View File

@@ -5,7 +5,8 @@ set -e
VERSION=$(bin/buildinfo.py long) VERSION=$(bin/buildinfo.py long)
SHORT_VERSION=$(bin/buildinfo.py short) SHORT_VERSION=$(bin/buildinfo.py short)
OUTDIR=release/ BUILDDIR=.pio/build/$1
OUTDIR=release
rm -f $OUTDIR/firmware* rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
@@ -14,16 +15,19 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1 platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f .pio/build/$1/firmware.* rm -f $BUILDDIR/firmware*
# The shell vars the build tool expects to find # The shell vars the build tool expects to find
export APP_VERSION=$VERSION export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION basename=firmware-$1-$VERSION
pio run --environment $1 # -v pio run --environment $1 -t mtjson # -v
SRCELF=.pio/build/$1/firmware.elf
cp $SRCELF $OUTDIR/$basename.elf
SRCBIN=.pio/build/$1/firmware.bin cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
cp $SRCBIN $OUTDIR/$basename.bin
echo "Copying STM32 bin file"
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json

View File

@@ -184,6 +184,8 @@ Input:
Logging: Logging:
LogLevel: info # debug, info, warn, error LogLevel: info # debug, info, warn, error
# TraceFile: /var/log/meshtasticd.json # TraceFile: /var/log/meshtasticd.json
# JSONFile: /packets.json # File location for JSON output of decoded packets
# JSONFilter: position # filter for packets to save to JSON file
# AsciiLogs: true # default if not specified is !isatty() on stdout # AsciiLogs: true # default if not specified is !isatty() on stdout
Webserver: Webserver:

View File

@@ -5,22 +5,14 @@ TITLE Meshtastic device-install
SET "SCRIPT_NAME=%~nx0" SET "SCRIPT_NAME=%~nx0"
SET "DEBUG=0" SET "DEBUG=0"
SET "PYTHON=" SET "PYTHON="
SET "TFT_BUILD=0"
SET "BIGDB8=0"
SET "MUIDB8=0"
SET "BIGDB16=0"
SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_BAUD=115200"
SET "ESPTOOL_CMD=" SET "ESPTOOL_CMD="
SET "LOGCOUNTER=0" SET "LOGCOUNTER=0"
SET "BPS_RESET=0" SET "BPS_RESET=0"
@REM Default offsets.
@REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable. @REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202
SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv heltec-v4" SET "OTA_OFFSET=0x260000"
SET "C3=esp32c3" SET "SPIFFS_OFFSET=0x300000"
@REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable.
SET "BIGDB_8MB=crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger"
SET "MUIDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator"
SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv heltec-v4"
GOTO getopts GOTO getopts
:help :help
@@ -29,7 +21,7 @@ ECHO.
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--1200bps-reset] ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--1200bps-reset]
ECHO. ECHO.
ECHO Options: ECHO Options:
ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required) ECHO -f filename The firmware .factory.bin file to flash. Custom to your device type and region. (required)
ECHO The file must be located in this current directory. ECHO The file must be located in this current directory.
ECHO -p PORT Set the environment variable for ESPTOOL_PORT. ECHO -p PORT Set the environment variable for ESPTOOL_PORT.
ECHO If not set, ESPTOOL iterates all ports (Dangerous). ECHO If not set, ESPTOOL iterates all ports (Dangerous).
@@ -40,12 +32,12 @@ ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps
ECHO Some hardware requires this twice. ECHO Some hardware requires this twice.
ECHO. ECHO.
ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11 ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.factory.bin -p COM11
ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11 ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.factory.bin -p COM11
GOTO eof GOTO eof
:version :version
ECHO %SCRIPT_NAME% [Version 2.6.2] ECHO %SCRIPT_NAME% [Version 2.7.0]
ECHO Meshtastic ECHO Meshtastic
GOTO eof GOTO eof
@@ -78,8 +70,8 @@ IF "__!FILENAME!__"=="____" (
CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported." CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported."
GOTO help GOTO help
) )
IF "__!FILENAME:firmware-=!__"=="__!FILENAME!__" ( IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" (
CALL :LOG_MESSAGE ERROR "Filename must be a firmware-* file." CALL :LOG_MESSAGE ERROR "Filename must be a firmware-*.factory.bin file."
GOTO help GOTO help
) )
@REM Remove ".\" or "./" file prefix if present. @REM Remove ".\" or "./" file prefix if present.
@@ -93,12 +85,26 @@ IF NOT EXIST !FILENAME! (
GOTO eof GOTO eof
) )
IF NOT "!FILENAME:update=!"=="!FILENAME!" ( CALL :LOG_MESSAGE DEBUG "Checking for metadata..."
CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!" @REM Derive metadata filename from firmware filename.
CALL :LOG_MESSAGE INFO "Use script device-update.bat to flash update !FILENAME!." SET "METAFILE=!FILENAME:.factory.bin=!.mt.json"
GOTO eof IF EXIST !METAFILE! (
@REM Print parsed json with powershell
CALL :LOG_MESSAGE INFO "Firmware metadata: !METAFILE!"
powershell -NoProfile -Command "(Get-Content '!METAFILE!' | ConvertFrom-Json | Out-String).Trim()"
@REM Save metadata values to variables for later use.
FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
"(Get-Content '!METAFILE!' | ConvertFrom-Json).mcu"`) DO SET "MCU=%%A"
FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
"(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'ota_1' } | Select-Object -ExpandProperty offset"`
) DO SET "OTA_OFFSET=%%A"
FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
"(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'spiffs' } | Select-Object -ExpandProperty offset"`
) DO SET "SPIFFS_OFFSET=%%A"
) ELSE ( ) ELSE (
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!" CALL :LOG_MESSAGE ERROR "No metadata file found: !METAFILE!"
GOTO eof
) )
:skip-filename :skip-filename
@@ -108,7 +114,7 @@ IF NOT "__%PYTHON%__"=="____" (
SET "ESPTOOL_CMD=!PYTHON! -m esptool" SET "ESPTOOL_CMD=!PYTHON! -m esptool"
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied." CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
) ELSE ( ) ELSE (
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool... CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..."
WHERE esptool >nul 2>&1 WHERE esptool >nul 2>&1
IF %ERRORLEVEL% EQU 0 ( IF %ERRORLEVEL% EQU 0 (
@REM WHERE exits with code 0 if esptool is found. @REM WHERE exits with code 0 if esptool is found.
@@ -146,100 +152,26 @@ IF %BPS_RESET% EQU 1 (
GOTO eof GOTO eof
) )
@REM Check if FILENAME contains "-tft-" and set target partitionScheme accordingly. @REM Extract PROGNAME from %FILENAME% for later use.
@REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3 SET "PROGNAME=!FILENAME:.factory.bin=!"
IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" ( CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
CALL :LOG_MESSAGE DEBUG "We are working with a *-tft-* file. !FILENAME!"
SET "TFT_BUILD=1" IF "__!MCU!__" == "__esp32s3__" (
@REM We are working with ESP32-S3
SET "OTA_FILENAME=bleota-s3.bin"
) ELSE IF "__!MCU!__" == "__esp32c3__" (
@REM We are working with ESP32-C3
SET "OTA_FILENAME=bleota-c3.bin"
) ELSE ( ) ELSE (
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *-tft-* file. !FILENAME!" @REM Everything else
SET "OTA_FILENAME=bleota.bin"
) )
FOR %%a IN (%BIGDB_8MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %BIGDB_8MB%.
SET "BIGDB8=1"
GOTO end_loop_bigdb_8mb
)
)
:end_loop_bigdb_8mb
FOR %%a IN (%MUIDB_8MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %MUIDB_8MB%.
SET "MUIDB8=1"
GOTO end_loop_muidb_8mb
)
)
:end_loop_muidb_8mb
FOR %%a IN (%BIGDB_16MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %BIGDB_16MB%.
SET "BIGDB16=1"
GOTO end_loop_bigdb_16mb
)
)
:end_loop_bigdb_16mb
IF %BIGDB8% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 8mb partition selected."
IF %MUIDB8% EQU 1 CALL :LOG_MESSAGE INFO "MUIDB 8mb partition selected."
IF %BIGDB16% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 16mb partition selected."
@REM Extract BASENAME from %FILENAME% for later use.
SET "BASENAME=!FILENAME:firmware-=!"
CALL :LOG_MESSAGE DEBUG "Computed firmware basename: !BASENAME!"
@REM Account for S3 and C3 board's different OTA partition.
FOR %%a IN (%S3%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %S3%.
SET "OTA_FILENAME=bleota-s3.bin"
GOTO :end_loop_s3
)
)
FOR %%a IN (%C3%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %C3%.
SET "OTA_FILENAME=bleota-c3.bin"
GOTO :end_loop_c3
)
)
@REM Everything else
SET "OTA_FILENAME=bleota.bin"
:end_loop_s3
:end_loop_c3
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!" CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
@REM Set SPIFFS filename with "littlefs-" prefix. @REM Set SPIFFS filename with "littlefs-" prefix.
SET "SPIFFS_FILENAME=littlefs-%BASENAME%" SET "SPIFFS_FILENAME=littlefs-!PROGNAME:firmware-=!.bin"
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!" CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!"
@REM Default offsets.
@REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202
SET "OTA_OFFSET=0x260000"
SET "SPIFFS_OFFSET=0x300000"
@REM Offsets for BigDB 8mb.
IF %BIGDB8% EQU 1 (
SET "OTA_OFFSET=0x340000"
SET "SPIFFS_OFFSET=0x670000"
)
@REM Offsets for MUIDB 8mb.
IF %MUIDB8% EQU 1 (
SET "OTA_OFFSET=0x5D0000"
SET "SPIFFS_OFFSET=0x670000"
)
@REM Offsets for BigDB 16mb.
IF %BIGDB16% EQU 1 (
SET "OTA_OFFSET=0x650000"
SET "SPIFFS_OFFSET=0xc90000"
)
CALL :LOG_MESSAGE DEBUG "Set OTA_OFFSET to: !OTA_OFFSET!" CALL :LOG_MESSAGE DEBUG "Set OTA_OFFSET to: !OTA_OFFSET!"
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_OFFSET to: !SPIFFS_OFFSET!" CALL :LOG_MESSAGE DEBUG "Set SPIFFS_OFFSET to: !SPIFFS_OFFSET!"

View File

@@ -2,69 +2,15 @@
PYTHON=${PYTHON:-$(which python3 python | head -n 1)} PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
BPS_RESET=false BPS_RESET=false
TFT_BUILD=false
MCU="" MCU=""
# Constants # Constants
RESET_BAUD=1200 RESET_BAUD=1200
FIRMWARE_OFFSET=0x00 FIRMWARE_OFFSET=0x00
# Default littlefs* offset.
# Variant groups OFFSET=0x300000
BIGDB_8MB=( # Default OTA Offset
"crowpanel-esp32s3" OTA_OFFSET=0x260000
"heltec_capsule_sensor_v3"
"heltec-v3"
"heltec-vision-master-e213"
"heltec-vision-master-e290"
"heltec-vision-master-t190"
"heltec-wireless-paper"
"heltec-wireless-tracker"
"heltec-wsl-v3"
"icarus"
"seeed-xiao-s3"
"tbeam-s3-core"
"tracksenger"
)
MUIDB_8MB=(
"picomputer-s3"
"unphone"
"seeed-sensecap-indicator"
)
BIGDB_16MB=(
"dreamcatcher"
"elecrow-adv"
"ESP32-S3-Pico"
"heltec-v4"
"m5stack-cores3"
"mesh-tab"
"station-g2"
"t-deck"
"t-energy-s3"
"t-eth-elite"
"t-watch-s3"
"tlora-pager"
)
S3_VARIANTS=(
"s3"
"-v3"
"-v4"
"t-deck"
"wireless-paper"
"wireless-tracker"
"station-g2"
"unphone"
"t-eth-elite"
"tlora-pager"
"mesh-tab"
"dreamcatcher"
"ESP32-S3-Pico"
"seeed-sensecap-indicator"
"heltec_capsule_sensor_v3"
"vision-master"
"icarus"
"tracksenger"
"elecrow-adv"
)
# Determine the correct esptool command to use # Determine the correct esptool command to use
if "$PYTHON" -m esptool version >/dev/null 2>&1; then if "$PYTHON" -m esptool version >/dev/null 2>&1; then
@@ -78,6 +24,14 @@ else
exit 1 exit 1
fi fi
# Check for jq
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq not found" >&2
echo "Install jq with your package manager." >&2
echo "e.g. 'apt install jq', 'dnf install jq', 'brew install jq', etc." >&2
exit 1
fi
set -e set -e
# Usage info # Usage info
@@ -89,7 +43,7 @@ Flash image file to device, but first erasing and writing system information.
-h Display this help and exit. -h Display this help and exit.
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous). -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON") -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
-f FILENAME The firmware .bin file to flash. Custom to your device type and region. -f FILENAME The firmware *.factory.bin file to flash. Custom to your device type and region.
--1200bps-reset Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset) --1200bps-reset Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
EOF EOF
@@ -138,69 +92,43 @@ fi
shift shift
} }
if [[ "$FILENAME" != firmware-* ]]; then if [[ $(basename "$FILENAME") != firmware-*.factory.bin ]]; then
echo "Filename must be a firmware-* file." echo "Filename must be a firmware-*.factory.bin file."
exit 1 exit 1
fi fi
# Check if FILENAME contains "-tft-" and set target partitionScheme accordingly. # Extract PROGNAME from %FILENAME% for later use.
if [[ "${FILENAME//-tft-/}" != "$FILENAME" ]]; then PROGNAME="${FILENAME/.factory.bin/}"
TFT_BUILD=true # Derive metadata filename from %PROGNAME%.
fi METAFILE="${PROGNAME}.mt.json"
# Extract BASENAME from %FILENAME% for later use. if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
BASENAME="${FILENAME/firmware-/}" # Display metadata if it exists
if [[ -f "$METAFILE" ]]; then
if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then echo "Firmware metadata: ${METAFILE}"
# Default littlefs* offset. jq . "$METAFILE"
OFFSET=0x300000 # Extract relevant fields from metadata
if [[ $(jq -r '.part' "$METAFILE") != "null" ]]; then
# Default OTA Offset OTA_OFFSET=$(jq -r '.part[] | select(.subtype == "ota_1") | .offset' "$METAFILE")
OTA_OFFSET=0x260000 SPIFFS_OFFSET=$(jq -r '.part[] | select(.subtype == "spiffs") | .offset' "$METAFILE")
# littlefs* offset for BigDB 8mb and OTA OFFSET.
for variant in "${BIGDB_8MB[@]}"; do
if [ -z "${FILENAME##*"$variant"*}" ]; then
OFFSET=0x670000
OTA_OFFSET=0x340000
fi
done
for variant in "${MUIDB_8MB[@]}"; do
if [ -z "${FILENAME##*"$variant"*}" ]; then
OFFSET=0x670000
OTA_OFFSET=0x5D0000
fi
done
# littlefs* offset for BigDB 16mb and OTA OFFSET.
for variant in "${BIGDB_16MB[@]}"; do
if [ -z "${FILENAME##*"$variant"*}" ]; then
OFFSET=0xc90000
OTA_OFFSET=0x650000
fi
done
# Account for S3 board's different OTA partition
# FIXME: Use PlatformIO info to determine MCU type, this is unmaintainable
for variant in "${S3_VARIANTS[@]}"; do
if [ -z "${FILENAME##*"$variant"*}" ]; then
MCU="esp32s3"
fi
done
if [ "$MCU" != "esp32s3" ]; then
if [ -n "${FILENAME##*"esp32c3"*}" ]; then
OTAFILE=bleota.bin
else
OTAFILE=bleota-c3.bin
fi fi
MCU=$(jq -r '.mcu' "$METAFILE")
else else
echo "ERROR: No metadata file found at ${METAFILE}"
exit 1
fi
# Determine OTA filename based on MCU type
if [ "$MCU" == "esp32s3" ]; then
OTAFILE=bleota-s3.bin OTAFILE=bleota-s3.bin
elif [ "$MCU" == "esp32c3" ]; then
OTAFILE=bleota-c3.bin
else
OTAFILE=bleota.bin
fi fi
# Set SPIFFS filename with "littlefs-" prefix. # Set SPIFFS filename with "littlefs-" prefix.
SPIFFSFILE=littlefs-${BASENAME} SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
if [[ ! -f "$FILENAME" ]]; then if [[ ! -f "$FILENAME" ]]; then
echo "Error: file ${FILENAME} wasn't found. Terminating." echo "Error: file ${FILENAME} wasn't found. Terminating."

View File

@@ -30,11 +30,11 @@ ECHO --change-mode Attempt to place the device in correct mode. (1200bps
ECHO Some hardware requires this twice. ECHO Some hardware requires this twice.
ECHO. ECHO.
ECHO Example: %SCRIPT_NAME% -p COM17 --change-mode ECHO Example: %SCRIPT_NAME% -p COM17 --change-mode
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4-update.bin -p COM11 ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11
GOTO eof GOTO eof
:version :version
ECHO %SCRIPT_NAME% [Version 2.6.2] ECHO %SCRIPT_NAME% [Version 2.7.0]
ECHO Meshtastic ECHO Meshtastic
GOTO eof GOTO eof
@@ -78,12 +78,12 @@ IF NOT EXIST !FILENAME! (
GOTO eof GOTO eof
) )
IF "!FILENAME:update=!"=="!FILENAME!" ( IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" (
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!" CALL :LOG_MESSAGE DEBUG "We are working with a *.factory.bin* file. !FILENAME!"
CALL :LOG_MESSAGE INFO "Use script device-install.bat to flash !FILENAME!." CALL :LOG_MESSAGE INFO "Use script device-install.bat to flash !FILENAME!."
GOTO eof GOTO eof
) ELSE ( ) ELSE (
CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!" CALL :LOG_MESSAGE DEBUG "We are not working with a *.factory.bin* file. !FILENAME!"
) )
:skip-filename :skip-filename

View File

@@ -29,7 +29,7 @@ Flash image file to device, leave existing system intact."
-h Display this help and exit -h Display this help and exit
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous). -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON") -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
-f FILENAME The *update.bin file to flash. Custom to your device type. -f FILENAME The *.bin file to flash. Custom to your device type.
--change-mode Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset) --change-mode Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
EOF EOF
@@ -78,7 +78,7 @@ fi
shift shift
} }
if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then
echo "Trying to flash update ${FILENAME}" echo "Trying to flash update ${FILENAME}"
$ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}" $ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}"
else else

View File

@@ -75,7 +75,7 @@ TOOLS = {
} }
BACKTRACE_REGEX = re.compile( BACKTRACE_REGEX = re.compile(
r"(?:\s+(0x40[0-2](?:\d|[a-f]|[A-F]){5}):0x(?:\d|[a-f]|[A-F]){8})\b" r"\b(0x4[0-9a-fA-F]{7,8}):0x[0-9a-fA-F]{8}\b"
) )
EXCEPTION_REGEX = re.compile("^Exception \\((?P<exc>[0-9]*)\\):$") EXCEPTION_REGEX = re.compile("^Exception \\((?P<exc>[0-9]*)\\):$")
COUNTER_REGEX = re.compile( COUNTER_REGEX = re.compile(
@@ -89,7 +89,7 @@ POINTER_REGEX = re.compile(
STACK_BEGIN = ">>>stack>>>" STACK_BEGIN = ">>>stack>>>"
STACK_END = "<<<stack<<<" STACK_END = "<<<stack<<<"
STACK_REGEX = re.compile( STACK_REGEX = re.compile(
"^(?P<off>[0-9a-f]+):\W+(?P<c1>[0-9a-f]+) (?P<c2>[0-9a-f]+) (?P<c3>[0-9a-f]+) (?P<c4>[0-9a-f]+)(\W.*)?$" r"^(?P<off>[0-9a-f]+):\W+(?P<c1>[0-9a-f]+) (?P<c2>[0-9a-f]+) (?P<c3>[0-9a-f]+) (?P<c4>[0-9a-f]+)(\W.*)?$"
) )
StackLine = namedtuple("StackLine", ["offset", "content"]) StackLine = namedtuple("StackLine", ["offset", "content"])
@@ -223,7 +223,7 @@ class AddressResolver(object):
if match is None: if match is None:
if last is not None and line.startswith("(inlined by)"): if last is not None and line.startswith("(inlined by)"):
line = line[12:].strip() line = line[12:].strip()
self._address_map[last] += "\n \-> inlined by: " + line self._address_map[last] += "\n \\-> inlined by: " + line
continue continue
if match.group("result") == "?? ??:0": if match.group("result") == "?? ??:0":

View File

@@ -2,4 +2,4 @@
set -e set -e
pio run --environment native pio run --environment native
gdbserver --once localhost:2345 .pio/build/native/program "$@" gdbserver --once localhost:2345 .pio/build/native/meshtasticd "$@"

View File

@@ -2,4 +2,4 @@
set -e set -e
pio run --environment native pio run --environment native
.pio/build/native/program "$@" .pio/build/native/meshtasticd "$@"

View File

@@ -2,98 +2,77 @@
# trunk-ignore-all(ruff/F821) # trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports # trunk-ignore-all(flake8/F821): For SConstruct imports
import sys import sys
from os.path import join from os.path import join, basename, isfile
import subprocess import subprocess
import json import json
import re import re
import time
from datetime import datetime from datetime import datetime
from readprops import readProps from readprops import readProps
Import("env") Import("env")
platform = env.PioPlatform() platform = env.PioPlatform()
progname = env.get("PROGNAME")
lfsbin = f"{progname.replace('firmware-', 'littlefs-')}.bin"
def manifest_gather(source, target, env):
def esp32_create_combined_bin(source, target, env): out = []
# this sub is borrowed from ESPEasy build toolchain. It's licensed under GPL V3 check_paths = [
# https://github.com/letscontrolit/ESPEasy/blob/mega/tools/pio/post_esp32.py progname,
print("Generating combined binary for serial flashing") f"{progname}.elf",
f"{progname}.bin",
app_offset = 0x10000 f"{progname}.factory.bin",
f"{progname}.hex",
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") f"{progname}.merged.hex",
sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) f"{progname}.uf2",
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") f"{progname}.factory.uf2",
chip = env.get("BOARD_MCU") f"{progname}.zip",
flash_size = env.BoardConfig().get("upload.flash_size") lfsbin
flash_freq = env.BoardConfig().get("build.f_flash", "40m")
flash_freq = flash_freq.replace("000000L", "m")
flash_mode = env.BoardConfig().get("build.flash_mode", "dio")
memory_type = env.BoardConfig().get("build.arduino.memory_type", "qio_qspi")
if flash_mode == "qio" or flash_mode == "qout":
flash_mode = "dio"
if memory_type == "opi_opi" or memory_type == "opi_qspi":
flash_mode = "dout"
cmd = [
"--chip",
chip,
"merge_bin",
"-o",
new_file_name,
"--flash_mode",
flash_mode,
"--flash_freq",
flash_freq,
"--flash_size",
flash_size,
] ]
for p in check_paths:
f = env.File(env.subst(f"$BUILD_DIR/{p}"))
if f.exists():
d = {
"name": p,
"md5": f.get_content_hash(), # Returns MD5 hash
"bytes": f.get_size() # Returns file size in bytes
}
out.append(d)
print(d)
manifest_write(out, env)
print(" Offset | File") def manifest_write(files, env):
for section in sections: manifest = {
sect_adr, sect_file = section.split(" ", 1) "version": verObj["long"],
print(f" - {sect_adr} | {sect_file}") "build_epoch": build_epoch,
cmd += [sect_adr, sect_file] "board": env.get("PIOENV"),
"mcu": env.get("BOARD_MCU"),
"repo": repo_owner,
"files": files,
"part": None,
"has_mui": False,
"has_inkhud": False,
}
# Get partition table (generated in esp32_pre.py) if it exists
if env.get("custom_mtjson_part"):
# custom_mtjson_part is a JSON string, convert it back to a dict
pj = json.loads(env.get("custom_mtjson_part"))
manifest["part"] = pj
# Enable has_mui for TFT builds
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
manifest["has_mui"] = True
if "MESHTASTIC_INCLUDE_INKHUD" in env.get("CPPDEFINES", []):
manifest["has_inkhud"] = True
print(f" - {hex(app_offset)} | {firmware_name}") # Write the manifest to the build directory
cmd += [hex(app_offset), firmware_name] with open(env.subst("$BUILD_DIR/${PROGNAME}.mt.json"), "w") as f:
json.dump(manifest, f, indent=2)
print("Using esptool.py arguments: %s" % " ".join(cmd))
esptool.main(cmd)
if platform.name == "espressif32":
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
import esptool
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
esp32_kind = env.GetProjectOption("custom_esp32_kind")
if esp32_kind == "esp32":
# Free up some IRAM by removing auxiliary SPI flash chip drivers.
# Wrapped stub symbols are defined in src/platform/esp32/iram-quirk.c.
env.Append(
LINKFLAGS=[
"-Wl,--wrap=esp_flash_chip_gd",
"-Wl,--wrap=esp_flash_chip_issi",
"-Wl,--wrap=esp_flash_chip_winbond",
]
)
else:
# For newer ESP32 targets, using newlib nano works better.
env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])
if platform.name == "nordicnrf52":
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex",
env.VerboseAction(f"\"{sys.executable}\" ./bin/uf2conv.py \"$BUILD_DIR/firmware.hex\" -c -f 0xADA52840 -o \"$BUILD_DIR/firmware.uf2\"",
"Generating UF2 file"))
Import("projenv") Import("projenv")
prefsLoc = projenv["PROJECT_DIR"] + "/version.properties" prefsLoc = projenv["PROJECT_DIR"] + "/version.properties"
verObj = readProps(prefsLoc) verObj = readProps(prefsLoc)
print("Using meshtastic platformio-custom.py, firmware version " + verObj["long"] + " on " + env.get("PIOENV")) print(f"Using meshtastic platformio-custom.py, firmware version {verObj['long']} on {env.get('PIOENV')}")
# get repository owner if git is installed # get repository owner if git is installed
try: try:
@@ -139,10 +118,10 @@ flags = [
"-DBUILD_EPOCH=" + str(build_epoch), "-DBUILD_EPOCH=" + str(build_epoch),
] + pref_flags ] + pref_flags
print ("Using flags:") print("Using flags:")
for flag in flags: for flag in flags:
print(flag) print(flag)
projenv.Append( projenv.Append(
CCFLAGS=flags, CCFLAGS=flags,
) )
@@ -180,4 +159,22 @@ def load_boot_logo(source, target, env):
# Load the boot logo on TFT builds # Load the boot logo on TFT builds
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []): if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
env.AddPreAction('$BUILD_DIR/littlefs.bin', load_boot_logo) env.AddPreAction(f"$BUILD_DIR/{lfsbin}", load_boot_logo)
mtjson_deps = ["buildprog"]
if platform.name == "espressif32":
# Build littlefs image as part of mtjson target
# Equivalent to `pio run -t buildfs`
target_lfs = env.DataToBin(
join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR"
)
mtjson_deps.append(target_lfs)
env.AddCustomTarget(
name="mtjson",
dependencies=mtjson_deps,
actions=[manifest_gather],
title="Meshtastic Manifest",
description="Generating Meshtastic manifest JSON + Checksums",
always_build=False,
)

19
bin/platformio-pre.py Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
Import("env")
platform = env.PioPlatform()
if platform.name == "native":
env.Replace(PROGNAME="meshtasticd")
else:
from readprops import readProps
prefsLoc = env["PROJECT_DIR"] + "/version.properties"
verObj = readProps(prefsLoc)
env.Replace(PROGNAME=f"firmware-{env.get('PIOENV')}-{verObj['long']}")
env.Replace(ESP32_FS_IMAGE_NAME=f"littlefs-{env.get('PIOENV')}-{verObj['long']}")
# Print the new program name for verification
print(f"PROGNAME: {env.get('PROGNAME')}")
if platform.name == "espressif32":
print(f"ESP32_FS_IMAGE_NAME: {env.get('ESP32_FS_IMAGE_NAME')}")

View File

@@ -3,7 +3,7 @@
set -e set -e
echo "Starting simulator" echo "Starting simulator"
.pio/build/native/program & .pio/build/native/meshtasticd -s &
sleep 20 # 5 seconds was not enough sleep 20 # 5 seconds was not enough
echo "Simulator started, launching python test..." echo "Simulator started, launching python test..."

1
debian/rules vendored
View File

@@ -28,5 +28,4 @@ override_dh_auto_build:
# Build with platformio # Build with platformio
$(PIO_ENV) platformio run -e native-tft $(PIO_ENV) platformio run -e native-tft
# Move the binary and default config to the correct name # Move the binary and default config to the correct name
mv .pio/build/native-tft/program .pio/build/native-tft/meshtasticd
cp bin/config-dist.yaml bin/config.yaml cp bin/config-dist.yaml bin/config.yaml

View File

@@ -1,10 +1,9 @@
#!/usr/bin/env python3
# trunk-ignore-all(flake8/F821) # trunk-ignore-all(flake8/F821)
# trunk-ignore-all(ruff/F821) # trunk-ignore-all(ruff/F821)
Import("env") Import("env")
# NOTE: This is not currently used, but can serve as an example on how to write extra_scripts
# print("Current CLI targets", COMMAND_LINE_TARGETS) # print("Current CLI targets", COMMAND_LINE_TARGETS)
# print("Current Build targets", BUILD_TARGETS) # print("Current Build targets", BUILD_TARGETS)
# print("CPP defs", env.get("CPPDEFINES")) # print("CPP defs", env.get("CPPDEFINES"))

86
extra_scripts/esp32_extra.py Executable file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
# trunk-ignore-all(ruff/E402): Hacky esptool import
# trunk-ignore-all(flake8/E402): Hacky esptool import
import sys
from os.path import join
Import("env")
platform = env.PioPlatform()
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
# IntelHex workaround, remove after fixed upstream
# https://github.com/platformio/platform-espressif32/issues/1632
try:
import intelhex
except ImportError:
env.Execute("$PYTHONEXE -m pip install intelhex")
import esptool
def esp32_create_combined_bin(source, target, env):
# this sub is borrowed from ESPEasy build toolchain. It's licensed under GPL V3
# https://github.com/letscontrolit/ESPEasy/blob/mega/tools/pio/post_esp32.py
print("Generating combined binary for serial flashing")
app_offset = 0x10000
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
chip = env.get("BOARD_MCU")
board = env.BoardConfig()
flash_size = board.get("upload.flash_size")
flash_freq = board.get("build.f_flash", "40m")
flash_freq = flash_freq.replace("000000L", "m")
flash_mode = board.get("build.flash_mode", "dio")
memory_type = board.get("build.arduino.memory_type", "qio_qspi")
if flash_mode == "qio" or flash_mode == "qout":
flash_mode = "dio"
if memory_type == "opi_opi" or memory_type == "opi_qspi":
flash_mode = "dout"
cmd = [
"--chip",
chip,
"merge_bin",
"-o",
new_file_name,
"--flash_mode",
flash_mode,
"--flash_freq",
flash_freq,
"--flash_size",
flash_size,
]
print(" Offset | File")
for section in sections:
sect_adr, sect_file = section.split(" ", 1)
print(f" - {sect_adr} | {sect_file}")
cmd += [sect_adr, sect_file]
print(f" - {hex(app_offset)} | {firmware_name}")
cmd += [hex(app_offset), firmware_name]
print("Using esptool.py arguments: %s" % " ".join(cmd))
esptool.main(cmd)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
esp32_kind = env.GetProjectOption("custom_esp32_kind")
if esp32_kind == "esp32":
# Free up some IRAM by removing auxiliary SPI flash chip drivers.
# Wrapped stub symbols are defined in src/platform/esp32/iram-quirk.c.
env.Append(
LINKFLAGS=[
"-Wl,--wrap=esp_flash_chip_gd",
"-Wl,--wrap=esp_flash_chip_issi",
"-Wl,--wrap=esp_flash_chip_winbond",
]
)
else:
# For newer ESP32 targets, using newlib nano works better.
env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])

73
extra_scripts/esp32_pre.py Executable file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
import json
import sys
from os.path import isfile
Import("env")
# From https://github.com/platformio/platform-espressif32/blob/develop/builder/main.py
def _parse_size(value):
if isinstance(value, int):
return value
elif value.isdigit():
return int(value)
elif value.startswith("0x"):
return int(value, 16)
elif value[-1].upper() in ("K", "M"):
base = 1024 if value[-1].upper() == "K" else 1024 * 1024
return int(value[:-1]) * base
return value
def _parse_partitions(env):
partitions_csv = env.subst("$PARTITIONS_TABLE_CSV")
if not isfile(partitions_csv):
sys.stderr.write(
"Could not find the file %s with partitions " "table.\n" % partitions_csv
)
env.Exit(1)
return
result = []
# The first offset is 0x9000 because partition table is flashed to 0x8000 and
# occupies an entire flash sector, which size is 0x1000
next_offset = 0x9000
with open(partitions_csv) as fp:
for line in fp.readlines():
line = line.strip()
if not line or line.startswith("#"):
continue
tokens = [t.strip() for t in line.split(",")]
if len(tokens) < 5:
continue
bound = 0x10000 if tokens[1] in ("0", "app") else 4
calculated_offset = (next_offset + bound - 1) & ~(bound - 1)
partition = {
"name": tokens[0],
"type": tokens[1],
"subtype": tokens[2],
"offset": tokens[3] or calculated_offset,
"size": tokens[4],
"flags": tokens[5] if len(tokens) > 5 else None,
}
result.append(partition)
next_offset = _parse_size(partition["offset"]) + _parse_size(
partition["size"]
)
return result
def mtjson_esp32_part(target, source, env):
part = _parse_partitions(env)
pj = json.dumps(part)
# print(f"JSON_PARTITIONS: {pj}")
# Dump json string to 'custom_mtjson_part' variable to use later when writing the manifest
env.Replace(custom_mtjson_part=pj)
env.AddPreAction("mtjson", mtjson_esp32_part)

50
extra_scripts/nrf52_extra.py Executable file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
import sys
from os.path import basename
Import("env")
# Custom HEX from ELF
# Convert hex to uf2 for nrf52
def nrf52_hex_to_uf2(source, target, env):
hex_path = target[0].get_abspath()
# When using merged hex, drop 'merged' from uf2 filename
uf2_path = hex_path.replace(".merged.", ".")
uf2_path = uf2_path.replace(".hex", ".uf2")
env.Execute(
env.VerboseAction(
f'"{sys.executable}" ./bin/uf2conv.py "{hex_path}" -c -f 0xADA52840 -o "{uf2_path}"',
f"Generating UF2 file from {basename(hex_path)}",
)
)
def nrf52_mergehex(source, target, env):
hex_path = target[0].get_abspath()
merged_hex_path = hex_path.replace(".hex", ".merged.hex")
merge_with = None
if "wio-sdk-wm1110" == str(env.get("PIOENV")):
merge_with = env.subst("$PROJECT_DIR/bin/s140_nrf52_7.3.0_softdevice.hex")
else:
print("merge_with not defined for this target")
if merge_with is not None:
env.Execute(
env.VerboseAction(
f'"$PROJECT_DIR/bin/mergehex" -m "{hex_path}" "{merge_with}" -o "{merged_hex_path}"',
"Merging HEX with SoftDevice",
)
)
print(f'Merged file saved at "{basename(merged_hex_path)}"')
nrf52_hex_to_uf2([hex_path, merge_with], [env.File(merged_hex_path)], env)
# if WM1110 target, merge hex with softdevice 7.3.0
if "wio-sdk-wm1110" == env.get("PIOENV"):
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", nrf52_mergehex)
else:
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", nrf52_hex_to_uf2)

View File

@@ -1,7 +1,9 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821) # trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports # trunk-ignore-all(flake8/F821): For SConstruct imports
Import("env") Import("env")
# Custom HEX from ELF # Custom HEX from ELF
env.AddPostAction( env.AddPostAction(
"$BUILD_DIR/${PROGNAME}.elf", "$BUILD_DIR/${PROGNAME}.elf",

View File

@@ -76,7 +76,7 @@ platformio run -e native-tft
%install %install
# Install meshtasticd binary # Install meshtasticd binary
mkdir -p %{buildroot}%{_bindir} mkdir -p %{buildroot}%{_bindir}
install -m 0755 .pio/build/native-tft/program %{buildroot}%{_bindir}/meshtasticd install -m 0755 .pio/build/native-tft/meshtasticd %{buildroot}%{_bindir}/meshtasticd
# Install portduino VFS dir # Install portduino VFS dir
install -p -d -m 0770 %{buildroot}%{_localstatedir}/lib/meshtasticd install -p -d -m 0770 %{buildroot}%{_localstatedir}/lib/meshtasticd

View File

@@ -14,7 +14,9 @@ description = Meshtastic
[env] [env]
test_build_src = true test_build_src = true
extra_scripts = bin/platformio-custom.py extra_scripts =
pre:bin/platformio-pre.py
bin/platformio-custom.py
; note: we add src to our include search path so that lmic_project_config can override ; note: we add src to our include search path so that lmic_project_config can override
; note: TINYGPS_OPTION_NO_CUSTOM_FIELDS is VERY important. We don't use custom fields and somewhere in that pile ; note: TINYGPS_OPTION_NO_CUSTOM_FIELDS is VERY important. We don't use custom fields and somewhere in that pile
; of code is a heap corruption bug! ; of code is a heap corruption bug!
@@ -101,17 +103,13 @@ lib_deps =
thingsboard/TBPubSubClient@2.12.1 thingsboard/TBPubSubClient@2.12.1
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
arduino-libraries/NTPClient@3.2.1 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 # renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
arcao/Syslog@2.0.0 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] [radiolib_base]
lib_deps = lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
@@ -121,7 +119,7 @@ lib_deps =
[device-ui_base] [device-ui_base]
lib_deps = lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/3bf332240416c5cb8c919fac2a0ec7260eb3be75.zip https://github.com/meshtastic/device-ui/archive/862ed040c4ab44f0dfbbe492691f144886102588.zip
; Common libs for environmental measurements in telemetry module ; Common libs for environmental measurements in telemetry module
[environmental_base] [environmental_base]
@@ -160,8 +158,8 @@ lib_deps =
emotibit/EmotiBit MLX90632@1.0.8 emotibit/EmotiBit MLX90632@1.0.8
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library # renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
adafruit/Adafruit MLX90614 Library@2.1.5 adafruit/Adafruit MLX90614 Library@2.1.5
# renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221 # renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow
https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
mprograms/QMC5883LCompass@1.2.3 mprograms/QMC5883LCompass@1.2.3
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU
@@ -182,8 +180,8 @@ lib_deps =
dfrobot/DFRobot_BMM150@1.0.0 dfrobot/DFRobot_BMM150@1.0.0
# renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561 # renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561
adafruit/Adafruit TSL2561@1.1.2 adafruit/Adafruit TSL2561@1.1.2
# renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/BH1750_WE@^1.1.10 # renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE
wollewald/BH1750_WE@^1.1.10 wollewald/BH1750_WE@1.1.10
; (not included in native / portduino) ; (not included in native / portduino)
[environmental_extra] [environmental_extra]
@@ -205,7 +203,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library # renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6 sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001 # renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
ClosedCube OPT3001@1.1.2 closedcube/ClosedCube OPT3001@1.1.2
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2 # renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
boschsensortec/bsec2@1.10.2610 boschsensortec/bsec2@1.10.2610
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library # renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library

View File

@@ -31,6 +31,9 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
return useShortName ? "LongF" : "LongFast"; return useShortName ? "LongF" : "LongFast";
break; break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
return useShortName ? "LongT" : "LongTurbo";
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
return useShortName ? "LongM" : "LongMod"; return useShortName ? "LongM" : "LongMod";
break; break;

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/ * For more information, see: https://meshtastic.org/
*/ */
#include "power.h" #include "power.h"
#include "MessageStore.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "PowerFSM.h" #include "PowerFSM.h"
#include "Throttle.h" #include "Throttle.h"
@@ -786,7 +787,9 @@ void Power::shutdown()
playShutdownMelody(); playShutdownMelody();
#endif #endif
nodeDB->saveToDisk(); nodeDB->saveToDisk();
#if HAS_SCREEN
messageStore.saveToFlash();
#endif
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) #if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
#ifdef PIN_LED1 #ifdef PIN_LED1
ledOff(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 hour = hms / SEC_PER_HOUR;
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
#ifdef ARCH_PORTDUINO #ifdef ARCH_PORTDUINO
::printf("%s ", logLevel); ::printf("%s ", logLevel);
if (color) { if (color) {

View File

@@ -16,6 +16,7 @@ struct ToneDuration {
}; };
// Some common frequencies. // Some common frequencies.
#define NOTE_SILENT 1
#define NOTE_C3 131 #define NOTE_C3 131
#define NOTE_CS3 139 #define NOTE_CS3 139
#define NOTE_D3 147 #define NOTE_D3 147
@@ -29,11 +30,16 @@ struct ToneDuration {
#define NOTE_AS3 233 #define NOTE_AS3 233
#define NOTE_B3 247 #define NOTE_B3 247
#define NOTE_CS4 277 #define NOTE_CS4 277
#define NOTE_B4 494
#define NOTE_F5 698
#define NOTE_G6 1568
#define NOTE_E7 2637
const int DURATION_1_16 = 62; // 1/16 note
const int DURATION_1_8 = 125; // 1/8 note const int DURATION_1_8 = 125; // 1/8 note
const int DURATION_1_4 = 250; // 1/4 note const int DURATION_1_4 = 250; // 1/4 note
const int DURATION_1_2 = 500; // 1/2 note const int DURATION_1_2 = 500; // 1/2 note
const int DURATION_3_4 = 750; // 1/4 note const int DURATION_3_4 = 750; // 3/4 note
const int DURATION_1_1 = 1000; // 1/1 note const int DURATION_1_1 = 1000; // 1/1 note
void playTones(const ToneDuration *tone_durations, int size) void playTones(const ToneDuration *tone_durations, int size)
@@ -71,13 +77,24 @@ void playLongBeep()
void playGPSEnableBeep() void playGPSEnableBeep()
{ {
#if defined(R1_NEO) || defined(MUZI_BASE)
ToneDuration melody[] = {
{NOTE_F5, DURATION_1_2}, {NOTE_G6, DURATION_1_8}, {NOTE_E7, DURATION_1_4}, {NOTE_SILENT, DURATION_1_2}};
#else
ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}}; ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}};
#endif
playTones(melody, sizeof(melody) / sizeof(ToneDuration)); playTones(melody, sizeof(melody) / sizeof(ToneDuration));
} }
void playGPSDisableBeep() void playGPSDisableBeep()
{ {
#if defined(R1_NEO) || defined(MUZI_BASE)
ToneDuration melody[] = {{NOTE_B4, DURATION_1_16}, {NOTE_B4, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8},
{NOTE_F3, DURATION_1_16}, {NOTE_F3, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8},
{NOTE_C3, DURATION_1_1}, {NOTE_SILENT, DURATION_1_1}};
#else
ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, DURATION_1_4}}; ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, DURATION_1_4}};
#endif
playTones(melody, sizeof(melody) / sizeof(ToneDuration)); playTones(melody, sizeof(melody) / sizeof(ToneDuration));
} }

View File

@@ -46,6 +46,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif #endif
#include "FSCommon.h" #include "FSCommon.h"
#include "MeshService.h" #include "MeshService.h"
#include "MessageStore.h"
#include "RadioLibInterface.h" #include "RadioLibInterface.h"
#include "error.h" #include "error.h"
#include "gps/GeoCoord.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 "modules/WaypointModule.h"
#include "sleep.h" #include "sleep.h"
#include "target_specific.h" #include "target_specific.h"
extern MessageStore messageStore;
using graphics::Emote;
using graphics::emotes;
using graphics::numEmotes;
#if USE_TFTDISPLAY #if USE_TFTDISPLAY
extern uint16_t TFT_MESH; 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. // we'll need to hold onto pointers for the modules that can draw a frame.
std::vector<MeshModule *> moduleFrames; std::vector<MeshModule *> moduleFrames;
// Global variables for screen function overlay symbols
std::vector<std::string> functionSymbol;
std::string functionSymbolString;
#if HAS_GPS #if HAS_GPS
// GeoCoord object for the screen // GeoCoord object for the screen
GeoCoord geoCoord; GeoCoord geoCoord;
@@ -230,24 +224,9 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
{ {
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
if (NotificationRenderer::virtualKeyboard) { // Start OnScreenKeyboardModule session (non-touch variant)
delete NotificationRenderer::virtualKeyboard; OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback);
NotificationRenderer::virtualKeyboard = nullptr;
}
NotificationRenderer::textInputCallback = nullptr;
NotificationRenderer::virtualKeyboard = new VirtualKeyboard();
if (header) {
NotificationRenderer::virtualKeyboard->setHeader(header);
}
if (initialText) {
NotificationRenderer::virtualKeyboard->setInputText(initialText);
}
// Set up callback with safer cleanup mechanism
NotificationRenderer::textInputCallback = textCallback; NotificationRenderer::textInputCallback = textCallback;
NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); });
// Store the message and set the expiration timestamp (use same pattern as other notifications) // Store the message and set the expiration timestamp (use same pattern as other notifications)
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
@@ -278,19 +257,11 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
} else { } else {
// otherwise, just display the module frame that's aligned with the current frame // otherwise, just display the module frame that's aligned with the current frame
module_frame = state->currentFrame; 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); MeshModule &pi = *moduleFrames.at(module_frame);
pi.drawFrame(display, state, x, y); 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. * Given a recent lat/lon return a guess of the heading the user is walking on.
* *
@@ -337,16 +308,16 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
{ {
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; 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; 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) { // Only validate the combined value once
TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); 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));
} }
} }
@@ -565,10 +536,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
void Screen::setup() void Screen::setup()
{ {
// === Enable display rendering === // Enable display rendering
useDisplay = true; 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 // For OLED displays (SSD1306), default brightness is 255 if not set
if (uiconfig.screen_brightness == 0) { if (uiconfig.screen_brightness == 0) {
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) #if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
@@ -580,7 +551,7 @@ void Screen::setup()
brightness = uiconfig.screen_brightness; brightness = uiconfig.screen_brightness;
} }
// === Detect OLED subtype (if supported by board variant) === // Detect OLED subtype (if supported by board variant)
#ifdef AutoOLEDWire_h #ifdef AutoOLEDWire_h
if (isAUTOOled) if (isAUTOOled)
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model); static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
@@ -602,7 +573,7 @@ void Screen::setup()
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH); static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
#endif #endif
// === Initialize display and UI system === // Initialize display and UI system
ui->init(); ui->init();
displayWidth = dispdev->width(); displayWidth = dispdev->width();
displayHeight = dispdev->height(); displayHeight = dispdev->height();
@@ -614,7 +585,7 @@ void Screen::setup()
ui->disableAllIndicators(); // Disable page indicator dots ui->disableAllIndicators(); // Disable page indicator dots
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
// === Apply loaded brightness === // Apply loaded brightness
#if defined(ST7789_CS) #if defined(ST7789_CS)
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness); static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306) #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
@@ -622,20 +593,20 @@ void Screen::setup()
#endif #endif
LOG_INFO("Applied screen brightness: %d", brightness); LOG_INFO("Applied screen brightness: %d", brightness);
// === Set custom overlay callbacks === // Set custom overlay callbacks
static OverlayCallback overlays[] = { static OverlayCallback overlays[] = {
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
}; };
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
// === Enable UTF-8 to display mapping === // Enable UTF-8 to display mapping
dispdev->setFontTableLookupFunction(customFontTableLookup); dispdev->setFontTableLookupFunction(customFontTableLookup);
#ifdef USERPREFS_OEM_TEXT #ifdef USERPREFS_OEM_TEXT
logo_timeout *= 2; // Give more time for branded boot logos logo_timeout *= 2; // Give more time for branded boot logos
#endif #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 EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
@@ -651,10 +622,10 @@ void Screen::setup()
ui->setFrames(alertFrames, 1); ui->setFrames(alertFrames, 1);
ui->disableAutoTransition(); // Require manual navigation between frames 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); 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 #ifdef SCREEN_MIRROR
dispdev->mirrorScreen(); dispdev->mirrorScreen();
#else #else
@@ -672,7 +643,7 @@ void Screen::setup()
} }
#endif #endif
// === Generate device ID from MAC address === // Generate device ID from MAC address
uint8_t dmac[6]; uint8_t dmac[6];
getMacAddr(dmac); getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
@@ -681,9 +652,9 @@ void Screen::setup()
handleSetOn(false); // Ensure proper init for Arduino targets handleSetOn(false); // Ensure proper init for Arduino targets
#endif #endif
// === Turn on display and trigger first draw === // Turn on display and trigger first draw
handleSetOn(true); handleSetOn(true);
determineResolution(dispdev->height(), dispdev->width()); graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width());
ui->update(); ui->update();
#ifndef USE_EINK #ifndef USE_EINK
ui->update(); // Some SSD1306 clones drop the first draw, so run twice ui->update(); // Some SSD1306 clones drop the first draw, so run twice
@@ -704,7 +675,7 @@ void Screen::setup()
touchScreenImpl1->init(); touchScreenImpl1->init();
#endif #endif
// === Subscribe to device status updates === // Subscribe to device status updates
powerStatusObserver.observe(&powerStatus->onNewStatus); powerStatusObserver.observe(&powerStatus->onNewStatus);
gpsStatusObserver.observe(&gpsStatus->onNewStatus); gpsStatusObserver.observe(&gpsStatus->onNewStatus);
nodeStatusObserver.observe(&nodeStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus);
@@ -712,12 +683,14 @@ void Screen::setup()
#if !MESHTASTIC_EXCLUDE_ADMIN #if !MESHTASTIC_EXCLUDE_ADMIN
adminMessageObserver.observe(adminModule); adminMessageObserver.observe(adminModule);
#endif #endif
if (textMessageModule)
textMessageObserver.observe(textMessageModule);
if (inputBroker) if (inputBroker)
inputObserver.observe(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); MeshModule::observeUIEvents(&uiFrameEventObserver);
} }
@@ -788,6 +761,23 @@ int32_t Screen::runOnce()
if (displayHeight == 0) { if (displayHeight == 0) {
displayHeight = dispdev->getHeight(); 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); menuHandler::handleMenuSwitch(dispdev);
// Show boot screen for first logo_timeout seconds, then switch to normal operation. // Show boot screen for first logo_timeout seconds, then switch to normal operation.
@@ -843,17 +833,17 @@ int32_t Screen::runOnce()
break; break;
case Cmd::ON_PRESS: case Cmd::ON_PRESS:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleOnPress(); showFrame(FrameDirection::NEXT);
} }
break; break;
case Cmd::SHOW_PREV_FRAME: case Cmd::SHOW_PREV_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleShowPrevFrame(); showFrame(FrameDirection::PREVIOUS);
} }
break; break;
case Cmd::SHOW_NEXT_FRAME: case Cmd::SHOW_NEXT_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleShowNextFrame(); showFrame(FrameDirection::NEXT);
} }
break; break;
case Cmd::START_ALERT_FRAME: { case Cmd::START_ALERT_FRAME: {
@@ -874,6 +864,7 @@ int32_t Screen::runOnce()
break; break;
case Cmd::STOP_ALERT_FRAME: case Cmd::STOP_ALERT_FRAME:
NotificationRenderer::pauseBanner = false; NotificationRenderer::pauseBanner = false;
break;
case Cmd::STOP_BOOT_SCREEN: case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
@@ -1044,9 +1035,6 @@ void Screen::setFrames(FrameFocus focus)
} }
#endif #endif
// Declare this early so its available in FOCUS_PRESERVE block
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
if (!hiddenFrames.home) { if (!hiddenFrames.home) {
fsi.positions.home = numframes; fsi.positions.home = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
@@ -1058,11 +1046,16 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.push_back(icon_mail); indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK #ifndef USE_EINK
if (!hiddenFrames.nodelist) { if (!hiddenFrames.nodelist_nodes) {
fsi.positions.nodelist = numframes; fsi.positions.nodelist_nodes = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes;
indicatorIcons.push_back(icon_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 #endif
// Show detailed node views only on E-Ink builds // Show detailed node views only on E-Ink builds
@@ -1084,11 +1077,13 @@ void Screen::setFrames(FrameFocus focus)
} }
#endif #endif
#if HAS_GPS #if HAS_GPS
#ifdef USE_EINK
if (!hiddenFrames.nodelist_bearings) { if (!hiddenFrames.nodelist_bearings) {
fsi.positions.nodelist_bearings = numframes; fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list); indicatorIcons.push_back(icon_list);
} }
#endif
if (!hiddenFrames.gps) { if (!hiddenFrames.gps) {
fsi.positions.gps = numframes; fsi.positions.gps = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
@@ -1188,7 +1183,7 @@ void Screen::setFrames(FrameFocus focus)
} }
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE 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); LOG_DEBUG("Finished build frames. numframes: %d", numframes);
ui->setFrames(normalFrames, numframes); ui->setFrames(normalFrames, numframes);
@@ -1208,10 +1203,6 @@ void Screen::setFrames(FrameFocus focus)
case FOCUS_FAULT: case FOCUS_FAULT:
ui->switchToFrame(fsi.positions.fault); ui->switchToFrame(fsi.positions.fault);
break; break;
case FOCUS_TEXTMESSAGE:
hasUnreadMessage = false; // ✅ Clear when message is *viewed*
ui->switchToFrame(fsi.positions.textMessage);
break;
case FOCUS_MODULE: case FOCUS_MODULE:
// Whichever frame was marked by MeshModule::requestFocus(), if any // Whichever frame was marked by MeshModule::requestFocus(), if any
// If no module requested focus, will show the first frame instead // If no module requested focus, will show the first frame instead
@@ -1254,8 +1245,11 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
void Screen::toggleFrameVisibility(const std::string &frameName) void Screen::toggleFrameVisibility(const std::string &frameName)
{ {
#ifndef USE_EINK #ifndef USE_EINK
if (frameName == "nodelist") { if (frameName == "nodelist_nodes") {
hiddenFrames.nodelist = !hiddenFrames.nodelist; hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes;
}
if (frameName == "nodelist_location") {
hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location;
} }
#endif #endif
#ifdef USE_EINK #ifdef USE_EINK
@@ -1270,9 +1264,11 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
} }
#endif #endif
#if HAS_GPS #if HAS_GPS
#ifdef USE_EINK
if (frameName == "nodelist_bearings") { if (frameName == "nodelist_bearings") {
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
} }
#endif
if (frameName == "gps") { if (frameName == "gps") {
hiddenFrames.gps = !hiddenFrames.gps; hiddenFrames.gps = !hiddenFrames.gps;
} }
@@ -1294,8 +1290,10 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
bool Screen::isFrameHidden(const std::string &frameName) const bool Screen::isFrameHidden(const std::string &frameName) const
{ {
#ifndef USE_EINK #ifndef USE_EINK
if (frameName == "nodelist") if (frameName == "nodelist_nodes")
return hiddenFrames.nodelist; return hiddenFrames.nodelist_nodes;
if (frameName == "nodelist_location")
return hiddenFrames.nodelist_location;
#endif #endif
#ifdef USE_EINK #ifdef USE_EINK
if (frameName == "nodelist_lastheard") if (frameName == "nodelist_lastheard")
@@ -1306,8 +1304,10 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return hiddenFrames.nodelist_distance; return hiddenFrames.nodelist_distance;
#endif #endif
#if HAS_GPS #if HAS_GPS
#ifdef USE_EINK
if (frameName == "nodelist_bearings") if (frameName == "nodelist_bearings")
return hiddenFrames.nodelist_bearings; return hiddenFrames.nodelist_bearings;
#endif
if (frameName == "gps") if (frameName == "gps")
return hiddenFrames.gps; return hiddenFrames.gps;
#endif #endif
@@ -1323,37 +1323,6 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return false; 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() void Screen::handleStartFirmwareUpdateScreen()
{ {
LOG_DEBUG("Show firmware screen"); LOG_DEBUG("Show firmware screen");
@@ -1406,28 +1375,6 @@ void Screen::decreaseBrightness()
/* TO DO: add little popup in center of screen saying what brightness level it is set to*/ /* 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() void Screen::handleOnPress()
{ {
// If screen was off, just wake it, otherwise advance to next frame // If screen was off, just wake it, otherwise advance to next frame
@@ -1439,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 // Only advance frames when UI is stable
// If we are in a transition, the press must have bounced, drop it.
if (ui->getUiState()->frameState == FIXED) { if (ui->getUiState()->frameState == FIXED) {
ui->previousFrame();
lastScreenTransition = millis();
setFastFramerate();
}
}
void Screen::handleShowNextFrame() if (direction == FrameDirection::NEXT) {
{ ui->nextFrame();
// If screen was off, just wake it, otherwise advance to next frame } else {
// If we are in a transition, the press must have bounced, drop it. ui->previousFrame();
if (ui->getUiState()->frameState == FIXED) { }
ui->nextFrame();
lastScreenTransition = millis(); lastScreenTransition = millis();
setFastFramerate(); setFastFramerate();
} }
@@ -1481,7 +1422,6 @@ void Screen::setFastFramerate()
int Screen::handleStatusUpdate(const meshtastic::Status *arg) int Screen::handleStatusUpdate(const meshtastic::Status *arg)
{ {
// LOG_DEBUG("Screen got status update %d", arg->getStatusType());
switch (arg->getStatusType()) { switch (arg->getStatusType()) {
case STATUS_TYPE_NODE: case STATUS_TYPE_NODE:
if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) {
@@ -1513,14 +1453,14 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Incoming message // Incoming message
devicestate.has_rx_text_message = true; // Needed to include the message frame devicestate.has_rx_text_message = true; // Needed to include the message frame
hasUnreadMessage = true; // Enables mail icon in the header hasUnreadMessage = true; // Enables mail icon in the header
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input)
// Only wake/force display if the configuration allows it // Only wake/force display if the configuration allows it
if (shouldWakeOnReceivedMessage()) { if (shouldWakeOnReceivedMessage()) {
setOn(true); // Wake up the screen first setOn(true); // Wake up the screen first
forceDisplay(); // Forces screen redraw forceDisplay(); // Forces screen redraw
} }
// === Prepare banner content === // === Prepare banner/popup content ===
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
const meshtastic_Channel channel = const meshtastic_Channel channel =
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex()); channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
@@ -1544,38 +1484,84 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Unlike generic messages, alerts (when enabled via the ext notif module) ignore any // Unlike generic messages, alerts (when enabled via the ext notif module) ignore any
// 'mute' preferences set to any specific node or channel. // 'mute' preferences set to any specific node or channel.
if (isAlert) { // If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it
if (longName && longName[0]) { if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); // Wake and force redraw so popup is visible immediately
} else { if (shouldWakeOnReceivedMessage()) {
strcpy(banner, "Alert Received"); setOn(true);
forceDisplay();
} }
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
// Build popup: title = message source name, content = message text (sanitized)
// Title
char titleBuf[64] = {0};
if (longName && longName[0]) {
// Sanitize sender name
std::string t = sanitizeString(longName);
strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1);
} else { } else {
strcpy(banner, "New Message"); strncpy(titleBuf, "Message", sizeof(titleBuf) - 1);
} }
// Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize
char content[256] = {0};
{
std::string raw;
raw.reserve(packet->decoded.payload.size);
for (size_t i = 0; i < packet->decoded.payload.size; ++i) {
char c = msgRaw[i];
if (c == ASCII_BELL)
continue; // strip bell
raw.push_back(c);
}
std::string sanitized = sanitizeString(raw);
strncpy(content, sanitized.c_str(), sizeof(content) - 1);
}
NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000);
// Maintain existing buzzer behavior on M5 if applicable
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
screen->setOn(true);
screen->showSimpleBanner(banner, 1500);
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) || (isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
(!isBroadcast(packet->to) && isToUs(packet))) { (!isBroadcast(packet->to) && isToUs(packet))) {
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
// - packet contains an alert and alert bell buzzer is enabled
// - packet is a non-broadcast that is addressed to this node
playLongBeep(); playLongBeep();
} }
#else
screen->showSimpleBanner(banner, 3000);
#endif #endif
} else {
// No keyboard active: use regular banner flow, respecting mute settings
if (isAlert) {
if (longName && longName[0]) {
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
} else {
strcpy(banner, "Alert Received");
}
screen->showSimpleBanner(banner, 3000);
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
if (longName && longName[0]) {
if (currentResolution == ScreenResolution::UltraLow) {
strcpy(banner, "New Message");
} else {
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
}
} else {
strcpy(banner, "New Message");
}
#if defined(M5STACK_UNITC6L)
screen->setOn(true);
screen->showSimpleBanner(banner, 1500);
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
(!isBroadcast(packet->to) && isToUs(packet))) {
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
// - packet contains an alert and alert bell buzzer is enabled
// - packet is a non-broadcast that is addressed to this node
playLongBeep();
}
#else
screen->showSimpleBanner(banner, 3000);
#endif
}
} }
} }
} }
@@ -1593,16 +1579,26 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event)
if (showingNormalScreen) { if (showingNormalScreen) {
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call // 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); setFrames(FOCUS_MODULE);
}
// Regenerate the frameset, while Attempt to maintain focus on the current frame // Regenerate the frameset, while attempting to maintain focus on the current frame
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) {
setFrames(FOCUS_PRESERVE); setFrames(FOCUS_PRESERVE);
}
// Don't regenerate the frameset, just re-draw whatever is on screen ASAP // 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(); 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; return 0;
@@ -1640,7 +1636,48 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::handleMenuSwitch(dispdev); menuHandler::handleMenuSwitch(dispdev);
return 0; 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, // 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 // so long as a mesh module isn't using these events for some other purpose
if (showingNormalScreen) { if (showingNormalScreen) {
@@ -1654,10 +1691,39 @@ int Screen::handleInputEvent(const InputEvent *event)
// If no modules are using the input, move between frames // If no modules are using the input, move between frames
if (!inputIntercepted) { 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) { 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) { } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
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(); 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) { } else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
menuHandler::homeBaseMenu(); menuHandler::homeBaseMenu();
@@ -1672,20 +1738,21 @@ int Screen::handleInputEvent(const InputEvent *event)
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
menuHandler::loraMenu(); menuHandler::loraMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
if (devicestate.rx_text_message.from) { if (!messageStore.getMessages().empty()) {
menuHandler::messageResponseMenu(); menuHandler::messageResponseMenu();
} else { } else {
#if defined(M5STACK_UNITC6L) if (currentResolution == ScreenResolution::UltraLow) {
menuHandler::textMessageMenu(); menuHandler::textMessageMenu();
#else } else {
menuHandler::textMessageBaseMenu(); menuHandler::textMessageBaseMenu();
#endif }
} }
} else if (framesetInfo.positions.firstFavorite != 255 && } else if (framesetInfo.positions.firstFavorite != 255 &&
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
menuHandler::favoriteBaseMenu(); 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_lastheard ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
@@ -1696,7 +1763,7 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::wifiBaseMenu(); menuHandler::wifiBaseMenu();
} }
} else if (event->inputEvent == INPUT_BROKER_BACK) { } else if (event->inputEvent == INPUT_BROKER_BACK) {
showPrevFrame(); showFrame(FrameDirection::PREVIOUS);
} else if (event->inputEvent == INPUT_BROKER_CANCEL) { } else if (event->inputEvent == INPUT_BROKER_CANCEL) {
setOn(false); setOn(false);
} }

View File

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

View File

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

View File

@@ -42,8 +42,11 @@ namespace graphics
// Shared state (declare inside namespace) // Shared state (declare inside namespace)
extern bool hasUnreadMessage; extern bool hasUnreadMessage;
extern bool isMuted; extern bool isMuted;
extern bool isHighResolution; enum class ScreenResolution : uint8_t { UltraLow = 0, Low = 1, High = 2 };
void determineResolution(int16_t screenheight, int16_t screenwidth); 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) // 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); 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) { if (screenHeight <= 64) {
textY = boxY + (boxHeight - inputLineH) / 2; textY = boxY + (boxHeight - inputLineH) / 2;
} else { } else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1; const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2; const int innerBottom = boxY + boxHeight - 2;

View File

@@ -1,15 +1,10 @@
#include "configuration.h" #include "configuration.h"
#if HAS_SCREEN #if HAS_SCREEN
#include "ClockRenderer.h" #include "ClockRenderer.h"
#include "NodeDB.h"
#include "UIRenderer.h"
#include "configuration.h"
#include "gps/GeoCoord.h"
#include "gps/RTC.h" #include "gps/RTC.h"
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h" #include "graphics/draw/UIRenderer.h"
#include "graphics/emotes.h"
#include "graphics/images.h" #include "graphics/images.h"
#include "main.h" #include "main.h"
@@ -23,6 +18,31 @@ namespace graphics
namespace ClockRenderer 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) void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
{ {
uint16_t segmentWidth = SEGMENT_WIDTH * 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 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; 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) 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 // Read 7-segment pattern for the digit from flash
// segment {innerIndex + 1} uint8_t seg[7];
// e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off. for (uint8_t i = 0; i < 7; i++) {
uint8_t numbers[10][7] = { seg[i] = pgm_read_byte(&digitSegments[number][i]);
{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) |⋰
// ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
uint16_t segmentWidth = SEGMENT_WIDTH * scale; uint16_t segmentWidth = SEGMENT_WIDTH * scale;
uint16_t segmentHeight = SEGMENT_HEIGHT * scale; uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
// segment x and y coordinates // Precompute segment positions
uint16_t segmentOneX = x + segmentHeight + 2; uint16_t segmentOneX = x + segmentHeight + 2;
uint16_t segmentOneY = y; 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 segmentSevenX = segmentOneX;
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
if (numbers[number][0]) { // Draw only the active segments
graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); if (seg[0])
} drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
if (seg[1])
if (numbers[number][1]) { drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); if (seg[2])
} drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
if (seg[3])
if (numbers[number][2]) { drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); if (seg[4])
} drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
if (seg[5])
if (numbers[number][3]) { drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); if (seg[6])
} drawHorizontalSegment(display, segmentSevenX, segmentSevenY, 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);
}
} }
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) 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); 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) void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
display->clear(); display->clear();
@@ -192,7 +146,6 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
const char *titleStr = ""; const char *titleStr = "";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true); graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
char timeString[16]; char timeString[16];
@@ -237,7 +190,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
float target_width = display->getWidth() * screenwidth_target_ratio; float target_width = display->getWidth() * screenwidth_target_ratio;
float target_height = float target_height =
display->getHeight() - display->getHeight() -
(isHighResolution ((currentResolution == ScreenResolution::High)
? 46 ? 46
: 33); // Be careful adjusting this number, we have to account for header and the text under the time : 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; scaleInitialized = true;
} }
size_t len = strlen(timeString);
// calculate hours:minutes string width // 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++) { for (size_t i = 0; i < len; i++) {
char character = timeString[i]; char character = timeString[i];
@@ -310,9 +262,16 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
// draw seconds string + AM/PM // draw seconds string + AM/PM
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
int xOffset = (isHighResolution) ? 0 : -1; int xOffset = -1;
if (currentResolution == ScreenResolution::High) {
xOffset = 0;
}
if (hour >= 10) { if (hour >= 10) {
xOffset += (isHighResolution) ? 32 : 18; if (currentResolution == ScreenResolution::High) {
xOffset += 32;
} else {
xOffset += 18;
}
} }
if (config.display.use_12h_clock) { if (config.display.use_12h_clock) {
@@ -320,7 +279,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
} }
#ifndef USE_EINK #ifndef USE_EINK
xOffset = (isHighResolution) ? 18 : 10; xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10;
if (scale >= 2.0f) { if (scale >= 2.0f) {
xOffset -= (int)(4.5f * scale); xOffset -= (int)(4.5f * scale);
} }
@@ -339,19 +298,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const char *titleStr = ""; const char *titleStr = "";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true); graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
// clock face center coordinates // clock face center coordinates
int16_t centerX = display->getWidth() / 2; int16_t centerX = display->getWidth() / 2;
int16_t centerY = display->getHeight() / 2; int16_t centerY = display->getHeight() / 2;
// clock face radius // clock face radius
int16_t radius = 0; int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9;
if (display->getHeight() < display->getWidth()) {
radius = (display->getHeight() / 2) * 0.9;
} else {
radius = (display->getWidth() / 2) * 0.9;
}
#ifdef T_WATCH_S3 #ifdef T_WATCH_S3
radius = (display->getWidth() / 2) * 0.8; radius = (display->getWidth() / 2) * 0.8;
#endif #endif
@@ -366,17 +319,8 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// tick mark outer y coordinate; (first nested circle) // tick mark outer y coordinate; (first nested circle)
int16_t tickMarkOuterNoonY = secondHandNoonY; int16_t tickMarkOuterNoonY = secondHandNoonY;
// seconds tick mark inner y coordinate; (second nested circle) double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4);
double secondsTickMarkInnerNoonY = (double)noonY + 4; double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6);
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;
}
// minute hand y coordinate // minute hand y coordinate
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
@@ -386,7 +330,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// hour hand radius and y coordinate // hour hand radius and y coordinate
int16_t hourHandRadius = radius * 0.35; int16_t hourHandRadius = radius * 0.35;
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
hourHandRadius = radius * 0.55; hourHandRadius = radius * 0.55;
} }
int16_t hourHandNoonY = centerY - hourHandRadius; 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 uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
if (rtc_sec > 0) { if (rtc_sec > 0) {
long hms = rtc_sec % SEC_PER_DAY; int hour, minute, second;
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; 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) { if (config.display.use_12h_clock) {
isPM = hour >= 12; bool isPM = hour >= 12;
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
int yOffset = isHighResolution ? 1 : 0; int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0;
#ifdef USE_EINK #ifdef USE_EINK
yOffset += 3; yOffset += 3;
#endif #endif
@@ -499,12 +437,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
#else #else
#ifdef USE_EINK #ifdef USE_EINK
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
// draw hour number // draw hour number
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
} }
#else #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 // draw hour number
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); 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 startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
// draw minute tick mark // draw minute tick mark
display->drawLine(startX, startY, endX, endY); 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 // This could draw a "N" indicator or north arrow
// For now, we'll draw a simple north indicator // For now, we'll draw a simple north indicator
// const float radius = 17.0f; // const float radius = 17.0f;
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
radius += 4; radius += 4;
} }
Point north(0, -radius); Point north(0, -radius);
@@ -59,7 +59,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_CENTER); display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setColor(BLACK); 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); display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
} else { } else {
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); 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); std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds);
// Line 1 (Still) // Line 1 (Still)
#if !defined(M5STACK_UNITC6L) if (currentResolution != graphics::ScreenResolution::UltraLow) {
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
if (config.display.heading_bold) if (config.display.heading_bold)
display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
display->setColor(WHITE); display->setColor(WHITE);
#endif }
// Setup string to assemble analogClock string // Setup string to assemble analogClock string
std::string analogClock = ""; std::string analogClock = "";
@@ -301,9 +301,8 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
// Tear apart hms into h:m:s // Tear apart hms into h:m:s
int hour = hms / SEC_PER_HOUR; int hour, min, sec;
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; graphics::decomposeTime(rtc_sec, hour, min, sec);
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
char timebuf[12]; char timebuf[12];
@@ -379,7 +378,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int line = 1; int line = 1;
// === Set Title // === Set Title
const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa"; const char *titleStr = (currentResolution == ScreenResolution::High) ? "LoRa Info" : "LoRa";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);
@@ -391,11 +390,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
char shortnameble[35]; char shortnameble[35];
getMacAddr(dmac); getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
#if defined(M5STACK_UNITC6L) if (currentResolution == ScreenResolution::UltraLow) {
snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId);
#else } else {
snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
#endif }
int textWidth = display->getStringWidth(shortnameble); int textWidth = display->getStringWidth(shortnameble);
int nameX = (SCREEN_WIDTH - textWidth); int nameX = (SCREEN_WIDTH - textWidth);
display->drawString(nameX, getTextPositions(display)[line++], shortnameble); display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
@@ -414,11 +413,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
char regionradiopreset[25]; char regionradiopreset[25];
const char *region = myRegion ? myRegion->name : NULL; const char *region = myRegion ? myRegion->name : NULL;
if (region != nullptr) { if (region != nullptr) {
#if defined(M5STACK_UNITC6L) if (currentResolution == ScreenResolution::UltraLow) {
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
#else } else {
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
#endif }
} }
textWidth = display->getStringWidth(regionradiopreset); textWidth = display->getStringWidth(regionradiopreset);
nameX = (SCREEN_WIDTH - textWidth) / 2; nameX = (SCREEN_WIDTH - textWidth) / 2;
@@ -430,17 +429,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
float freq = RadioLibInterface::instance->getFreq(); float freq = RadioLibInterface::instance->getFreq();
snprintf(freqStr, sizeof(freqStr), "%.3f", freq); snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
if (config.lora.channel_num == 0) { if (config.lora.channel_num == 0) {
#if defined(M5STACK_UNITC6L) if (currentResolution == ScreenResolution::UltraLow) {
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr);
#else } else {
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
#endif }
} else { } else {
#if defined(M5STACK_UNITC6L) if (currentResolution == ScreenResolution::UltraLow) {
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
#else } else {
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
#endif }
} }
size_t len = strlen(frequencyslot); size_t len = strlen(frequencyslot);
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { 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]; char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); 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_y = getTextPositions(display)[line] + 3;
int chutil_bar_width = (isHighResolution) ? 100 : 50; int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50;
int chutil_bar_height = (isHighResolution) ? 12 : 7; int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7;
int extraoffset = (isHighResolution) ? 6 : 3; int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3;
int chutil_percent = airTime->channelUtilizationPercent(); int chutil_percent = airTime->channelUtilizationPercent();
int centerofscreen = SCREEN_WIDTH / 2; int centerofscreen = SCREEN_WIDTH / 2;
@@ -530,15 +530,18 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
int line = 1; int line = 1;
const int barHeight = 6; const int barHeight = 6;
const int labelX = x; const int labelX = x;
int barsOffset = (isHighResolution) ? 24 : 0; int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0;
#ifdef USE_EINK #ifdef USE_EINK
#ifndef T_DECK_PRO
barsOffset -= 12; barsOffset -= 12;
#endif #endif
#if defined(M5STACK_UNITC6L)
const int barX = x + 45 + barsOffset;
#else
const int barX = x + 40 + barsOffset;
#endif #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) { auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
if (total == 0) if (total == 0)
return; return;
@@ -546,7 +549,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
int percent = (used * 100) / total; int percent = (used * 100) / total;
char combinedStr[24]; char combinedStr[24];
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
total / 1024); total / 1024);
} else { } else {
@@ -574,7 +577,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
#endif #endif
// Value string // Value string
display->setTextAlignment(TEXT_ALIGN_RIGHT); display->setTextAlignment(TEXT_ALIGN_RIGHT);
display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr); display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr);
}; };
// === Memory values === // === Memory values ===
@@ -626,25 +629,33 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
line += 1; line += 1;
char appversionstr[35]; char appversionstr[35];
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION));
char appversionstr_formatted[40]; char appversionstr_formatted[40];
char *lastDot = strrchr(appversionstr, '.');
#if defined(M5STACK_UNITC6L) const char *ver = optstr(APP_VERSION);
if (lastDot != nullptr) { char verbuf[32];
*lastDot = '\0'; // truncate string 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 textWidth = display->getStringWidth(appversionstr);
int nameX = (SCREEN_WIDTH - textWidth) / 2; int nameX = (SCREEN_WIDTH - textWidth) / 2;
@@ -663,7 +674,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
const char *clientWord = nullptr; const char *clientWord = nullptr;
// Determine if narrow or wide screen // Determine if narrow or wide screen
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
clientWord = "Client"; clientWord = "Client";
} else { } else {
clientWord = "App"; clientWord = "App";
@@ -704,11 +715,23 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
int textX_offset = 10; int textX_offset = 10;
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
textX_offset = textX_offset * 4; 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 { } else {
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@ class menuHandler
clock_face_picker, clock_face_picker,
clock_menu, clock_menu,
position_base_menu, position_base_menu,
node_base_menu,
gps_toggle_menu, gps_toggle_menu,
gps_format_menu, gps_format_menu,
compass_point_north_menu, compass_point_north_menu,
@@ -43,6 +44,10 @@ class menuHandler
key_verification_final_prompt, key_verification_final_prompt,
trace_route_menu, trace_route_menu,
throttle_message, throttle_message,
message_response_menu,
message_viewmode_menu,
reply_menu,
delete_messages_menu,
node_name_length_menu, node_name_length_menu,
FrameToggles, FrameToggles,
DisplayUnits DisplayUnits
@@ -61,6 +66,9 @@ class menuHandler
static void TwelveHourPicker(); static void TwelveHourPicker();
static void ClockFacePicker(); static void ClockFacePicker();
static void messageResponseMenu(); static void messageResponseMenu();
static void messageViewModeMenu();
static void replyMenu();
static void deleteMessagesMenu();
static void homeBaseMenu(); static void homeBaseMenu();
static void textMessageBaseMenu(); static void textMessageBaseMenu();
static void systemBaseMenu(); static void systemBaseMenu();
@@ -99,5 +107,24 @@ class menuHandler
static void BluetoothToggleMenu(); static void BluetoothToggleMenu();
}; };
/* Generic Menu Options designations */
enum class OptionsAction { Back, Select };
template <typename T> struct MenuOption {
const char *label;
OptionsAction action;
bool hasValue;
T value;
MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn)
: label(labelIn), action(actionIn), hasValue(true), value(valueIn)
{
}
MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
};
using RadioPresetOption = MenuOption<meshtastic_Config_LoRaConfig_ModemPreset>;
} // namespace graphics } // namespace graphics
#endif #endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,11 @@
#pragma once #pragma once
#include "MessageStore.h" // for StoredMessage
#if HAS_SCREEN
#include "OLEDDisplay.h" #include "OLEDDisplay.h"
#include "OLEDDisplayUi.h" #include "OLEDDisplayUi.h"
#include "graphics/emotes.h" #include "graphics/emotes.h"
#include "mesh/generated/meshtastic/mesh.pb.h" // for meshtastic_MeshPacket
#include <cstdint>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -10,6 +14,27 @@ namespace graphics
namespace MessageRenderer 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 // Text and emote rendering
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount); 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); std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
// Function to calculate heights for each line // 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 // Reset scroll state when new messages arrive
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x, void resetScrollState();
int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold);
// 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 MessageRenderer
} // namespace graphics } // namespace graphics
#endif

View File

@@ -23,7 +23,6 @@ extern graphics::Screen *screen;
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
static uint32_t lastSwitchTime = 0; static uint32_t lastSwitchTime = 0;
#else
#endif #endif
namespace graphics 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 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; 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 // 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[25]; // single static buffer we return
static char nodeName[16] = "?"; nodeName[0] = '\0';
if (config.display.use_long_node_name == true) {
if (node->has_user && strlen(node->user.long_name) > 0) { auto writeFallbackId = [&] {
name = node->user.long_name; std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
} else { };
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
} // 1) Choose target candidate (long vs short) only if present
} else { const char *raw = nullptr;
if (node->has_user && strlen(node->user.short_name) > 0) { if (node && node->has_user) {
name = node->user.short_name; raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
}
} }
// Use sanitizeString() function and copy directly into nodeName // 2) Sanitize (empty if raw is null/empty)
std::string sanitized_name = sanitizeString(name ? name : ""); std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{};
if (!sanitized_name.empty()) { // 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed)
strncpy(nodeName, sanitized_name.c_str(), sizeof(nodeName) - 1); if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) {
nodeName[sizeof(nodeName) - 1] = '\0'; writeFallbackId();
} else { } 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) { // 4) Width-based truncation + ellipsis (long-name mode only)
int availWidth = (SCREEN_WIDTH / 2) - 65; if (config.display.use_long_node_name && display) {
int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38);
if (availWidth < 0) if (availWidth < 0)
availWidth = 0; availWidth = 0;
size_t origLen = strlen(nodeName); const size_t beforeLen = std::strlen(nodeName);
while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) {
nodeName[strlen(nodeName) - 1] = '\0'; // 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 truncated, append "..." (respect buffer size)
if (strlen(nodeName) < origLen) { if (len < beforeLen) {
size_t len = strlen(nodeName); // Make sure there's room for "..." and '\0'
size_t maxLen = sizeof(nodeName) - 4; // 3 for "..." and 1 for '\0' const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0'
if (len > maxLen) { const size_t needed = 3; // "..."
nodeName[maxLen] = '\0'; if (len > capForText - needed) {
len = maxLen; len = capForText - needed;
nodeName[len] = '\0';
} }
strcat(nodeName, "..."); std::strcat(nodeName, "...");
} }
} }
return nodeName; return nodeName;
} }
const char *getCurrentModeTitle(int screenWidth) const char *getCurrentModeTitle_Nodes(int screenWidth)
{ {
switch (currentMode) { switch (currentMode_Nodes) {
case MODE_LAST_HEARD: case MODE_LAST_HEARD:
return "Last Heard"; return "Last Heard";
case MODE_HOP_SIGNAL: case MODE_HOP_SIGNAL:
#ifdef USE_EINK #ifdef USE_EINK
return "Hops/Sig"; return "Hops/Sig";
#else #else
return (isHighResolution) ? "Hops/Signal" : "Hops/Sig"; return (currentResolution == ScreenResolution::High) ? "Hops/Signal" : "Hops/Sig";
#endif #endif
default:
return "Nodes";
}
}
const char *getCurrentModeTitle_Location(int screenWidth)
{
switch (currentMode_Location) {
case MODE_DISTANCE: case MODE_DISTANCE:
return "Distance"; return "Distance";
case MODE_BEARING:
return "Bearings";
default: default:
return "Nodes"; 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) 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) { 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 scrollbarX = display->getWidth() - 2;
int scrollbarHeight = display->getHeight() - scrollStartY - 10; int scrollbarHeight = display->getHeight() - scrollStartY - 10;
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); 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); int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
for (int i = 0; i < thumbHeight; i++) { 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) void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{ {
bool isLeftCol = (x < SCREEN_WIDTH / 2); 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]; char timeStr[10];
uint32_t seconds = sinceLastSeen(node); uint32_t seconds = sinceLastSeen(node);
@@ -188,9 +226,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); 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 (node->is_favorite) {
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); 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); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = columnWidth - 25; int nameMaxWidth = columnWidth - 25;
int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
int barsXOffset = columnWidth - barsOffset; int barsXOffset = columnWidth - barsOffset;
const char *nodeName = getSafeNodeName(display, node); const char *nodeName = getSafeNodeName(display, node, columnWidth);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); 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 (node->is_favorite) {
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); 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) void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{ {
bool isLeftCol = (x < SCREEN_WIDTH / 2); 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] = ""; char distStr[10] = "";
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); 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->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); 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 (node->is_favorite) {
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); 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) { if (strlen(distStr) > 0) {
int offset = (isHighResolution) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) int offset = (currentResolution == ScreenResolution::High)
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) ? (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 rightEdge = x + columnWidth - offset;
int textWidth = display->getStringWidth(distStr); int textWidth = display->getStringWidth(distStr);
display->drawString(rightEdge - textWidth, y, 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: case MODE_LAST_HEARD:
drawEntryLastHeard(display, node, x, y, columnWidth); drawEntryLastHeard(display, node, x, y, columnWidth);
break; break;
case MODE_HOP_SIGNAL: case MODE_HOP_SIGNAL:
drawEntryHopSignal(display, node, x, y, columnWidth); drawEntryHopSignal(display, node, x, y, columnWidth);
break; break;
case MODE_DISTANCE:
drawNodeDistance(display, node, x, y, columnWidth);
break;
default: default:
break; break;
} }
@@ -351,15 +388,16 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
// Adjust max text width depending on column and screen width // 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->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); 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 (node->is_favorite) {
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -374,7 +412,7 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
return; return;
bool isLeftCol = (x < SCREEN_WIDTH / 2); 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 centerX = x + columnWidth - arrowXOffset;
int centerY = y + FONT_HEIGHT_SMALL / 2; int centerY = y + FONT_HEIGHT_SMALL / 2;
@@ -431,11 +469,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
locationScreen = true; locationScreen = true;
else if (strcmp(title, "Distance") == 0) else if (strcmp(title, "Distance") == 0)
locationScreen = true; locationScreen = true;
#if defined(M5STACK_UNITC6L)
int columnWidth = display->getWidth();
#else
int columnWidth = display->getWidth() / 2;
#endif
display->clear(); display->clear();
// Draw the battery/time header // Draw the battery/time header
@@ -444,39 +477,74 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// Space below header // Space below header
y += COMMON_HEADER_HEIGHT; 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 totalEntries = nodeDB->getNumMeshNodes();
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
int numskipped = 0; int numskipped = 0;
int visibleNodeRows = totalRowsAvailable; 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 yOffset = 0;
int col = 0; int col = 0;
int lastNodeY = y; int lastNodeY = y;
int shownCount = 0; int shownCount = 0;
int rowCount = 0; int rowCount = 0;
for (int i = startIndex; i < endIndex; ++i) { for (int idx = startIndex; idx < endIndex; idx++) {
if (locationScreen && !nodeDB->getMeshNodeByIndex(i)->has_position) { uint32_t nodeNum = drawList[idx];
numskipped++; auto *node = nodeDB->getMeshNode(nodeNum);
continue;
}
int xPos = x + (col * columnWidth); int xPos = x + (col * columnWidth);
int yPos = y + yOffset; int yPos = y + yOffset;
renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
if (extras) { renderer(display, node, xPos, yPos, columnWidth);
extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon);
} if (extras)
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
yOffset += rowYOffset; yOffset += rowYOffset;
@@ -495,17 +563,73 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// This should correct the scrollbar // This should correct the scrollbar
totalEntries -= numskipped; totalEntries -= numskipped;
#if !defined(M5STACK_UNITC6L)
// Draw column separator // Draw column separator
if (shownCount > 0) { if (currentResolution != ScreenResolution::UltraLow && shownCount > 0) {
const int firstNodeY = y + 3; 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; const int scrollStartY = y + 3;
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY);
graphics::drawCommonFooter(display, x, y); 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 #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 variables to track mode and duration
static NodeListMode lastRenderedMode = MODE_COUNT; static ListMode_Node lastRenderedMode = MODE_COUNT_NODE;
static unsigned long modeStartTime = 0; static unsigned long modeStartTime = 0;
unsigned long now = millis(); unsigned long now = millis();
@@ -529,23 +654,65 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state,
} }
#endif #endif
// On very first call (on boot or state enter) // On very first call (on boot or state enter)
if (lastRenderedMode == MODE_COUNT) { if (lastRenderedMode == MODE_COUNT_NODE) {
currentMode = MODE_LAST_HEARD; currentMode_Nodes = MODE_LAST_HEARD;
modeStartTime = now; modeStartTime = now;
} }
// Time to switch to next mode? // Time to switch to next mode?
if (now - modeStartTime >= getModeCycleIntervalMs()) { 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; modeStartTime = now;
} }
// Render screen based on currentMode // Render screen based on currentMode
const char *title = getCurrentModeTitle(display->getWidth()); const char *title = getCurrentModeTitle_Nodes(display->getWidth());
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes);
// Track the last mode to avoid reinitializing modeStartTime // 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 #endif
@@ -566,14 +733,12 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
#endif #endif
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
} }
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
const char *title = "Distance"; const char *title = "Distance";
drawNodeListScreen(display, state, x, y, title, drawNodeDistance); drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
} }
#endif #endif
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
float heading = 0; float heading = 0;

View File

@@ -23,8 +23,11 @@ namespace NodeListRenderer
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); 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); typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
// Node list mode enumeration // Node list mode enumeration for Last Heard and Hop Signal views
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; 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 // Main node list screen function
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, 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 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 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 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); void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
// Extras renderers // 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 drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawHopSignalScreen(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 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); void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Utility functions // Utility functions
const char *getCurrentModeTitle(int screenWidth); const char *getCurrentModeTitle_Nodes(int screenWidth);
const char *getSafeNodeName(meshtastic_NodeInfoLite *node); 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); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
// Scrolling controls
void scrollUp();
void scrollDown();
// Bitmap drawing function // Bitmap drawing function
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display); 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" #include "configuration.h"
#if HAS_SCREEN
#if HAS_SCREEN
#include "DisplayFormatters.h" #include "DisplayFormatters.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "NotificationRenderer.h" #include "NotificationRenderer.h"
@@ -38,7 +38,7 @@ extern bool hasUnreadMessage;
namespace graphics namespace graphics
{ {
int bannerSignalBars = -1;
InputEvent NotificationRenderer::inEvent; InputEvent NotificationRenderer::inEvent;
int8_t NotificationRenderer::curSelected = 0; int8_t NotificationRenderer::curSelected = 0;
char NotificationRenderer::alertBannerMessage[256] = {0}; char NotificationRenderer::alertBannerMessage[256] = {0};
@@ -85,9 +85,13 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
void NotificationRenderer::resetBanner() void NotificationRenderer::resetBanner()
{ {
notificationTypeEnum previousType = current_notification_type;
alertBannerMessage[0] = '\0'; alertBannerMessage[0] = '\0';
current_notification_type = notificationTypeEnum::none; current_notification_type = notificationTypeEnum::none;
OnScreenKeyboardModule::instance().clearPopup();
inEvent.inputEvent = INPUT_BROKER_NONE; inEvent.inputEvent = INPUT_BROKER_NONE;
inEvent.kbchar = 0; inEvent.kbchar = 0;
curSelected = 0; curSelected = 0;
@@ -100,6 +104,13 @@ void NotificationRenderer::resetBanner()
currentNumber = 0; currentNumber = 0;
nodeDB->pause_sort(false); nodeDB->pause_sort(false);
// If we're exiting from text_input (virtual keyboard), stop module and trigger frame update
// to ensure any messages received during keyboard use are now displayed
if (previousType == notificationTypeEnum::text_input && screen) {
OnScreenKeyboardModule::instance().stop(false);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
} }
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
@@ -163,13 +174,15 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
// modulo to extract // modulo to extract
uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1)); uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1));
// Handle input // Handle input
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
if (this_digit == 9) { if (this_digit == 9) {
currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1)); currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1));
} else { } else {
currentNumber += (pow_of_10(numDigits - curSelected - 1)); currentNumber += (pow_of_10(numDigits - curSelected - 1));
} }
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
if (this_digit == 0) { if (this_digit == 0) {
currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1)); currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1));
} else { } else {
@@ -251,10 +264,10 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
// Handle input // Handle input
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
curSelected--; curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
curSelected++; curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
alertBannerCallback(selectedNodenum); alertBannerCallback(selectedNodenum);
@@ -308,7 +321,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
} }
if (i == curSelected) { if (i == curSelected) {
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); strncpy(scratchLineBuffer[scratchLineNum], "> ", 3);
strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36);
strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3);
@@ -368,10 +381,10 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
// Handle input // Handle input
if (alertBannerOptions > 0) { if (alertBannerOptions > 0) {
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
curSelected--; curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
curSelected++; curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
if (optionsEnumPtr != nullptr) { if (optionsEnumPtr != nullptr) {
@@ -436,7 +449,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
if (i == curSelected) { if (i == curSelected) {
if (isHighResolution) { if (currentResolution == ScreenResolution::High) {
strncpy(lineBuffer, "> ", 3); strncpy(lineBuffer, "> ", 3);
strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); strncpy(lineBuffer + 2, optionsArrayPtr[i], 36);
strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3);
@@ -464,7 +477,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
bool is_picker = false; bool is_picker = false;
uint16_t lineCount = 0; uint16_t lineCount = 0;
// === Layout Configuration === // Layout Configuration
constexpr uint16_t hPadding = 5; constexpr uint16_t hPadding = 5;
constexpr uint16_t vPadding = 2; constexpr uint16_t vPadding = 2;
bool needs_bell = false; bool needs_bell = false;
@@ -478,13 +491,32 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT); 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) { while (lines[lineCount] != nullptr) {
auto newlinePointer = strchr(lines[lineCount], '\n'); auto newlinePointer = strchr(lines[lineCount], '\n');
if (newlinePointer) if (newlinePointer)
lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first
else // if the newline wasn't found, then pull string length from strlen else // if the newline wasn't found, then pull string length from strlen
lineLengths[lineCount] = strlen(lines[lineCount]); lineLengths[lineCount] = strlen(lines[lineCount]);
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); 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) { if (!is_picker) {
needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr);
if (lineWidths[lineCount] > maxWidth) if (lineWidths[lineCount] > maxWidth)
@@ -494,12 +526,16 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
} }
// count lines // count lines
// Ensure box accounts for signal bars if present
if (widestLineWithBars > maxWidth)
maxWidth = widestLineWithBars;
uint16_t boxWidth = hPadding * 2 + maxWidth; uint16_t boxWidth = hPadding * 2 + maxWidth;
#if defined(M5STACK_UNITC6L)
if (needs_bell) { if (needs_bell) {
if (isHighResolution && boxWidth <= 150) if ((currentResolution == ScreenResolution::High) && boxWidth <= 150)
boxWidth += 26; boxWidth += 26;
if (!isHighResolution && boxWidth <= 100) if ((currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) && boxWidth <= 100)
boxWidth += 20; boxWidth += 20;
} }
@@ -508,14 +544,17 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
uint16_t boxHeight = contentHeight + vPadding * 2; uint16_t boxHeight = contentHeight + vPadding * 2;
if (visibleTotalLines == 1) if (visibleTotalLines == 1) {
boxHeight += (isHighResolution ? 4 : 3); boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3;
}
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
if (totalLines > visibleTotalLines) if (totalLines > visibleTotalLines) {
boxWidth += (isHighResolution ? 4 : 2); boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2;
}
int16_t boxTop = (display->height() / 2) - (boxHeight / 2); int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1;
#if defined(M5STACK_UNITC6L)
if (visibleTotalLines == 1) { if (visibleTotalLines == 1) {
boxTop += 25; boxTop += 25;
} }
@@ -526,127 +565,9 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
if (boxTop < 0) if (boxTop < 0)
boxTop = 0; boxTop = 0;
} }
#endif
// === Draw Box === // 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 ===
display->setColor(BLACK); display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
@@ -662,7 +583,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE); display->setColor(WHITE);
// === Draw Content === // Draw Content
int16_t lineY = boxTop + vPadding; int16_t lineY = boxTop + vPadding;
for (int i = 0; i < lineCount; i++) { for (int i = 0; i < lineCount; i++) {
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
@@ -691,17 +612,47 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
lineY += (effectiveLineHeight - 2 - background_yOffset); lineY += (effectiveLineHeight - 2 - background_yOffset);
} else { } else {
// Pop-up // 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); lineY += (effectiveLineHeight);
} }
} }
// === Scroll Bar (Thicker, inside box, not over title) === // Scroll Bar (Thicker, inside box, not over title)
if (totalLines > visibleTotalLines) { if (totalLines > visibleTotalLines) {
const uint8_t scrollBarWidth = 5; const uint8_t scrollBarWidth = 5;
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; 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; uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
float ratio = (float)visibleTotalLines / totalLines; float ratio = (float)visibleTotalLines / totalLines;
@@ -712,7 +663,6 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
} }
#endif
} }
/// Draw the last text message we received /// Draw the last text message we received
@@ -769,40 +719,8 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
} }
if (inEvent.inputEvent != INPUT_BROKER_NONE) { if (inEvent.inputEvent != INPUT_BROKER_NONE) {
if (inEvent.inputEvent == INPUT_BROKER_UP) { bool handled = OnScreenKeyboardModule::processVirtualKeyboardInput(inEvent, virtualKeyboard);
// high frequency for move cursor left/right than up/down with encoders if (!handled && inEvent.inputEvent == INPUT_BROKER_CANCEL) {
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
virtualKeyboard->moveCursorLeft();
} else {
virtualKeyboard->moveCursorUp();
}
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN) {
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
virtualKeyboard->moveCursorRight();
} else {
virtualKeyboard->moveCursorDown();
}
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
virtualKeyboard->moveCursorLeft();
} else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) {
virtualKeyboard->moveCursorRight();
} else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
virtualKeyboard->moveCursorUp();
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
virtualKeyboard->moveCursorDown();
} else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
virtualKeyboard->moveCursorLeft();
} else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
virtualKeyboard->moveCursorRight();
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
virtualKeyboard->handlePress();
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) {
virtualKeyboard->handleLongPress();
} else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) {
auto callback = textInputCallback; auto callback = textInputCallback;
delete virtualKeyboard; delete virtualKeyboard;
virtualKeyboard = nullptr; virtualKeyboard = nullptr;
@@ -821,12 +739,28 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
inEvent.inputEvent = INPUT_BROKER_NONE; inEvent.inputEvent = INPUT_BROKER_NONE;
} }
// Re-check pointer before drawing to avoid use-after-free and crashes
if (!virtualKeyboard) {
// Ensure we exit text_input state and restore frames
if (current_notification_type == notificationTypeEnum::text_input) {
resetBanner();
}
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
// If screen is null, do nothing (safe fallback)
return;
}
// Clear the screen to avoid overlapping with underlying frames or overlays // Clear the screen to avoid overlapping with underlying frames or overlays
display->setColor(BLACK); display->setColor(BLACK);
display->fillRect(0, 0, display->getWidth(), display->getHeight()); display->fillRect(0, 0, display->getWidth(), display->getHeight());
display->setColor(WHITE); display->setColor(WHITE);
// Draw the virtual keyboard // Draw the virtual keyboard
virtualKeyboard->draw(display, 0, 0); virtualKeyboard->draw(display, 0, 0);
// Draw transient popup overlay (if any) managed by OnScreenKeyboardModule
OnScreenKeyboardModule::instance().drawPopupOverlay(display);
} else { } else {
// If virtualKeyboard is null, reset the banner to avoid getting stuck // If virtualKeyboard is null, reset the banner to avoid getting stuck
LOG_INFO("Virtual keyboard is null - resetting banner"); LOG_INFO("Virtual keyboard is null - resetting banner");
@@ -839,5 +773,12 @@ bool NotificationRenderer::isOverlayBannerShowing()
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
} }
void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs)
{
if (!title || !content || current_notification_type != notificationTypeEnum::text_input)
return;
OnScreenKeyboardModule::instance().showPopup(title, content, durationMs);
}
} // namespace graphics } // namespace graphics
#endif #endif

View File

@@ -4,6 +4,7 @@
#include "OLEDDisplayUi.h" #include "OLEDDisplayUi.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/VirtualKeyboard.h" #include "graphics/VirtualKeyboard.h"
#include "modules/OnScreenKeyboardModule.h"
#include <functional> #include <functional>
#include <string> #include <string>
#define MAX_LINES 5 #define MAX_LINES 5
@@ -31,6 +32,7 @@ class NotificationRenderer
static bool pauseBanner; static bool pauseBanner;
static void resetBanner(); static void resetBanner();
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);

View File

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

View File

@@ -34,7 +34,7 @@ class UIRenderer
public: public:
// Common UI elements // Common UI elements
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, 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 // GPS status functions
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); 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 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); 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 // Overlay and special screens
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); 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 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); 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) // Check if the display can render a string (detect special chars; emoji)
static bool haveGlyphs(const char *str); static bool haveGlyphs(const char *str);
}; // namespace UIRenderer }; // namespace UIRenderer

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, 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}; 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_width 8
#define chirpy_small_image_height 8 #define chirpy_small_image_height 8
const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};

View File

@@ -8,4 +8,5 @@ build_flags =
-D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling -D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling
-D HAS_BUTTON=0 ; Suppress default ButtonThread -D HAS_BUTTON=0 ; Suppress default ButtonThread
lib_deps = 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

@@ -53,6 +53,7 @@ typedef struct _InputEvent {
class InputPollable class InputPollable
{ {
public: public:
virtual ~InputPollable() = default;
virtual void pollOnce() = 0; virtual void pollOnce() = 0;
}; };

View File

@@ -3,6 +3,9 @@
#include "RotaryEncoderImpl.h" #include "RotaryEncoderImpl.h"
#include "InputBroker.h" #include "InputBroker.h"
#include "RotaryEncoder.h" #include "RotaryEncoder.h"
#ifdef ARCH_ESP32
#include "sleep.h"
#endif
#define ORIGIN_NAME "RotaryEncoder" #define ORIGIN_NAME "RotaryEncoder"
@@ -11,6 +14,20 @@ RotaryEncoderImpl *rotaryEncoderImpl;
RotaryEncoderImpl::RotaryEncoderImpl() RotaryEncoderImpl::RotaryEncoderImpl()
{ {
rotary = nullptr; rotary = nullptr;
#ifdef ARCH_ESP32
isFirstInit = true;
#endif
}
RotaryEncoderImpl::~RotaryEncoderImpl()
{
LOG_DEBUG("RotaryEncoderImpl destructor");
detachRotaryEncoderInterrupts();
if (rotary != nullptr) {
delete rotary;
rotary = nullptr;
}
} }
bool RotaryEncoderImpl::init() bool RotaryEncoderImpl::init()
@@ -25,15 +42,22 @@ bool RotaryEncoderImpl::init()
eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw); eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw);
eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press); eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, if (rotary == nullptr) {
moduleConfig.canned_message.inputbroker_pin_press); rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
rotary->resetButton(); moduleConfig.canned_message.inputbroker_pin_press);
}
interruptInstance = this; attachRotaryEncoderInterrupts();
auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); };
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE); #ifdef ARCH_ESP32
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE); // Register callbacks for before and after lightsleep
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE); // Used to detach and reattach interrupts
if (isFirstInit) {
lsObserver.observe(&notifyLightSleep);
lsEndObserver.observe(&notifyLightSleepEnd);
isFirstInit = false;
}
#endif
LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a,
moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw,
@@ -71,6 +95,50 @@ void RotaryEncoderImpl::pollOnce()
} }
} }
void RotaryEncoderImpl::detachRotaryEncoderInterrupts()
{
LOG_DEBUG("RotaryEncoderImpl detach button interrupts");
if (interruptInstance == this) {
detachInterrupt(moduleConfig.canned_message.inputbroker_pin_a);
detachInterrupt(moduleConfig.canned_message.inputbroker_pin_b);
detachInterrupt(moduleConfig.canned_message.inputbroker_pin_press);
interruptInstance = nullptr;
} else {
LOG_WARN("RotaryEncoderImpl: interrupts already detached");
}
}
void RotaryEncoderImpl::attachRotaryEncoderInterrupts()
{
LOG_DEBUG("RotaryEncoderImpl attach button interrupts");
if (rotary != nullptr && interruptInstance == nullptr) {
rotary->resetButton();
interruptInstance = this;
auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); };
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE);
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE);
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE);
} else {
LOG_WARN("RotaryEncoderImpl: interrupts already attached");
}
}
#ifdef ARCH_ESP32
int RotaryEncoderImpl::beforeLightSleep(void *unused)
{
detachRotaryEncoderInterrupts();
return 0; // Indicates success;
}
int RotaryEncoderImpl::afterLightSleep(esp_sleep_wakeup_cause_t cause)
{
attachRotaryEncoderInterrupts();
return 0; // Indicates success;
}
#endif
RotaryEncoderImpl *RotaryEncoderImpl::interruptInstance; RotaryEncoderImpl *RotaryEncoderImpl::interruptInstance;
#endif #endif

View File

@@ -8,12 +8,18 @@
class RotaryEncoder; class RotaryEncoder;
class RotaryEncoderImpl : public InputPollable class RotaryEncoderImpl final : public InputPollable
{ {
public: public:
RotaryEncoderImpl(); RotaryEncoderImpl();
bool init(void); ~RotaryEncoderImpl() override;
bool init();
virtual void pollOnce() override; virtual void pollOnce() override;
// Disconnect and reconnect interrupts for light sleep
#ifdef ARCH_ESP32
int beforeLightSleep(void *unused);
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
#endif
protected: protected:
static RotaryEncoderImpl *interruptInstance; static RotaryEncoderImpl *interruptInstance;
@@ -23,6 +29,21 @@ class RotaryEncoderImpl : public InputPollable
input_broker_event eventPressed = INPUT_BROKER_NONE; input_broker_event eventPressed = INPUT_BROKER_NONE;
RotaryEncoder *rotary; RotaryEncoder *rotary;
private:
#ifdef ARCH_ESP32
bool isFirstInit;
#endif
void detachRotaryEncoderInterrupts();
void attachRotaryEncoderInterrupts();
#ifdef ARCH_ESP32
// Get notified when lightsleep begins and ends
CallbackObserver<RotaryEncoderImpl, void *> lsObserver =
CallbackObserver<RotaryEncoderImpl, void *>(this, &RotaryEncoderImpl::beforeLightSleep);
CallbackObserver<RotaryEncoderImpl, esp_sleep_wakeup_cause_t> lsEndObserver =
CallbackObserver<RotaryEncoderImpl, esp_sleep_wakeup_cause_t>(this, &RotaryEncoderImpl::afterLightSleep);
#endif
}; };
extern RotaryEncoderImpl *rotaryEncoderImpl; extern RotaryEncoderImpl *rotaryEncoderImpl;

View File

@@ -88,6 +88,50 @@ int32_t TrackballInterruptBase::runOnce()
} }
} }
if (directionDetected && directionStartTime > 0) {
uint32_t directionDuration = millis() - directionStartTime;
uint8_t directionPressedNow = 0;
directionInterval++;
if (!digitalRead(_pinUp)) {
directionPressedNow = TB_ACTION_UP;
} else if (!digitalRead(_pinDown)) {
directionPressedNow = TB_ACTION_DOWN;
} else if (!digitalRead(_pinLeft)) {
directionPressedNow = TB_ACTION_LEFT;
} else if (!digitalRead(_pinRight)) {
directionPressedNow = TB_ACTION_RIGHT;
}
const uint8_t DIRECTION_REPEAT_THRESHOLD = 3;
if (directionPressedNow == TB_ACTION_NONE) {
// Reset state
directionDetected = false;
directionStartTime = 0;
directionInterval = 0;
this->action = TB_ACTION_NONE;
} else if (directionDuration >= LONG_PRESS_DURATION && directionInterval >= DIRECTION_REPEAT_THRESHOLD) {
// repeat event when long press these direction.
switch (directionPressedNow) {
case TB_ACTION_UP:
e.inputEvent = this->_eventUp;
break;
case TB_ACTION_DOWN:
e.inputEvent = this->_eventDown;
break;
case TB_ACTION_LEFT:
e.inputEvent = this->_eventLeft;
break;
case TB_ACTION_RIGHT:
e.inputEvent = this->_eventRight;
break;
}
directionInterval = 0;
}
}
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
if (this->action == TB_ACTION_PRESSED && !pressDetected) { if (this->action == TB_ACTION_PRESSED && !pressDetected) {
// Start long press detection // Start long press detection
@@ -113,17 +157,22 @@ int32_t TrackballInterruptBase::runOnce()
pressDetected = true; pressDetected = true;
pressStartTime = millis(); pressStartTime = millis();
// Don't send event yet, wait to see if it's a long press // Don't send event yet, wait to see if it's a long press
} else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) { } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp) && !directionDetected) {
// LOG_DEBUG("Trackball event UP"); directionDetected = true;
directionStartTime = millis();
e.inputEvent = this->_eventUp; e.inputEvent = this->_eventUp;
} else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown)) { // send event first,will automatically trigger every 50ms * 3 after 500ms
// LOG_DEBUG("Trackball event DOWN"); } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown) && !directionDetected) {
directionDetected = true;
directionStartTime = millis();
e.inputEvent = this->_eventDown; e.inputEvent = this->_eventDown;
} else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft)) { } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft) && !directionDetected) {
// LOG_DEBUG("Trackball event LEFT"); directionDetected = true;
directionStartTime = millis();
e.inputEvent = this->_eventLeft; e.inputEvent = this->_eventLeft;
} else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight)) { } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight) && !directionDetected) {
// LOG_DEBUG("Trackball event RIGHT"); directionDetected = true;
directionStartTime = millis();
e.inputEvent = this->_eventRight; e.inputEvent = this->_eventRight;
} }
#endif #endif

View File

@@ -49,10 +49,14 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
// Long press detection for press button // Long press detection for press button
uint32_t pressStartTime = 0; uint32_t pressStartTime = 0;
uint32_t directionStartTime = 0;
uint8_t directionInterval = 0;
bool pressDetected = false; bool pressDetected = false;
bool directionDetected = false;
uint32_t lastLongPressEventTime = 0; uint32_t lastLongPressEventTime = 0;
uint32_t lastDirectionPressEventTime = 0;
static const uint32_t LONG_PRESS_DURATION = 500; // ms static const uint32_t LONG_PRESS_DURATION = 500; // ms
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; // ms - interval between repeated long press events
private: private:
input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventDown = INPUT_BROKER_NONE;

View File

@@ -3,6 +3,14 @@
#include "InputBroker.h" #include "InputBroker.h"
#include "mesh/NodeDB.h" #include "mesh/NodeDB.h"
#ifndef UPDOWN_LONG_PRESS_DURATION
#define UPDOWN_LONG_PRESS_DURATION 300
#endif
#ifndef UPDOWN_LONG_PRESS_REPEAT_INTERVAL
#define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 300
#endif
class UpDownInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread class UpDownInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread
{ {
public: public:
@@ -40,8 +48,8 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, public concur
uint32_t lastPressLongEventTime = 0; uint32_t lastPressLongEventTime = 0;
uint32_t lastUpLongEventTime = 0; uint32_t lastUpLongEventTime = 0;
uint32_t lastDownLongEventTime = 0; uint32_t lastDownLongEventTime = 0;
static const uint32_t LONG_PRESS_DURATION = 300; static const uint32_t LONG_PRESS_DURATION = UPDOWN_LONG_PRESS_DURATION;
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; static const uint32_t LONG_PRESS_REPEAT_INTERVAL = UPDOWN_LONG_PRESS_REPEAT_INTERVAL;
private: private:
uint8_t _pinDown = 0; uint8_t _pinDown = 0;

View File

@@ -489,8 +489,6 @@ int32_t KbI2cBase::runOnce()
case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT
case 0x91: // fn+t case 0x91: // fn+t
case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE 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 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE
case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST
// just pass those unmodified // just pass those unmodified

View File

@@ -428,10 +428,17 @@ void setup()
#endif #endif
#if ARCH_PORTDUINO #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; struct timeval tv;
tv.tv_sec = time(NULL); tv.tv_sec = time(NULL);
tv.tv_usec = 0; tv.tv_usec = 0;
perhapsSetRTC(RTCQualityDevice, &tv); perhapsSetRTC(ourQuality, &tv);
#endif #endif
powerMonInit(); powerMonInit();
@@ -439,6 +446,13 @@ void setup()
LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n");
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
#ifndef SENSECAP_INDICATOR
// use PSRAM for malloc calls > 256 bytes
heap_caps_malloc_extmem_enable(256);
#endif
#endif
#if defined(DEBUG_MUTE) && defined(DEBUG_PORT) #if defined(DEBUG_MUTE) && defined(DEBUG_PORT)
DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n"); DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n");
DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO));

View File

@@ -96,6 +96,8 @@ class Channels
bool setDefaultPresetCryptoForHash(ChannelHash channelHash); bool setDefaultPresetCryptoForHash(ChannelHash channelHash);
int16_t getHash(ChannelIndex i) { return hashes[i]; }
private: private:
/** Given a channel index, change to use the crypto key specified by that index /** 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 generateHash(ChannelIndex channelNum);
int16_t getHash(ChannelIndex i) { return hashes[i]; }
/** /**
* Validate a channel, fixing any errors as needed * 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 REDRAW_ONLY, // Don't change which frames are show, just redraw, asap
REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus() REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus()
REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout 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; } action = REDRAW_ONLY;
// We might want to pass additional data inside this struct at some point // We might want to pass additional data inside this struct at some point
@@ -225,4 +226,4 @@ class MeshModule
/** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet /** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet
* This ensures that if the request packet was sent reliably, the reply is sent that way as well. * This ensures that if the request packet was sent reliably, the reply is sent that way as well.
*/ */
void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to); void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to);

View File

@@ -7,10 +7,12 @@
#include "../concurrency/Periodic.h" #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 "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 "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "PowerFSM.h" #include "PowerFSM.h"
#include "RTC.h" #include "RTC.h"
#include "TypeConversions.h" #include "TypeConversions.h"
#include "graphics/draw/MessageRenderer.h"
#include "main.h" #include "main.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "meshUtils.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.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 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 // Send the packet into the mesh
DEBUG_HEAP_BEFORE; DEBUG_HEAP_BEFORE;
auto a = packetPool.allocCopy(p); auto a = packetPool.allocCopy(p);

View File

@@ -805,11 +805,15 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.output_ms = 500; moduleConfig.external_notification.output_ms = 500;
moduleConfig.external_notification.nag_timeout = 2; moduleConfig.external_notification.nag_timeout = 2;
#endif #endif
#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) #if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE)
// Default to RAK led pin 2 (blue) // Default to RAK led pin 2 (blue)
moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output = PIN_LED2; moduleConfig.external_notification.output = PIN_LED2;
#if defined(MUZI_BASE)
moduleConfig.external_notification.active = false;
#else
moduleConfig.external_notification.active = true; moduleConfig.external_notification.active = true;
#endif
moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.output_ms = 1000;
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;

View File

@@ -503,6 +503,11 @@ void RadioInterface::applyModemConfig()
cr = 5; cr = 5;
sf = 10; sf = 10;
break; break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
bw = (myRegion->wideLora) ? 1625.0 : 500;
cr = 8;
sf = 11;
break;
default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal. default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal.
bw = (myRegion->wideLora) ? 812.5 : 250; bw = (myRegion->wideLora) ? 812.5 : 250;
cr = 5; cr = 5;
@@ -539,13 +544,26 @@ void RadioInterface::applyModemConfig()
} }
if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) { if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) {
static const char *err_string = "Regional frequency range is smaller than bandwidth. Fall back to default preset"; const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f;
LOG_ERROR(err_string); const float requestedBwKHz = bw;
const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset
const char *presetName =
DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset);
char err_string[160];
if (isWideRequest) {
snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.",
myRegion->name, presetName);
} else {
snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.",
myRegion->name, regionSpanKHz, requestedBwKHz);
}
LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_ERROR; cn->level = meshtastic_LogRecord_Level_ERROR;
sprintf(cn->message, err_string); snprintf(cn->message, sizeof(cn->message), "%s", err_string);
service->sendClientNotification(cn); service->sendClientNotification(cn);
// Set to default modem preset // Set to default modem preset

View File

@@ -150,7 +150,9 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas
PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0; PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0;
// We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records // We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records
if (ackId || nakId) { if ((ackId || nakId) &&
// Implicit ACKs from MQTT should not stop retransmissions
!(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) {
LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId); LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId);
if (ackId) { if (ackId) {
stopRetransmission(p->to, ackId); stopRetransmission(p->to, ackId);

View File

@@ -37,8 +37,8 @@
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool; static MemoryDynamic<meshtastic_MeshPacket> dynamicPool;
Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool; Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
#elif defined(ARCH_STM32WL) #elif defined(ARCH_STM32WL) || defined(BOARD_HAS_PSRAM)
// On STM32 there isn't enough heap left over for the rest of the firmware if we allocate this statically. // On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this statically.
// For now, make it dynamic again. // For now, make it dynamic again.
#define MAX_PACKETS \ #define MAX_PACKETS \
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
@@ -526,6 +526,10 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
#elif ARCH_PORTDUINO #elif ARCH_PORTDUINO
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
} else if (portduino_config.JSONFilename != "") {
if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) {
JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl;
}
} }
#endif #endif
return DecodeState::DECODE_SUCCESS; return DecodeState::DECODE_SUCCESS;
@@ -688,7 +692,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
// Store a copy of encrypted packet for MQTT // Store a copy of encrypted packet for MQTT
DEBUG_HEAP_BEFORE; DEBUG_HEAP_BEFORE;
meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p); p_encrypted = packetPool.allocCopy(*p);
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
// Take those raw bytes and convert them back into a well structured protobuf we can understand // Take those raw bytes and convert them back into a well structured protobuf we can understand
@@ -754,6 +758,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
} }
packetPool.release(p_encrypted); // Release the encrypted packet packetPool.release(p_encrypted); // Release the encrypted packet
p_encrypted = nullptr;
} }
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)

View File

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

View File

@@ -293,7 +293,8 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
typedef enum _meshtastic_Config_LoRaConfig_ModemPreset { typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
/* Long Range - Fast */ /* Long Range - Fast */
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST = 0, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST = 0,
/* Long Range - Slow */ /* Long Range - Slow
Deprecated in 2.7: Unpopular slow preset. */
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW = 1, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW = 1,
/* Very Long Range - Slow /* Very Long Range - Slow
Deprecated in 2.5: Works only with txco and is unusably slow */ Deprecated in 2.5: Works only with txco and is unusably slow */
@@ -311,7 +312,10 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
/* Short Range - Turbo /* Short Range - Turbo
This is the fastest preset and the only one with 500kHz bandwidth. This is the fastest preset and the only one with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth. */ It is not legal to use in all regions due to this wider bandwidth. */
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8 meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8,
/* Long Range - Turbo
This preset performs similarly to LongFast, but with 500Khz bandwidth. */
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9
} meshtastic_Config_LoRaConfig_ModemPreset; } meshtastic_Config_LoRaConfig_ModemPreset;
typedef enum _meshtastic_Config_BluetoothConfig_PairingMode { typedef enum _meshtastic_Config_BluetoothConfig_PairingMode {
@@ -689,8 +693,8 @@ extern "C" {
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1)) #define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1))
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST #define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO #define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO+1)) #define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1))
#define _meshtastic_Config_BluetoothConfig_PairingMode_MIN meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN #define _meshtastic_Config_BluetoothConfig_PairingMode_MIN meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN
#define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN #define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN

View File

@@ -237,8 +237,8 @@ typedef enum _meshtastic_HardwareModel {
meshtastic_HardwareModel_T_ETH_ELITE = 91, meshtastic_HardwareModel_T_ETH_ELITE = 91,
/* Heltec HRI-3621 industrial probe */ /* Heltec HRI-3621 industrial probe */
meshtastic_HardwareModel_HELTEC_SENSOR_HUB = 92, meshtastic_HardwareModel_HELTEC_SENSOR_HUB = 92,
/* Reserved Fried Chicken ID for future use */ /* Muzi Works Muzi-Base device */
meshtastic_HardwareModel_RESERVED_FRIED_CHICKEN = 93, meshtastic_HardwareModel_MUZI_BASE = 93,
/* Heltec Magnetic Power Bank with Meshtastic compatible */ /* Heltec Magnetic Power Bank with Meshtastic compatible */
meshtastic_HardwareModel_HELTEC_MESH_POCKET = 94, meshtastic_HardwareModel_HELTEC_MESH_POCKET = 94,
/* Seeed Solar Node */ /* Seeed Solar Node */

View File

@@ -417,6 +417,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
} }
case meshtastic_AdminMessage_enter_dfu_mode_request_tag: { case meshtastic_AdminMessage_enter_dfu_mode_request_tag: {
LOG_INFO("Client requesting to enter DFU mode"); LOG_INFO("Client requesting to enter DFU mode");
#if HAS_SCREEN
IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0));
#endif
#if defined(ARCH_NRF52) || defined(ARCH_RP2040) #if defined(ARCH_NRF52) || defined(ARCH_RP2040)
enterDfuMode(); enterDfuMode();
#endif #endif

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 === // === State/UI ===
bool shouldDraw(); bool shouldDraw();
bool hasMessages(); bool hasMessages();
void showTemporaryMessage(const String &message);
void resetSearch(); void resetSearch();
void updateDestinationSelectionList(); void updateDestinationSelectionList();
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); 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; unsigned long lastUpdateMillis = 0;
String searchQuery; String searchQuery;
String freetext; String freetext;
String temporaryMessage;
// === Message Storage === // === 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]; char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT];
int messagesCount = 0; int messagesCount = 0;
int currentMessageIndex = -1; 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) 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 ChannelIndex channel = 0; // Channel index used when sending a message
bool ack = false; // True = ACK received, False = NACK or failed 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 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 float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
uint8_t lastAckHopStart = 0; // Hop start value from the received ACK packet int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
uint8_t lastAckHopLimit = 0; // Hop limit value from the received ACK packet uint32_t lastRequestId = 0; // tracks the request_id of our last sent 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)
// === State Tracking === // === State Tracking ===
cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;

View File

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

View File

@@ -168,7 +168,7 @@ int32_t ExternalNotificationModule::runOnce()
delay = EXT_NOTIFICATION_FAST_THREAD_MS; delay = EXT_NOTIFICATION_FAST_THREAD_MS;
#endif #endif
#ifdef T_WATCH_S3 #if defined(T_WATCH_S3) || defined(T_LORA_PAGER)
drv.go(); drv.go();
#endif #endif
} }
@@ -283,7 +283,7 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on)
#ifdef UNPHONE #ifdef UNPHONE
unphone.rgb(red, green, blue); unphone.rgb(red, green, blue);
#endif #endif
#ifdef T_WATCH_S3 #if defined(T_WATCH_S3) || defined(T_LORA_PAGER)
if (on) { if (on) {
drv.go(); drv.go();
} else { } else {
@@ -319,7 +319,7 @@ void ExternalNotificationModule::stopNow()
externalTurnedOn[i] = 0; externalTurnedOn[i] = 0;
} }
setIntervalFromNow(0); setIntervalFromNow(0);
#ifdef T_WATCH_S3 #if defined(T_WATCH_S3) || defined(T_LORA_PAGER)
drv.stop(); drv.stop();
#endif #endif
@@ -541,6 +541,19 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
(!isBroadcast(mp.to) && isToUs(&mp))) { (!isBroadcast(mp.to) && isToUs(&mp))) {
// Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us
isNagging = true; isNagging = true;
#ifdef T_LORA_PAGER
if (canBuzz()) {
drv.setWaveform(0, 16); // Long buzzer 100%
drv.setWaveform(1, 0); // Pause
drv.setWaveform(2, 16);
drv.setWaveform(3, 0);
drv.setWaveform(4, 16);
drv.setWaveform(5, 0);
drv.setWaveform(6, 16);
drv.setWaveform(7, 0);
drv.go();
}
#endif
if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) {
setExternalState(2, true); setExternalState(2, true);
} else { } else {

View File

@@ -181,25 +181,25 @@ void setupModules()
// new ReplyModule(); // new ReplyModule();
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
#ifndef T_LORA_PAGER #if defined(T_LORA_PAGER)
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
if (!rotaryEncoderInterruptImpl1->init()) {
delete rotaryEncoderInterruptImpl1;
rotaryEncoderInterruptImpl1 = nullptr;
}
#elif defined(T_LORA_PAGER)
// use a special FSM based rotary encoder version for T-LoRa Pager // use a special FSM based rotary encoder version for T-LoRa Pager
rotaryEncoderImpl = new RotaryEncoderImpl(); rotaryEncoderImpl = new RotaryEncoderImpl();
if (!rotaryEncoderImpl->init()) { if (!rotaryEncoderImpl->init()) {
delete rotaryEncoderImpl; delete rotaryEncoderImpl;
rotaryEncoderImpl = nullptr; rotaryEncoderImpl = nullptr;
} }
#else #elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2)
upDownInterruptImpl1 = new UpDownInterruptImpl1(); upDownInterruptImpl1 = new UpDownInterruptImpl1();
if (!upDownInterruptImpl1->init()) { if (!upDownInterruptImpl1->init()) {
delete upDownInterruptImpl1; delete upDownInterruptImpl1;
upDownInterruptImpl1 = nullptr; upDownInterruptImpl1 = nullptr;
} }
#else
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
if (!rotaryEncoderInterruptImpl1->init()) {
delete rotaryEncoderInterruptImpl1;
rotaryEncoderInterruptImpl1 = nullptr;
}
#endif #endif
cardKbI2cImpl = new CardKbI2cImpl(); cardKbI2cImpl = new CardKbI2cImpl();
cardKbI2cImpl->init(); cardKbI2cImpl->init();
@@ -217,7 +217,7 @@ void setupModules()
} }
#endif // HAS_BUTTON #endif // HAS_BUTTON
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") {
seesawRotary = new SeesawRotary("SeesawRotary"); seesawRotary = new SeesawRotary("SeesawRotary");
if (!seesawRotary->init()) { if (!seesawRotary->init()) {
delete seesawRotary; delete seesawRotary;

View File

@@ -0,0 +1,272 @@
#include "configuration.h"
#if HAS_SCREEN
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/NotificationRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#include "modules/OnScreenKeyboardModule.h"
#include <Arduino.h>
#include <algorithm>
namespace graphics
{
OnScreenKeyboardModule &OnScreenKeyboardModule::instance()
{
static OnScreenKeyboardModule inst;
return inst;
}
OnScreenKeyboardModule::~OnScreenKeyboardModule()
{
if (keyboard) {
delete keyboard;
keyboard = nullptr;
}
}
void OnScreenKeyboardModule::start(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> cb)
{
if (keyboard) {
delete keyboard;
keyboard = nullptr;
}
keyboard = new VirtualKeyboard();
callback = cb;
if (header)
keyboard->setHeader(header);
if (initialText)
keyboard->setInputText(initialText);
// Route VK submission/cancel events back into the module
keyboard->setCallback([this](const std::string &text) {
if (text.empty()) {
this->onCancel();
} else {
this->onSubmit(text);
}
});
// Maintain legacy compatibility hooks
NotificationRenderer::virtualKeyboard = keyboard;
NotificationRenderer::textInputCallback = callback;
}
void OnScreenKeyboardModule::stop(bool callEmptyCallback)
{
auto cb = callback;
callback = nullptr;
if (keyboard) {
delete keyboard;
keyboard = nullptr;
}
// Keep NotificationRenderer legacy pointers in sync
NotificationRenderer::virtualKeyboard = nullptr;
NotificationRenderer::textInputCallback = nullptr;
clearPopup();
if (callEmptyCallback && cb)
cb("");
}
void OnScreenKeyboardModule::handleInput(const InputEvent &event)
{
if (!keyboard)
return;
if (processVirtualKeyboardInput(event, keyboard))
return;
if (event.inputEvent == INPUT_BROKER_CANCEL)
onCancel();
}
bool OnScreenKeyboardModule::processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *targetKeyboard)
{
if (!targetKeyboard)
return false;
switch (event.inputEvent) {
case INPUT_BROKER_UP:
case INPUT_BROKER_UP_LONG:
targetKeyboard->moveCursorUp();
return true;
case INPUT_BROKER_DOWN:
case INPUT_BROKER_DOWN_LONG:
targetKeyboard->moveCursorDown();
return true;
case INPUT_BROKER_LEFT:
case INPUT_BROKER_ALT_PRESS:
targetKeyboard->moveCursorLeft();
return true;
case INPUT_BROKER_RIGHT:
case INPUT_BROKER_USER_PRESS:
targetKeyboard->moveCursorRight();
return true;
case INPUT_BROKER_SELECT:
targetKeyboard->handlePress();
return true;
case INPUT_BROKER_SELECT_LONG:
targetKeyboard->handleLongPress();
return true;
default:
return false;
}
}
bool OnScreenKeyboardModule::draw(OLEDDisplay *display)
{
if (!keyboard)
return false;
// Timeout
if (keyboard->isTimedOut()) {
onCancel();
return false;
}
// Clear full screen behind keyboard
display->setColor(BLACK);
display->fillRect(0, 0, display->getWidth(), display->getHeight());
display->setColor(WHITE);
keyboard->draw(display, 0, 0);
// Draw popup overlay if needed
drawPopup(display);
return true;
}
void OnScreenKeyboardModule::onSubmit(const std::string &text)
{
auto cb = callback;
stop(false);
if (cb)
cb(text);
}
void OnScreenKeyboardModule::onCancel()
{
stop(true);
}
void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs)
{
if (!title || !content)
return;
strncpy(popupTitle, title, sizeof(popupTitle) - 1);
popupTitle[sizeof(popupTitle) - 1] = '\0';
strncpy(popupMessage, content, sizeof(popupMessage) - 1);
popupMessage[sizeof(popupMessage) - 1] = '\0';
popupUntil = millis() + durationMs;
popupVisible = true;
}
void OnScreenKeyboardModule::clearPopup()
{
popupTitle[0] = '\0';
popupMessage[0] = '\0';
popupUntil = 0;
popupVisible = false;
}
void OnScreenKeyboardModule::drawPopupOverlay(OLEDDisplay *display)
{
// Only render the popup overlay (without drawing the keyboard)
drawPopup(display);
}
void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
{
if (!popupVisible)
return;
if (millis() > popupUntil || popupMessage[0] == '\0') {
popupVisible = false;
return;
}
// Build lines and leverage NotificationRenderer inverted box drawing for consistent style
constexpr uint16_t maxContentLines = 3;
const bool hasTitle = popupTitle[0] != '\0';
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
const uint16_t maxWrapWidth = display->width() - 40;
auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector<std::string> {
std::vector<std::string> wrapped;
std::string current;
std::string word;
const char *p = text;
while (*p && wrapped.size() < maxContentLines) {
while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
if (*p == '\n') {
if (!current.empty()) {
wrapped.push_back(current);
current.clear();
if (wrapped.size() >= maxContentLines)
break;
}
}
++p;
}
if (!*p || wrapped.size() >= maxContentLines)
break;
word.clear();
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r')
word += *p++;
if (word.empty())
continue;
std::string test = current.empty() ? word : (current + " " + word);
uint16_t w = display->getStringWidth(test.c_str(), test.length(), true);
if (w <= availableWidth)
current = test;
else {
if (!current.empty()) {
wrapped.push_back(current);
current = word;
if (wrapped.size() >= maxContentLines)
break;
} else {
current = word;
while (current.size() > 1 &&
display->getStringWidth(current.c_str(), current.length(), true) > availableWidth)
current.pop_back();
}
}
}
if (!current.empty() && wrapped.size() < maxContentLines)
wrapped.push_back(current);
return wrapped;
};
std::vector<std::string> allLines;
if (hasTitle)
allLines.emplace_back(popupTitle);
char buf[sizeof(popupMessage)];
strncpy(buf, popupMessage, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *paragraph = strtok(buf, "\n");
while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) {
auto wrapped = wrapText(paragraph, maxWrapWidth);
for (const auto &ln : wrapped) {
if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0))
break;
allLines.push_back(ln);
}
paragraph = strtok(nullptr, "\n");
}
std::vector<const char *> ptrs;
for (const auto &ln : allLines)
ptrs.push_back(ln.c_str());
ptrs.push_back(nullptr);
// Use the standard notification box drawing from NotificationRenderer
NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0);
}
} // namespace graphics
#endif // HAS_SCREEN

View File

@@ -0,0 +1,55 @@
#pragma once
#include "configuration.h"
#if HAS_SCREEN
#include "graphics/Screen.h" // InputEvent
#include "graphics/VirtualKeyboard.h"
#include <OLEDDisplay.h>
#include <functional>
#include <string>
namespace graphics
{
class OnScreenKeyboardModule
{
public:
static OnScreenKeyboardModule &instance();
void start(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> callback);
void stop(bool callEmptyCallback);
void handleInput(const InputEvent &event);
static bool processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *keyboard);
bool draw(OLEDDisplay *display);
void showPopup(const char *title, const char *content, uint32_t durationMs);
void clearPopup();
// Draw only the popup overlay (used when legacy virtualKeyboard draws the keyboard)
void drawPopupOverlay(OLEDDisplay *display);
private:
OnScreenKeyboardModule() = default;
~OnScreenKeyboardModule();
OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete;
OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete;
void onSubmit(const std::string &text);
void onCancel();
void drawPopup(OLEDDisplay *display);
VirtualKeyboard *keyboard = nullptr;
std::function<void(const std::string &)> callback;
char popupTitle[64] = {0};
char popupMessage[256] = {0};
uint32_t popupUntil = 0;
bool popupVisible = false;
};
} // namespace graphics
#endif // HAS_SCREEN

View File

@@ -45,8 +45,12 @@ bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
{ {
auto p = *pptr; auto p = *pptr;
// If inbound message is a replay (or spoof!) of our own messages, we shouldn't process const auto transport = mp.transport_mechanism;
// (why use second-hand sources for our own data?) if (isFromUs(&mp) && !IS_ONE_OF(transport, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL,
meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API)) {
LOG_WARN("Ignoring packet supposedly from us over external transport");
return true;
}
// FIXME this can in fact happen with packets sent from EUD (src=RX_SRC_USER) // FIXME this can in fact happen with packets sent from EUD (src=RX_SRC_USER)
// to set fixed location, EUD-GPS location or just the time (see also issue #900) // to set fixed location, EUD-GPS location or just the time (see also issue #900)
@@ -472,19 +476,53 @@ void PositionModule::sendLostAndFoundText()
delete[] message; delete[] message;
} }
// Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision
static inline void computeImpreciseLatLon(int32_t inLat, int32_t inLon, uint8_t precisionBits, int32_t &outLat, int32_t &outLon)
{
if (precisionBits > 0 && precisionBits < 32) {
// Build mask for top 'precisionBits' bits of a 32-bit unsigned field
const uint32_t mask = (precisionBits == 32) ? UINT32_MAX : (UINT32_MAX << (32 - precisionBits));
// Note: latitude_i/longitude_i are stored as signed 32-bit in meshtastic code but
// the bitmask logic used previously operated as unsigned—preserve that behavior by
// casting to uint32_t for masking, then back to int32_t.
uint32_t lat_u = static_cast<uint32_t>(inLat) & mask;
uint32_t lon_u = static_cast<uint32_t>(inLon) & mask;
// Add the "center of cell" offset used elsewhere:
// The code previously added (1 << (31 - precision)) to produce the middle of the possible location.
uint32_t center_offset = (1u << (31 - precisionBits));
lat_u += center_offset;
lon_u += center_offset;
outLat = static_cast<int32_t>(lat_u);
outLon = static_cast<int32_t>(lon_u);
} else {
// full precision: return input unchanged
outLat = inLat;
outLon = inLon;
}
}
struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition) struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition)
{ {
// The minimum distance to travel before we are able to send a new position packet.
const uint32_t distanceTravelThreshold = const uint32_t distanceTravelThreshold =
Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100); Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100);
// Determine the distance in meters between two points on the globe int32_t lastLatImprecise, lastLonImprecise;
float distanceTraveledSinceLastSend = GeoCoord::latLongToMeter( int32_t currentLatImprecise, currentLonImprecise;
lastGpsLatitude * 1e-7, lastGpsLongitude * 1e-7, currentPosition.latitude_i * 1e-7, currentPosition.longitude_i * 1e-7);
return SmartPosition{.distanceTraveled = abs(distanceTraveledSinceLastSend), computeImpreciseLatLon(lastGpsLatitude, lastGpsLongitude, precision, lastLatImprecise, lastLonImprecise);
computeImpreciseLatLon(currentPosition.latitude_i, currentPosition.longitude_i, precision, currentLatImprecise,
currentLonImprecise);
float distMeters = GeoCoord::latLongToMeter(lastLatImprecise * 1e-7, lastLonImprecise * 1e-7, currentLatImprecise * 1e-7,
currentLonImprecise * 1e-7);
float distanceTraveled = fabsf(distMeters);
return SmartPosition{.distanceTraveled = distanceTraveled,
.distanceThreshold = distanceTravelThreshold, .distanceThreshold = distanceTravelThreshold,
.hasTraveledOverThreshold = abs(distanceTraveledSinceLastSend) >= distanceTravelThreshold}; .hasTraveledOverThreshold = distanceTraveled >= distanceTravelThreshold};
} }
void PositionModule::handleNewPosition() void PositionModule::handleNewPosition()

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