Compare commits

...

310 Commits

Author SHA1 Message Date
Ben Meadors
5f9d0bb325 Merge branch 'master' into uplink-downlink-udp-controls 2025-10-02 05:51:17 -05:00
github-actions[bot]
b978c6c86c Upgrade trunk (#8183)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-10-02 05:15:36 -05:00
Jonathan Bennett
76d4807130 Add support for the manually_verified bool in SharedContact (#8180) 2025-10-01 21:07:30 -05:00
github-actions[bot]
ec28c383af Upgrade trunk (#8159)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-10-01 15:32:25 -05:00
github-actions[bot]
641a2fc63d Update protobufs (#8178)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-10-01 15:32:06 -05:00
Mike Robbins
f7469159cf Reliable ACKs for DMs (#8165)
* RoutingModule::sendAckNak takes ackWantsAck arg to set want_ack on the ACK itself

* Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo)

* Update ReliableRouter::sniffReceived to use ReliableRouter::shouldSuccessAckWithWantAck

* Use isFromUs

* Update MockRoutingModule::sendAckNak to include ackWantsAck argument (currently ignored)

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-10-01 15:31:53 -05:00
Ben Meadors
af83670376 Merge pull request #8179 from meshtastic/develop
Develop -> master
2025-10-01 15:30:57 -05:00
github-actions[bot]
849bbad279 Automated version bumps (#8177)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-10-01 15:13:28 -05:00
rcarteraz
b28d095096 missed t-rexes 2025-10-01 13:32:44 -05:00
rcarteraz
17afdb9ccf no more t-rex 2025-10-01 13:32:44 -05:00
Jonathan Bennett
d5164b4fbf Check the BUILD_EPOCH if defined 2025-10-01 13:32:44 -05:00
Jonathan Bennett
ad44940732 Put the GPIO in the right state for wake from sleep 2025-10-01 13:32:44 -05:00
Jonathan Bennett
8b466b1db3 T-rex comment cleanup 2025-10-01 13:32:44 -05:00
Jonathan Bennett
f9937967fa Add HardwareModel and correct directories 2025-10-01 13:32:44 -05:00
Jonathan Bennett
4fd568f384 Initial support for T-Rex 2025-10-01 13:32:44 -05:00
github-actions[bot]
b08e4efb78 Update protobufs (#8172)
Co-authored-by: jp-bennett <5630967+jp-bennett@users.noreply.github.com>
2025-09-30 13:34:40 -05:00
Ben Meadors
ee6857511a Fix Heltec V3 missed button presses (#8167) 2025-09-30 08:05:00 -05:00
Tom Fifield
a3e6f16378 Introduce non-linear TX_GAIN_LORA (#8107)
* Introduce non-linear TX_GAIN_LORA

Previously, our TX_GAIN_LORA setting was a single number, intended
to represent the signal gain going through a power amp (plus or minus
antenna, attenuator, and other parts of the RF chain).

It turns out the relationship between the input power (i.e. from an SX1262)
and total output power is often non-linear. While we fudged a 1dBm difference
here and there with existing chips, the Heltec v4 has a 5dBm difference in gain
depending on which end of the input power (and frequency) you are at.

To allow people to run their Heltec v4 at max power when legal, and future
proof our code, this patch introduced an optional array-based TX_GAIN_LORA.

Define NUM_PA_POINTS and set TX_GAIN_LORA to gain values for a given input
power in 1dBm increments, and all will work.

For linear systems, just continue to define TX_GAIN_LORA as a number.

Fixes https://github.com/meshtastic/firmware/issues/8070

* Remove temporary power limit on heltec v4

* Add function RadioLibInterface::checkOutputPower

* Ensure SX126x reaches minimum supported power.

* Keep it simple, instead.
2025-09-30 08:20:39 +10:00
Clive Blackledge
a1c658a467 Bug / Send upgraded (duplicate) packets to phone if the queue removal failed. (#8148)
* Add seenRecently = true if wasUpgraded is true but unable to remove from queue (i.e. already sent/processed).

* Consistent comment between FloodingRouter and HopRouter
2025-09-28 16:43:51 -05:00
Clive Blackledge
777e11bad9 Bug / Send upgraded (duplicate) packets to phone if the queue removal failed. (#8148)
* Add seenRecently = true if wasUpgraded is true but unable to remove from queue (i.e. already sent/processed).

* Consistent comment between FloodingRouter and HopRouter
2025-09-28 16:42:51 -05:00
Ben Meadors
a15d654767 Finish deprecating the Repeater role behavior (#8144)
* Finish deprecating the Repeater role behavior

* Validate

* Fixed bad if/else block

* Get your crap together!
2025-09-28 15:30:53 -05:00
Ben Meadors
033fc0c8f3 Validate CR and SF lora config (#8146)
* Validate CR and SF lora config

* No zero-bw

* Update src/modules/AdminModule.cpp

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

* Fix braces

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-28 13:13:07 -05:00
Ben Meadors
7633ddcfd1 Merge remote-tracking branch 'origin/master' into develop 2025-09-28 07:43:38 -05:00
Ben Meadors
f20ce2c29e Merge branch 'master' into uplink-downlink-udp-controls 2025-09-28 07:40:30 -05:00
github-actions[bot]
8717c60f13 Update protobufs (#8142)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-09-28 07:35:56 -05:00
Ben Meadors
24940f7735 Merge branch 'master' into uplink-downlink-udp-controls 2025-09-28 06:39:24 -05:00
Ben Meadors
87a4a6ab73 Fix isFromUs and account for channel hashes instead of indexes 2025-09-28 06:39:15 -05:00
Jason P
067939ca24 Correct altitudeLine getting clobbered in the great merge (#8138)
* Correct altitudeLine getting clobbered in the great merge

* Fix variable usage in altitude calculation
2025-09-28 06:11:01 -05:00
Ben Meadors
c1e8c28128 Revert "Revert cross-preset UDP bridging"
This reverts commit 359a4dbd68.
2025-09-27 16:25:40 -05:00
Ben Meadors
b76a22996e Merge branch 'master' into uplink-downlink-udp-controls 2025-09-27 14:44:40 -05:00
Ben Meadors
2b60bae61c Merge remote-tracking branch 'origin/develop' 2025-09-27 09:08:59 -05:00
dfsx1
bc516ebbac Remove memcpy (#8079)
Obsolete since #7652 returns false for mismatching keys

Co-authored-by: dfsx1 <dfsx1@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-09-27 08:33:07 -05:00
Ben Meadors
045176789e Fix int comparison and client_base base should really not be on this list 2025-09-27 08:32:43 -05:00
Ben Meadors
667b7c50e2 Merge pull request #8136 from plashchynski/fix_no_gps_double_message
UIRenderer: display "No GPS present" only on the first line to avoid duplication
2025-09-27 08:24:00 -05:00
Ben Meadors
64c268f055 Merge branch 'develop' into fix_no_gps_double_message 2025-09-27 08:11:00 -05:00
Ben Meadors
806bf6ce2c Merge pull request #7703 from ford-jones/clear-rangetest-results
Range-test: Clean on reboot
2025-09-27 08:08:31 -05:00
Ben Meadors
6040e49b6b Merge branch 'master' into uplink-downlink-udp-controls 2025-09-27 08:05:48 -05:00
Ford Jones
7eb0109e33 Merge branch 'develop' into clear-rangetest-results 2025-09-28 01:02:44 +12:00
Ben Meadors
73147c4028 Merge pull request #8110 from meshtastic/develop
Develop --> Master
2025-09-27 08:00:52 -05:00
Dzmitry Plashchynski
e8627b2d01 UIRenderer: display "No GPS present" only on the first line to avoid duplication 2025-09-27 15:56:52 +03:00
Ben Meadors
ab00e991f6 Revert cross-preset default-key bridging with UDP and disable UDP by default (#8130)
* Revert cross-preset UDP bridging

* Don't enable UDP by default
2025-09-27 07:09:24 -05:00
Ben Meadors
888115e0b9 Revert "Update src/mesh/udp/UdpMulticastHandler.h"
This reverts commit 2b590d2300.
2025-09-27 06:10:04 -05:00
Ben Meadors
2b590d2300 Update src/mesh/udp/UdpMulticastHandler.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-27 06:06:05 -05:00
Ben Meadors
42532d5b7c Size 2025-09-27 05:57:08 -05:00
Ben Meadors
683539220d Re-use Uplink / downlink controls for UDP 2025-09-27 05:55:04 -05:00
WillyJL
a2d86454d3 I2S: Fix silent RTTTL regression (#8129) 2025-09-27 15:07:38 +10:00
Jonathan Bennett
bc3db1b5c1 Properly output the TCXO Voltage in yaml (#8128) 2025-09-26 18:23:09 -05:00
github-actions[bot]
2f1198ddf3 Upgrade trunk (#8118)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-09-26 11:17:38 -05:00
Jason P
0624059683 Saving changes are required (#8122) 2025-09-26 11:17:15 -05:00
Jason P
9980c56d81 Correct Inverted Mute Icon on Clock Display (#8111) 2025-09-26 08:48:34 +10:00
Ben Meadors
fc9f1ac056 Merge branch 'master' into develop 2025-09-25 17:09:47 -05:00
Ben Meadors
c65dbe490e Merge pull request #8101 from Links2004/reduce_cpu_load
reduce cpu load by optimizing OSThread runOnce calls
2025-09-25 16:29:42 -05:00
Ben Meadors
44636cc9f5 Merge pull request #8053 from GUVWAF/assymRelay
Make sure next-hop is only set when they received us directly
2025-09-25 13:54:22 -05:00
GUVWAF
aa876ba42f Merge branch 'develop' into assymRelay 2025-09-25 20:01:35 +02:00
GUVWAF
12c3ddf457 Resolve comments 2025-09-25 19:59:38 +02:00
Ben Meadors
d9f0590f8e Merge branch 'develop' into reduce_cpu_load 2025-09-25 11:59:22 -05:00
Ben Meadors
191d20ed04 Merge pull request #7982 from meshtastic/develop
Test develop --> master
2025-09-25 08:34:07 -05:00
Ford Jones
8d9fda38d6 Merge branch 'develop' into clear-rangetest-results 2025-09-25 22:47:16 +12:00
Erayd
3c25652cdf If a packet is heard multiple times, rebroadcast using the highest hop limit (#5534)
* If a packet is heard multiple times, rebroadcast using the highest hop limit

Sometimes a packet will be in the TX queue waiting to be transmitted,
when it is overheard being rebroadcast by another node, with a higher
hop limit remaining. When this occurs, modify the pending packet in
the TX queue to avoid unnecessarily wasting hops.

* Reprocess instead of modifying queued packet

In order to ensure that the traceroute module works correctly, rather
than modifying the hop limnit of the existing queued version of the
packet, simply drop it ifrom the queue and reprocess the version of the
packet with the superior hop limit.

* Update protobufs submodule

* Merge upstream/develop into overheard-hoptimisation branch

Resolved conflicts in:
- src/mesh/FloodingRouter.cpp: Integrated hop limit optimization with refactored duplicate handling
- src/mesh/MeshPacketQueue.h: Kept both hop_limit_lt parameter and new find() method

* Improve method naming and code clarity

- Rename findPacket() to getPacketFromQueue() for better clarity
- Make code DRY by having find() use getPacketFromQueue() internally
- Resolves method overloading conflict with clearer naming

* If a packet is heard multiple times, rebroadcast using the highest hop limit

Sometimes a packet will be in the TX queue waiting to be transmitted,
when it is overheard being rebroadcast by another node, with a higher
hop limit remaining. When this occurs, modify the pending packet in
the TX queue to avoid unnecessarily wasting hops.

* Improve router role checking using IS_ONE_OF macro

- Replace multiple individual role checks with cleaner IS_ONE_OF macro
- Add CLIENT_BASE support as suggested in PR #7992
- Include MeshTypes.h for IS_ONE_OF macro
- Makes code more maintainable and consistent with other parts of codebase

* Apply IS_ONE_OF improvement to NextHopRouter.cpp

- Replace multiple individual role checks with cleaner IS_ONE_OF macro
- Add CLIENT_BASE support for consistency
- Include MeshTypes.h for IS_ONE_OF macro
- Matches the pattern used in FloodingRouter.cpp

* Create and apply IS_ROUTER_ROLE() macro across codebase

- Add IS_ROUTER_ROLE() macro to meshUtils.h for consistent router role checking
- Update FloodingRouter.cpp to use macro in multiple locations
- Update NextHopRouter.cpp to use macro
- Include CLIENT_BASE role support

* Core Changes:
- Add hop_limit field to PacketRecord (17B→20B due to alignment)
- Extend wasSeenRecently() with wasUpgraded parameter
- Enable router optimization without duplicate app delivery
- Handle ROUTER_LATE delayed transmission properly

Technical Details:
- Memory overhead: ~4000 bytes for 1000 records
- Prevents duplicate message delivery while enabling routing optimization
- Maintains protocol integrity for ACK/NAK handling
- Supports upgrade from hop_limit=0 to hop_limit>0 scenarios

* Delete files accdentally added for merge

* Trunk formatting

* Packets are supposed to be unsigned. Thankfully, it's only a log message.

* Upgrade all packets, not just 0 hop packets.

* Not just unsigned, but hex. Updating packet log IDs.

* Fixed order of operations issue that prevented packetrs from being removed from the queue

* Fixing some bugs after testing. Only storing the maximum hop value in PacketRecord which makes sense. Also, updating messaging to make more sense in the logs.

* Fixed flow logic about how to handle re-inserting duplicate packets. Removed IS_ROUTER_ROLE macro and replaced it with better isRebroadcaster().

* Add logic to re-run modules, but avoid re-sending to phone.

* Refactor how to process the new packet with hops. Only update nodeDB and traceRouteModule.

* - Apply changes to both FloodingRouter and NextHopRouter classes to make packets mutable for traceroute
- MESHTASTIC_EXCLUDE_TRACEROUTE guard for when we don't want traceroute

* Allow MeshPacket to be modified in-place in processUpgradePacket

* let's not make a copy where a copy is unncessary.

---------

Co-authored-by: Clive Blackledge <clive@ansible.org>
Co-authored-by: Clive Blackledge <git@ansible.org>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-09-25 05:44:49 -05:00
Ben Meadors
9b3d76967b Merge branch 'develop' into assymRelay 2025-09-25 05:18:56 -05:00
Clive Blackledge
fd5ca8b73c Feat/0-cost hops for favorite routers (#7992)
* feat: implement router hop preservation for router-to-router communication

- Preserve hop_limit when both local device and previous relay are routers/CLIENT_BASE
- Only preserve hops for favorite routers to prevent abuse
- Apply to both FloodingRouter and NextHopRouter
- Update hop counting logic in MeshService for router-to-router communication

This allows routers to communicate over longer distances without
consuming hop limits, improving mesh network efficiency for
infrastructure nodes.

* chore: update protobufs submodule to latest

* Optimized to check friend list first before nodedb.

* Reverting unintended changes

* revert: remove protobufs submodule update

This reverts the protobufs submodule back to a84657c22 to remove
unintended changes from this branch.

* Slight rewrite to remove flawed NO_RELAY_NODE logic and added logic to add isFirstHop. If isFirstHop, always decrease hop_limit to avoid retry logic.

* DRY code. Remove NodeInfo logic that was left over.

* Trunk formatting
2025-09-25 05:17:51 -05:00
Chloe Bethel
18058ef507 Fix 2.4GHz reconfiguration on LR11xx (#8102)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-09-25 04:50:56 -05:00
Ben Meadors
d9ba0633f6 Merge branch 'develop' into clear-rangetest-results 2025-09-25 04:49:44 -05:00
Ben Meadors
68fc931518 Merge branch 'master' into develop 2025-09-25 04:48:08 -05:00
github-actions[bot]
0ad6b813fc Upgrade trunk (#8105)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-09-25 04:45:14 -05:00
Ben Meadors
d41fb7bcb5 Merge branch 'develop' into reduce_cpu_load 2025-09-25 04:43:56 -05:00
Ford Jones
fef4a2987d Merge branch 'develop' into clear-rangetest-results 2025-09-25 10:42:20 +12:00
WillyJL
47a82bdb98 Fix duplicated lines from merge (#8104) 2025-09-24 16:36:14 -05:00
Ben Meadors
8ed6514771 Merge branch 'develop' into assymRelay 2025-09-24 15:17:40 -05:00
Links2004
17ecd69416 onReceive does only exist for HardwareSerial not for USB CDC serial but we can at least check for USB connection in a longer interval 2025-09-24 22:09:32 +02:00
Links2004
bb6f19dddf the BluetoothPhoneAPI runOnce is triggerd by events any way no need to loop 2025-09-24 17:14:22 +02:00
Links2004
2fdc0d0928 save CPU cycles in ExternalNotificationModule
e.g. no need for a 25ms loop when we only blink a LED at 1sec
2025-09-24 17:14:22 +02:00
Links2004
85cdcad194 only run the ButtonThread if a button is pressed 2025-09-24 17:14:22 +02:00
Links2004
0b4a28866b add optional debug logging to see which OSThread / loops have what delays 2025-09-24 17:14:22 +02:00
Links2004
91e2e3f0e8 remove OSThread from BuzzerFeedbackThread 2025-09-24 17:14:22 +02:00
Links2004
14e64d6b9e move SerialConsole to event based trigger 2025-09-24 17:14:14 +02:00
Ford Jones
d461eb35fc Merge branch 'develop' into clear-rangetest-results 2025-09-24 23:59:46 +12:00
Ben Meadors
371313080b Merge branch 'master' into develop 2025-09-24 06:18:13 -05:00
Ben Meadors
db55d8a59d Trunk 2025-09-24 06:16:51 -05:00
Quency-D
949f881ae8 Add three expansion screens for heltec mesh solar. (#7995)
* Add three expansion screens for heltec mesh solar.

* delete whitespace

Update variants/nrf52840/heltec_mesh_solar/variant.h

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

* delete whitespace

Update variants/nrf52840/heltec_mesh_solar/platformio.ini

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-09-24 06:16:51 -05:00
renovate[bot]
ca3c45a2f3 Update Adafruit BusIO to v1.17.4 (#8098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 06:15:00 -05:00
Quency-D
c33c368315 Add three expansion screens for heltec mesh solar. (#7995)
* Add three expansion screens for heltec mesh solar.

* delete whitespace

Update variants/nrf52840/heltec_mesh_solar/variant.h

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

* delete whitespace

Update variants/nrf52840/heltec_mesh_solar/platformio.ini

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-09-24 06:14:24 -05:00
github-actions[bot]
1835ff2d78 Upgrade trunk (#8094)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-09-24 06:05:36 -05:00
Ben Meadors
8e04f9f631 Merge branch 'master' into develop 2025-09-24 06:03:14 -05:00
github-actions[bot]
83be632a1a Automated version bumps (#8100)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-09-24 06:02:55 -05:00
github-actions[bot]
1ed7aad976 Automated version bumps (#8100)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-09-24 06:01:31 -05:00
Ben Meadors
94d4bdf05c Revert "Fix build errors (#8067)"
This reverts commit d998f70b56.
2025-09-23 08:57:04 -05:00
Ben Meadors
1968a009dd Clear lasttoradio on BLE disconnect (#8095)
* On disconnect, clear the lastToRadio buffer

* Move it, bucko!
2025-09-23 07:31:25 -05:00
Ben Meadors
8e608e8186 Heltec V4 is 16mb 2025-09-23 06:04:47 -05:00
Jason P
d998f70b56 Fix build errors (#8067) 2025-09-23 05:39:57 -05:00
Ben Meadors
f55db903b2 Merge branch 'master' into develop 2025-09-23 05:38:52 -05:00
Jonathan Bennett
91efaba389 Remove line from BLE pin screen, to make pin readible on tiny screens 2025-09-22 21:59:00 -05:00
Jonathan Bennett
a8c66547cc Also pull a deviceID from esp32c6 devices (#8092) 2025-09-22 21:46:57 -05:00
Ben Meadors
f77ca2533b Try-fix: Unstick that PhoneAPI state (#8091)
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2025-09-22 21:46:35 -05:00
Jonathan Bennett
07b58a82d5 tlora-pager wake on button, and kb backlight toggling (#8090) 2025-09-22 21:06:23 -05:00
Ben Meadors
e1485b530f Handle ext. notification module things even if not enabled (#8089) 2025-09-22 19:59:05 -05:00
Ford Jones
a4e09aa9da Merge branch 'develop' into clear-rangetest-results 2025-09-23 10:31:09 +12:00
Jonathan Bennett
db941bff3b portduino bump to fix gpiod bug (#8083)
An earlier portduino causes problems with initializing gpiod lines. This pulls in the fix.
2025-09-22 12:00:01 -05:00
github-actions[bot]
13e1f99c7e Upgrade trunk (#8078)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-09-22 05:58:47 -05:00
Tom Fifield
388c821028 Allow label enforcement job to run on self-hosted runners (#7909)
Previously, this check would only run on github-provided runners.
2025-09-22 10:23:42 +10:00
Jason P
3d51287ba7 Introduce Radio Preset elections through BaseUI (#8071) 2025-09-21 17:54:54 -05:00
Ben Meadors
1e1f2a69b7 Merge branch 'master' into develop 2025-09-21 16:52:20 -05:00
Jason P
b3df32c6c5 Fix build errors (#8067) 2025-09-21 14:04:17 -05:00
Quency-D
cea9e1238b Add heltec_v4 board. (#7845)
* add heltec_v4 board.

* Update variants/esp32s3/heltec_v4/platformio.ini

Co-authored-by: Austin <vidplace7@gmail.com>

* Limit the maximum output power.

* Trunk fixes

Fixes formatting to match meshtastic trunk linter.

* 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>

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Austin <vidplace7@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-21 07:13:28 -05:00
Quency-D
11eb4a5b90 Add heltec_v4 board. (#7845)
* add heltec_v4 board.

* Update variants/esp32s3/heltec_v4/platformio.ini

Co-authored-by: Austin <vidplace7@gmail.com>

* Limit the maximum output power.

* Trunk fixes

Fixes formatting to match meshtastic trunk linter.

* 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>

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Austin <vidplace7@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-21 07:03:44 -05:00
Tom
d1fd102952 Add another seeed_xiao_nrf52840_kit build environment for I2C pinout (#8036)
* Update platformio.ini

* Remove some more extraneous lines
2025-09-21 06:28:28 -05:00
Markus
27b07cd1c5 Fix Rotary Encoder Button (#8001)
this fixes the Rotary Encoder Button, currenlty its not working at all.
Currently the action `ROTARY_ACTION_PRESSED` is only triggerd with a IRQ on RISING, which results in nothing since the function detects the "not longer" pressed button --> no action.

the `ROTARY_ACTION_PRESSED` implementation needs to be called on both edges (on press and release of the button)

changing the interupt setting to `CHANGE` fixes the problem.
2025-09-21 06:28:05 -05:00
Tom
5701755608 Add another seeed_xiao_nrf52840_kit build environment for I2C pinout (#8036)
* Update platformio.ini

* Remove some more extraneous lines
2025-09-21 06:27:39 -05:00
Ben Meadors
d09baddce5 Merge branch 'develop' into assymRelay 2025-09-21 06:27:20 -05:00
renovate[bot]
c42513d7c8 Update RadioLib to v7.3.0 (#8065)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 06:25:32 -05:00
Markus
2010871e4b Fix Rotary Encoder Button (#8001)
this fixes the Rotary Encoder Button, currenlty its not working at all.
Currently the action `ROTARY_ACTION_PRESSED` is only triggerd with a IRQ on RISING, which results in nothing since the function detects the "not longer" pressed button --> no action.

the `ROTARY_ACTION_PRESSED` implementation needs to be called on both edges (on press and release of the button)

changing the interupt setting to `CHANGE` fixes the problem.
2025-09-21 06:22:29 -05:00
Jason P
040b3b8c7f Resolve many warnings for BaseUI during builds (#8063)
* Resolve many warnings for BaseUI during builds

* Don't display "No GPS Lock" twice
2025-09-21 11:33:47 +10:00
Ben Meadors
b49496d99d Merge branch 'master' into develop 2025-09-20 12:18:55 -05:00
GUVWAF
34c2191f63 Use lora.use_preset config to get name (#8057) 2025-09-20 12:17:49 -05:00
GUVWAF
52527e281d Use lora.use_preset config to get name (#8057) 2025-09-20 12:17:14 -05:00
Markus
9b6cf53730 move HTTP contentTypes to Flash - saves 768 Bytes of RAM (#8055) 2025-09-20 12:16:42 -05:00
Markus
6a3b2ceafe move HTTP contentTypes to Flash - saves 768 Bytes of RAM (#8055) 2025-09-20 12:15:41 -05:00
ford-jones
3463006f73 Merge branch 'develop' of https://github.com/meshtastic/firmware into clear-rangetest-results 2025-09-21 00:26:19 +12:00
WillyJL
db2f79b6c4 Fix last build issues on develop (#8046) 2025-09-20 07:04:27 -05:00
GUVWAF
1d3c47c5fa Make sure to ACK ACKs/replies if next-hop routing is used (#8052)
* Make sure to ACK ACKs/replies if next-hop routing is used
To stop their retransmissions; hop limit of 0 is enough

* Update src/mesh/ReliableRouter.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-09-20 06:37:40 -05:00
Markus
44968415a5 fix build with HAS_TELEMETRY 0 (#8051) 2025-09-20 06:34:47 -05:00
Markus
8db9b24934 fix build with HAS_TELEMETRY 0 (#8051) 2025-09-20 06:33:41 -05:00
GUVWAF
1fc07607cb Make sure next-hop is only set when they received us directly 2025-09-20 13:03:46 +02:00
ford-jones
22b71a1e95 Pull latest changes from https://github.com/meshtastic/protobufs.git 2025-09-20 17:52:41 +12:00
ford-jones
2ccf91f443 Regen protos 2025-09-20 17:21:26 +12:00
ford-jones
58e4dcea61 Merge branch 'develop' of https://github.com/meshtastic/firmware into clear-rangetest-results 2025-09-20 14:31:05 +12:00
Jonathan Bennett
6f56ccd283 C6l fixes (#8047) 2025-09-19 21:16:19 -05:00
Ben Meadors
cc3ff1504a Merge branch 'master' into develop 2025-09-19 16:11:07 -05:00
Ben Meadors
9b6a7ed3bb Fix icon 2025-09-19 16:00:24 -05:00
github-actions[bot]
fdc8796052 Update protobufs (#8045)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-09-19 15:50:33 -05:00
WillyJL
787642ad4c Fix more build failures (#8044) 2025-09-19 15:17:31 -05:00
Ben Meadors
6677255f6c Fix 2025-09-19 12:09:56 -05:00
WillyJL
3a63a56cff Fix build fail on develop branch (#8043) 2025-09-19 12:00:59 -05:00
Ben Meadors
edb250e782 Merge branch 'master' into develop 2025-09-19 11:35:46 -05:00
Jason P
f32e06a321 Update Protobuf usage, add MLS, fix clock (#8041) 2025-09-19 10:51:07 -05:00
Austin
8095261dfd PPA: Enable Ubuntu 25.10 (questing) (#7940) 2025-09-19 10:18:08 -05:00
Tom
72b9a02f3e (resubmission) Manual GitHub actions to allow building one target or arch (#7997)
* Reset the modified files

* Fix some changes

* Fix some changes

* Trunk. That is all.

---------

Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com>
2025-09-19 10:16:46 -05:00
Quency-D
af26408d73 Add a new GPS model CM121. (#7852)
* Add a new GPS model CM121.

* Add CM121 to Unicore.

* Trunk fixes, remove unneded NMEA lines

---------

Co-authored-by: Tom Fifield <tom@tomfifield.net>
2025-09-19 10:05:41 -05:00
Jason P
c8f69913d6 Add formatting and menu picking for other GPS format options (#7974)
* Add back options for other GPS format options

* Rename variables and don't overlap elements

* Fix default value

* Should probably add a menu while I'm here!

* Shorten names just a bit to fit on screens

* Fix off by one

* Labels try to make things better

* Missed a label
2025-09-19 09:56:04 -05:00
github-actions[bot]
42fbb62f18 Update protobufs (#8038)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-09-19 08:47:53 -05:00
Jason P
e6adb197e4 Add formatting and menu picking for other GPS format options (#7974)
* Add back options for other GPS format options

* Rename variables and don't overlap elements

* Fix default value

* Should probably add a menu while I'm here!

* Shorten names just a bit to fit on screens

* Fix off by one

* Labels try to make things better

* Missed a label
2025-09-19 08:47:31 -05:00
Ben Meadors
cfb34a8816 Merge branch 'master' into develop 2025-09-19 08:46:22 -05:00
Jason P
8264d4d65e BaseUI Updates (#7787)
* Account for low resolution wide screen OLEDs

* Allow picking of Device Role and new Display Formatter for Device Role

* Add remainder of client roles to display formatter

* Don't update the role unless you pick a value

* Mascots are fun

* Fix warnings during compile time

* Improve some menus

* Mascots need to work everywhere

* Update Chirpy image

* Fix Trunk

* Update protobufs

* Add date to Clock screen

* Analog clocks love dates too

* Finalize date moves for analog clock
2025-09-19 08:44:14 -05:00
Ben Meadors
c11680fcc0 Fix formatting and trunk issues 2025-09-19 08:37:58 -05:00
Ben Meadors
479c1f5346 Merge branch 'master' into develop 2025-09-19 08:35:23 -05:00
Jason P
a1cf305336 Show GPS Date properly in drawCommonHeader (#7887)
* Commit good code that is sustainable

* Fix new build errors
2025-09-19 08:33:37 -05:00
Trent V.
7b2ff7e196 updated shebang to use a more standard path for bash (#7922)
Signed-off-by: Trenton VanderWert <trenton.vanderwert@gmail.com>
2025-09-19 08:30:01 -05:00
Ben Meadors
b1d314db1e Merge branch 'master' into develop 2025-09-19 08:27:24 -05:00
Jonathan Bennett
cc579dd0bd Portduino config refactor (#7796)
* Start portduino_config refactor

* refactor GPIOs to new portduino_config

* More portduino_config work

* More conversion to portduino_config

* Finish portduino_config transition

* trunk

* yaml output work

* Simplify the GPIO config

* Trunk
2025-09-19 08:24:35 -05:00
Ben Meadors
1ac2382d7c Revert "Fix excluded modules configuration handling (#7838)"
This reverts commit 9c6544ebfa.
2025-09-19 07:29:54 -05:00
Ben Meadors
2ef5b968f9 Merge branch 'master' into develop 2025-09-19 07:25:54 -05:00
Ben Meadors
6a92358b68 Fix 2025-09-19 07:22:23 -05:00
HarukiToreda
e20a91b945 Added Last Coordinate counter to Position screen (#7865)
Adding a counter to show the last time a GPS coordinate was detected to ensure the user is aware how long since the coordinate updated or to identify any errors.
2025-09-19 07:00:44 -05:00
Jason P
3fbe7fd8b2 BaseUI Updates (#7787)
* Account for low resolution wide screen OLEDs

* Allow picking of Device Role and new Display Formatter for Device Role

* Add remainder of client roles to display formatter

* Don't update the role unless you pick a value

* Mascots are fun

* Fix warnings during compile time

* Improve some menus

* Mascots need to work everywhere

* Update Chirpy image

* Fix Trunk

* Update protobufs

* Add date to Clock screen

* Analog clocks love dates too

* Finalize date moves for analog clock
2025-09-19 06:59:33 -05:00
Ben Meadors
8841c1540d Merge branch 'master' into develop 2025-09-19 06:46:13 -05:00
Ben Meadors
e2ce369782 Fixes 2025-09-19 06:29:18 -05:00
ford-jones
901bcc24ee Reflect requirement of ESP32 hardware in rangetest logs 2025-09-19 22:17:03 +12:00
Ben Meadors
b14e5770d5 Merge pull request #7873 from compumike/compumike/client-base-role
Add `CLIENT_BASE` role: `ROUTER` for favorites, `CLIENT` otherwise (for attic/roof nodes!)
2025-09-18 20:40:39 -05:00
Jonathan Bennett
68ba3b315c Auto-favorite remote admin node 2025-09-18 20:37:56 -05:00
Michael
2bafac242e Feature: Seamless Cross-Preset Communication via UDP Multicast Bridging (#7753)
* Added compatibility between nodes on different Presets through `Mesh via UDP`

* Optimize multicast handling and channel mapping

- FloodingRouter: remove redundant UDP-encrypted rebroadcast suppression.
- Router: guard multicast fallback with HAS_UDP_MULTICAST and map fallback-decoded packets
  to the local default channel via isDefaultChannel()
- UdpMulticastHandler: set transport_mechanism only after successful decode

* trunk fmt

* Move setting transport mechanism.

---------

Co-authored-by: GUVWAF <thijs@havinga.eu>
2025-09-18 20:37:05 -05:00
Ben Meadors
39648e609a Merge pull request #8004 from compumike/compumike/debug-heap-add-free-heap-debugging-to-all-log-lines
When `DEBUG_HEAP` is defined, add free heap bytes to every log line in `RedirectablePrint::log_to_serial`
2025-09-18 20:36:44 -05:00
HarukiToreda
dcd53eb7cb Phone GPS display on Position Screen for BaseUI (#7875)
* Phone GPS display on Position Screen

This is a PR to show when a phone shares GPS location with the node so you can reliably know what coordinate is being shared with the Mesh.
2025-09-18 20:29:46 -05:00
renovate[bot]
7821919fae Update actions/setup-python action to v6 (#8033)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 20:12:28 -05:00
renovate[bot]
f083864f1f Update actions/download-artifact action to v5 (#8032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 19:56:57 -05:00
renovate[bot]
8e1da8561e Update actions/checkout action to v5 (#8031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 19:56:47 -05:00
Jason P
953fcca304 BaseUI Show/Hide Frame Functionality (#7382)
* Rename System Frame (from Memory) in code base

* Create menu options to Show/Hide frames: Node Lists, Bearings, Position, LoRa, Clock and Favorites frames

* Move Region Picker into submenu

* Tweak wording for Send Position vs Node Info if the device has GPS
2025-09-18 19:55:08 -05:00
Ben Meadors
20bd237ff6 Merge branch 'master' into develop 2025-09-18 19:51:26 -05:00
Tom
c73fe85ec8 (resubmission) Manual GitHub actions to allow building one target or arch (#7997)
* Reset the modified files

* Fix some changes

* Fix some changes

* Trunk. That is all.

---------

Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com>
2025-09-18 19:51:14 -05:00
Ben Meadors
b13d023d58 Merge branch 'master' into develop 2025-09-18 19:50:16 -05:00
WillyJL
9345bdcb22 T-Lora Pager: Support LR1121 and SX1280 models (#7956)
* T-Lora Pager: Support LR1121 and SX1280 models

* Remove ifdefs
2025-09-18 19:49:28 -05:00
Ben Meadors
902405a985 Extra endif 2025-09-18 19:47:16 -05:00
Markus
ec29100a88 Allow Left / Right Events for selection and improve encoder responsives (#8016)
* Allow Left / Right Events for selection and improve encoder responsives

* add define for ROTARY_DELAY
2025-09-18 19:29:09 -05:00
Ben Meadors
017d07e108 Extra chirpy 2025-09-18 19:28:10 -05:00
Markus
89cccdbbe2 Allow Left / Right Events for selection and improve encoder responsives (#8016)
* Allow Left / Right Events for selection and improve encoder responsives

* add define for ROTARY_DELAY
2025-09-18 19:25:58 -05:00
Ben Meadors
8f0e17a653 Merge branch 'master' into develop 2025-09-18 19:18:53 -05:00
github-actions[bot]
e3772858b3 Automated version bumps (#8028)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-09-18 18:20:21 -05:00
github-actions[bot]
c71c1f2d15 Automated version bumps (#8028)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-09-18 18:17:14 -05:00
Markus
2567c03a3f Fix init for InputEvent (#8015) 2025-09-18 18:15:50 -05:00
Markus
d8381aa905 Fix init for InputEvent (#8015) 2025-09-18 06:32:56 -05:00
renovate[bot]
188283b382 Update actions/download-artifact action to v5 (#8021)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 05:56:05 -05:00
github-actions[bot]
953fdc9eed Upgrade trunk (#8025)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-09-18 05:55:44 -05:00
renovate[bot]
ec7415b3fd Update actions/setup-python action to v6 (#8023)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 05:55:24 -05:00
renovate[bot]
a70ffae82c Update actions/checkout action to v5 (#8020)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 05:51:22 -05:00
renovate[bot]
6a8732bbaa Update Adafruit BusIO to v1.17.3 (#8018)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 05:51:02 -05:00
Jonathan Bennett
ba18467bd1 Auto-favorite remote admin node 2025-09-17 08:37:51 -05:00
Ben Meadors
46317f413a Merge pull request #8004 from compumike/compumike/debug-heap-add-free-heap-debugging-to-all-log-lines
When `DEBUG_HEAP` is defined, add free heap bytes to every log line in `RedirectablePrint::log_to_serial`
2025-09-16 13:11:18 -05:00
Ben Meadors
f16aa730d3 Merge pull request #8006 from meshtastic/master
Backmerge
2025-09-16 07:21:31 -05:00
Tom
22fcd102a0 (resubmission) Manual GitHub actions to allow building one target or arch (#7997)
* Reset the modified files

* Fix some changes

* Fix some changes

* Trunk. That is all.

---------

Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com>
2025-09-16 06:41:22 -05:00
Ben Meadors
d31e3839fb Use long name 2025-09-16 06:11:29 -05:00
Michael
b9d53d667e Feature: Seamless Cross-Preset Communication via UDP Multicast Bridging (#7753)
* Added compatibility between nodes on different Presets through `Mesh via UDP`

* Optimize multicast handling and channel mapping

- FloodingRouter: remove redundant UDP-encrypted rebroadcast suppression.
- Router: guard multicast fallback with HAS_UDP_MULTICAST and map fallback-decoded packets
  to the local default channel via isDefaultChannel()
- UdpMulticastHandler: set transport_mechanism only after successful decode

* trunk fmt

* Move setting transport mechanism.

---------

Co-authored-by: GUVWAF <thijs@havinga.eu>
2025-09-15 19:29:47 -05:00
Mike Robbins
5d3c92f1a2 When DEBUG_HEAP is defined, add free heap bytes to every log line in RedirectablePrint::log_to_serial 2025-09-15 12:50:38 -07:00
Ben Meadors
09de0e3edb Merge branch 'master' into develop 2025-09-14 08:15:25 -05:00
Mike Robbins
00772996b6 Fix GPS gm_mktime memory leak (#7981) 2025-09-14 05:05:06 -05:00
Jason P
de3a65579d Add formatting and menu picking for other GPS format options (#7974)
* Add back options for other GPS format options

* Rename variables and don't overlap elements

* Fix default value

* Should probably add a menu while I'm here!

* Shorten names just a bit to fit on screens

* Fix off by one

* Labels try to make things better

* Missed a label
2025-09-13 16:06:36 -04:00
Trent V.
90ddbf6f2c updated shebang to use a more standard path for bash (#7922)
Signed-off-by: Trenton VanderWert <trenton.vanderwert@gmail.com>
2025-09-13 11:56:23 -05:00
WillyJL
566c2c3fdf T-Lora Pager: Support LR1121 and SX1280 models (#7956)
* T-Lora Pager: Support LR1121 and SX1280 models

* Remove ifdefs
2025-09-13 06:50:02 -05:00
Ben Meadors
e6bfc4a97a Merge pull request #7969 from meshtastic/master
Backmerge
2025-09-12 18:23:40 -05:00
Ben Meadors
a297d21707 Merge pull request #7964 from compumike/compumike/fix-nimble-bluetooth-memory-leak
Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap
2025-09-12 17:12:27 -05:00
Ben Meadors
a8cf4dfe2d Merge pull request #7965 from compumike/compumike/fix-nrf52-bluetooth-memory-leak
Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap
2025-09-12 17:12:18 -05:00
Mike Robbins
ead67446a3 Fix memory leak in NRF52Bluetooth: allocate BluetoothStatus on stack, not heap 2025-09-12 13:15:52 -07:00
Mike Robbins
43cf12edfb Fix memory leak in NimbleBluetooth: allocate BluetoothStatus on stack, not heap 2025-09-12 13:00:17 -07:00
Ben Meadors
106a052950 Merge pull request #7873 from compumike/compumike/client-base-role
Add `CLIENT_BASE` role: `ROUTER` for favorites, `CLIENT` otherwise (for attic/roof nodes!)
2025-09-12 13:11:53 -05:00
Mike Robbins
0fc33c352a Fix memory leak in NextHopRouter: always free packet copy when removing from pending 2025-09-12 10:40:13 -07:00
Mike Robbins
35340fc6e2 NextHopRouter::roleAllowsCancelingFromTxQueue (same logic as FloodingRouter::roleAllowsCancelingDupe) 2025-09-11 21:31:42 -07:00
Mike Robbins
4ab125bbf7 src/graphics/Screen.cpp: move #include "meshUtils.h" outside of "#ifdef HAS_SCREEN" so IS_ONE_OF works on all devices 2025-09-11 21:31:42 -07:00
Mike Robbins
87eff2c4a9 Fix logic in Screen::shouldWakeOnReceivedMessage and add CLIENT_HIDDEN and CLIENT_BASE to be treated the same as CLIENT and CLIENT_MUTE 2025-09-11 21:31:42 -07:00
Mike Robbins
527e88ca46 Add CLIENT_BASE to DisplayFormatters::getDeviceRole 2025-09-11 21:31:42 -07:00
Mike Robbins
4140ecfb49 Bring src/mesh/generated/meshtastic/config.pb.h from develop after rebase 2025-09-11 21:31:42 -07:00
Mike Robbins
27cdd464d1 getTxDelayMsecWeighted and startTransmitTimerRebroadcast: extract p->rxSnr 2025-09-11 21:31:42 -07:00
Mike Robbins
5a463373f2 Remove changes to src/mesh/generated/meshtastic/config.pb.h from this PR 2025-09-11 21:31:42 -07:00
Mike Robbins
b768860866 NodeDB::isFromOrToFavoritedNode: skip search for NODENUM_BROADCAST; one-pass search and early exit 2025-09-11 21:31:42 -07:00
Mike Robbins
c63102a312 Swap expression order to allow short-circuit evaluation 2025-09-11 21:31:42 -07:00
Mike Robbins
b1f55ef6e8 Fix linter 2025-09-11 21:31:42 -07:00
Mike Robbins
b305acf7e5 Add FloodingRouter::roleAllowsCancelingDupe and condition for CLIENT_BASE 2025-09-11 21:31:42 -07:00
Mike Robbins
ab5332950c Add RadioInterface::shouldRebroadcastEarlyLikeRouter and add CLIENT_BASE condition 2025-09-11 21:31:42 -07:00
Mike Robbins
484b4cd848 Add NodeDB::isFavorite, NodeDB::isFromOrToFavoritedNode 2025-09-11 21:31:42 -07:00
Mike Robbins
3cc2b70e4f Pass meshtastic_MeshPacket down into startTransmitTimerRebroadcast and getTxDelayMsecWeighted 2025-09-11 21:31:42 -07:00
Mike Robbins
7e00054fd7 Rename startTransmitTimerSNR to startTransmitTimerRebroadcast 2025-09-11 21:31:42 -07:00
Ben Meadors
83ae72cbb2 Merge pull request #7961 from meshtastic/master
Backmerge
2025-09-11 08:14:46 -05:00
Austin
6f7149e9a2 PPA: Enable Ubuntu 25.10 (questing) (#7940) 2025-09-10 07:01:04 +10:00
Austin
95dc61f57b Debian: Correctly generate changelog entries (#7945) 2025-09-10 06:59:43 +10:00
Austin
0aa48c9c22 Use sh in debhelper scripts (#7941) 2025-09-10 06:57:36 +10:00
Ben Meadors
088318512a Duplicate 2025-09-09 11:20:27 -05:00
Ben Meadors
ca4b98f2b1 Merge branch 'master' into develop 2025-09-09 08:42:29 -05:00
Ben Meadors
1643249db7 Revert "Remove board_level from Meshtiny. (#7933)" (#7935)
This reverts commit 2191fe465c.
2025-09-09 05:48:05 -05:00
Wilson
2191fe465c Remove board_level from Meshtiny. (#7933) 2025-09-09 14:20:24 +08:00
ford-jones
b75e8913e0 Fix: Compile latest protobufs 2025-09-09 13:14:20 +12:00
Ford Jones
87a1449f76 Merge branch 'develop' into clear-rangetest-results 2025-09-09 12:05:00 +12:00
Ben Meadors
569a911455 Merge pull request #7915 from meshtastic/master
Master backmerge
2025-09-08 05:58:00 -05:00
Tom Fifield
c5b95f5a4b Disable web server on Picomputer (#7907)
Meshtastic no longer fits on the flash of the Picomputer.

Since this is a handheld, portable device, it's unlikely that people are
connecting to it via the webserver. So, disable the webserver and it fits
again:

```
Checking size .pio/build/picomputer-s3-tft/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [===       ]  32.4% (used 106056 bytes from 327680 bytes)
Flash: [==========]  99.1% (used 3313913 bytes from 3342336 bytes)
```

Fixes: https://github.com/meshtastic/firmware/issues/7906
2025-09-08 05:56:19 -05:00
Tom Fifield
15f4aebcd5 Fix build error in rak_wismesh_tap_v2 (#7905)
In the logs was:
"No screen resolution defined in build_flags. Please define DISPLAY_SIZE."

set according to similar devices.
2025-09-08 05:54:08 -05:00
Tom Fifield
2354c52b16 Only log good times. (It's not always a good time then) (#7904)
Further to https://github.com/meshtastic/firmware/pull/7897 ,
there was another log line that was triggering indiscriminantly on
GPS_INTERVAL_THRESHOLD .

Rather than logging a bad time 4000 times, let's just log one good time
when it is set.
2025-09-08 05:53:49 -05:00
Tom Fifield
227d0fa7dc Merge pull request #7862 from meshtastic/master
Backmerge from Master into develop
2025-09-08 11:23:22 +10:00
Tom Fifield
7c1eff54fb Update protobufs (#7901)
* Update protobufs

* Update protobufs (#7831)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-09-08 11:05:19 +10:00
Tom Fifield
c92fa6aa8a chore(deps): update meshtastic/device-ui digest to a04bc94 (#7857) (#7900)
* chore(deps): update meshtastic/device-ui digest to a04bc94 (#7857)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Fix INA3221 higher current wrong readings (#7607)

* chore(deps): update meshtastic/device-ui digest to 10f0244 (#7840)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* use branch of ina3221 library with fixes

* using commit hash instead of branch name

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Marco Veneziano <macvenez@gmail.com>
2025-09-08 10:31:33 +10:00
Tom Fifield
77acbc6814 Add EPOCH_BUILD to latest setup step. (#7894)
Previously this was in setup-base. However, setup-base is no longer
used by the setup job.

Fixes https://github.com/meshtastic/gh-action-firmware/issues/10
Fixes https://github.com/meshtastic/firmware/issues/7888
2025-09-07 19:29:40 -05:00
Tom Fifield
81cb1e427f Guard bad time warning logs using GPS_DEBUG (#7897)
In 2.7.7 / 2.7.8 we introduced some new checks for time accuracy.

In combination, these result in a spamming of the logs when a bad time is found

When the GPS is active, we're calling the GPS thread every 0.2secs.

So this log could be printed 4,500 times in a no-lock scenario :)

Reserve this experience for developers using GPS_DEBUG.

Fixes https://github.com/meshtastic/firmware/issues/7896
2025-09-07 19:29:26 -05:00
Tom Fifield
f6ba9604a7 Trunk fix (#7898) 2025-09-08 09:46:26 +10:00
Dmitry Dubinin
9c6544ebfa Fix excluded modules configuration handling (#7838)
* Fix excluded modules configuration handling

- Add excluded_modules flags in getDeviceMetadata() for MQTT, PAXCOUNTER, STOREFORWARD, RANGETEST, NEIGHBORINFO
- Add conditional compilation guards in AdminModule for RANGETEST, AUDIO, PAXCOUNTER, STOREFORWARD, EXTNOTIF, DETECTIONSENSOR, AMBIENTLIGHTING
- Add skip logic in PhoneAPI for excluded modules during config enumeration
- Add conditional has_* flags in NodeDB only for included modules

Fixes issue where excluded modules still appeared in client applications and sometimes caused PAYLOADVARIANT_NOT_SET errors.

* Fix excluded modules issues and refactor code

- Restore original PAXCOUNTER logic: only exclude on non-ESP32 platforms due to memory constraints
- Fix has_store_forward flag to be conditionally compiled based on MESHTASTIC_EXCLUDE_STOREFORWARD
- Refactor PhoneAPI module config skipping logic to use helper function skipExcludedModuleConfig()
- Reduce code duplication in PhoneAPI by extracting common skip logic

This addresses the three issues identified in the code review:
1. PAXCOUNTER memory impact on non-ESP32 devices
2. Unconditional has_store_forward flag setting
3. Duplicated state management logic across multiple #else blocks

* Fix ambient lighting module exclusion in PhoneAPI and AdminModule

- Add conditional compilation guards for ambient lighting in PhoneAPI.cpp
- Replace old HAS_RGB_LED logic with MESHTASTIC_EXCLUDE_AMBIENTLIGHTING check in AdminModule.cpp
- Ensure ambient lighting module is properly excluded when MESHTASTIC_EXCLUDE_AMBIENTLIGHTING=1
2025-09-08 07:15:27 +10:00
Jason P
b6eeccadeb Show GPS Date properly in drawCommonHeader (#7887)
* Commit good code that is sustainable

* Fix new build errors
2025-09-07 15:34:07 -04:00
HarukiToreda
e7b7479589 Reverting changes made by PR #7520 and adjusting ADC (#7878)
* ADC value adjustment for T114
2025-09-06 19:14:26 +10:00
Jason P
e1634076f2 Fix date display to be upper right bound (#7876) 2025-09-05 22:21:33 -05:00
HarukiToreda
d6df664102 Phone GPS display on Position Screen for BaseUI (#7875)
* Phone GPS display on Position Screen

This is a PR to show when a phone shares GPS location with the node so you can reliably know what coordinate is being shared with the Mesh.
2025-09-05 22:06:58 -05:00
Jason P
50a5b36498 BaseUI Updates (#7787)
* Account for low resolution wide screen OLEDs

* Allow picking of Device Role and new Display Formatter for Device Role

* Add remainder of client roles to display formatter

* Don't update the role unless you pick a value

* Mascots are fun

* Fix warnings during compile time

* Improve some menus

* Mascots need to work everywhere

* Update Chirpy image

* Fix Trunk

* Update protobufs

* Add date to Clock screen

* Analog clocks love dates too

* Finalize date moves for analog clock
2025-09-05 21:44:32 -04:00
GUVWAF
4d6fe936ae Only stop retransmissions when receiving implicit ACK over LoRa (#7872)
* Only stop retransmissions when receiving implicit ACK over LoRa

* trunk fmt
2025-09-05 11:01:25 -05:00
Quency-D
f825e61b89 Add a new GPS model CM121. (#7852)
* Add a new GPS model CM121.

* Add CM121 to Unicore.

* Trunk fixes, remove unneded NMEA lines

---------

Co-authored-by: Tom Fifield <tom@tomfifield.net>
2025-09-05 19:29:53 +10:00
HarukiToreda
64cd62d6af Added Last Coordinate counter to Position screen (#7865)
Adding a counter to show the last time a GPS coordinate was detected to ensure the user is aware how long since the coordinate updated or to identify any errors.
2025-09-04 22:33:02 -05:00
Sam Duffield
f31fd34ce0 Add support for the Challenger rp2040 lora (#7826)
* Firmware Built... awaiting parts for test

* Add board_level key/value as per suggestion from vidplace7

* Trunk formatting applied
2025-09-04 06:49:47 -05:00
TN
26bcc9627d merge create_test_packet duplicate usage into a shared function (#7752) 2025-09-04 06:26:04 -05:00
Jonathan Bennett
cc37535b2d Enable bmx160 on native (#7844) 2025-09-04 06:16:38 -05:00
github-actions[bot]
ced334d13b Automated version bumps (#7843)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-09-04 06:14:47 -05:00
Marco Veneziano
521fbc44b4 Fix INA3221 higher current wrong readings (#7607)
* chore(deps): update meshtastic/device-ui digest to 10f0244 (#7840)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* use branch of ina3221 library with fixes

* using commit hash instead of branch name

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 16:31:16 +10:00
Tom Fifield
361771c9bb chore(deps): update meshtastic/device-ui digest to 10f0244 (#7840) (#7851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 16:28:53 +10:00
Davide Cavalca
fa45660b7d Add TSL2561 sensor (#7675)
* Add TSL2561 sensor

* Update platformio.ini

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

* Update src/modules/Telemetry/Sensor/TSL2561Sensor.cpp

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

* Update protobufs

* Clarify magic number in TSL2561Sensor.h

* Use the correct version for Adafruit TSL2561

* Lint fixes

* Fix typo

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
2025-09-04 16:25:45 +10:00
Chloe Bethel
2e8f4ad6af Add RF switch settings for STM32WL variants (#7813)
* Add RF switch settings for STM32WL variants

* Shuffle ifdefs in STM32WLE5JCInterface to make it not get built by other targets
2025-09-04 15:12:47 +10:00
Tom Fifield
18550ea80c chore(deps): update meshtastic/device-ui digest to 10f0244 (#7840) (#7847)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 14:17:21 +10:00
Jonathan Bennett
1c1c0cc791 Portduino config refactor (#7796)
* Start portduino_config refactor

* refactor GPIOs to new portduino_config

* More portduino_config work

* More conversion to portduino_config

* Finish portduino_config transition

* trunk

* yaml output work

* Simplify the GPIO config

* Trunk
2025-09-03 17:50:26 -05:00
Ben Meadors
789c1ab59d Merge branch 'master' into develop 2025-09-03 07:02:05 -05:00
Daniel.Cao
5850a7cd6b Add RAK WisMesh Tap V2 (ESP32S3) Hardware Variant (#7741)
* Add initial variant and platformio configuration for RAK WISMESHTAP V2

* Add initial variant and platformio configuration for rak wismesh tap v2

* Remove unnecessary Meshtastic build flags from rak_wismesh_tap_v2 configuration

* Enable LGFX button support in rak_wismesh_tap_v2 configuration

* Revert "Enable LGFX button support in rak_wismesh_tap_v2 configuration"

This reverts commit 2bd2c1a03b.

---------

Co-authored-by: Daniel.Cao <daniel.cao@rakwireless.com>
2025-09-03 06:20:19 -05:00
Jonathan Bennett
6c89ea7cee chore(deps): update platform-native digest to c490bcd (#7814) (#7832)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 06:16:00 -05:00
ford-jones
c62f262f63 Trunk fmt 2025-09-03 13:38:39 +12:00
ford-jones
798040b5b8 Merge branch 'develop' of https://github.com/meshtastic/firmware into clear-rangetest-results 2025-09-03 13:35:50 +12:00
ford-jones
ba582d6ef4 Protobuf naming reflected in config-switch 2025-09-03 12:23:59 +12:00
ford-jones
bbf6f01d42 Resolve merge conflict 2025-09-03 12:10:17 +12:00
ford-jones
142abb2a4e Updated naming to match protobuf 2025-09-03 12:06:35 +12:00
Chloe Bethel
0952007805 Make ExternalNotification show up in excluded_modules, more STM32 modules (#7797)
* Show ExternalNotification as excluded if it is

* Enable ExternalNotification, SerialModule and RangeTest on STM32WL

* Misc fixes for #7797 - ARCH_STM32 -> ARCH_STM32WL, use less flash by dropping weather station support for serialmodule, set tx/rx pins before begin

* Enable Serial1 on RAK3172, make SerialModule use it (console is on LPUART1)

* Fix SerialModule on RAK3172, fix board definition of RAK3172 to include the right pin mapping.
2025-09-02 07:08:57 -05:00
Tom Fifield
7612799ef6 Fix GPS that hard code 2080 as the start time. (#7803)
* Fix GPS that hard code 2080 as the start time.

Some GPS chips, such as the AG3335 in T1000e and L96 have a hardcoded
time of 2080-01-05 when they start up.

To fix that in a way that seems permanent, let's ignore times that
are more than 40 years since the firmware was built. We should followup
in late 2039 to see if any changes are needed.

Reported-By: @b8b8

* Update src/gps/RTC.cpp

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

* Put FORTY_YEARS in header and use in both places.

* Restore Ben's nicer log lines.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-02 06:40:59 -05:00
Tom Fifield
c5fad6cca1 Hold for 20s after GPS lock (#7801)
* Hold for >20s after GPS lock

GPS chips are designed to stay locked for a while to download some data and save it.
This data is important for speeding up future locks, and making them higher quality.
Our present configuration could make every lock perform similar to first lock.

This patch sets a hold of between 20s and 10% of the lock search time after lock
is acquired. This should allow the GPS to finish its work before we turn it off.

Fixes https://github.com/meshtastic/firmware/issues/7466

* Remove T1000E-specific GPS holds

The new code does the same thing, for all devices.

* Fix publishing settings

* Cleanups, removing unused variables.

* ifdef log line with GPS_DEBUG

* fixQual is not a bool.
2025-09-02 06:05:14 -05:00
Jason P
b8d7222423 If usePreset is False, show value as Custom (#7812) 2025-09-02 05:55:57 -05:00
github-actions[bot]
16d7de5989 Upgrade trunk (#7804)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-09-02 14:53:05 +10:00
Onyx Clawe
102c447fe3 Update variant.h (#7520)
Updated ADC, Full charge now results in 100% charge being reported instead of 95% charge

Co-authored-by: OnyxtheDragon <58921814+OnyxtheDragon@users.noreply.github.com>
2025-09-02 14:31:41 +10:00
Manuel
d66665b96e fix: T-LoRa Pager / T-Deck Pro shutdown (#7792)
* power down during LS and shutdown

* fix T-Deck Pro shutdown

* use device specific define

* slightly rephrase the power off display message
2025-09-02 14:31:41 +10:00
Tom Fifield
088be6bf6a Fix device-install.bat baud rate (master --> develop) (#7816)
* Upgrade trunk (#7763)

Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>

* Fix device-install.bat baud rate

As reported by @gruberaaron , work to improve the 1200bps reset for
esptool caused all runs of device-install.bat to use 1200bps as
the baud rate.

This change removes the general SET "ESPTOOL_BAUD=1200" that was causing
the issues and places the baud settings for reset mode inside the conditional.

Fixes https://github.com/meshtastic/firmware/issues/7172

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-09-02 14:22:48 +10:00
Jonathan Bennett
ca79760372 Add support for the RV-3028 on native Linux (#7802) 2025-08-31 21:08:58 -05:00
Wilson
4a669032dc Change user button to cancel button on meshtiny. (#7789) 2025-08-30 08:37:18 +08:00
github-actions[bot]
b53dd2ec90 Automated version bumps (#7790)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-08-29 19:27:14 -05:00
Ben Meadors
a0e14439cb Merge pull request #7785 from meshtastic/master
Backmerge
2025-08-29 13:59:19 -05:00
Tom Fifield
10c6836263 Can't trust RTCs to tell the time. (#7779)
Further to https://github.com/meshtastic/firmware/pull/7772 ,
we discovered that some RTCs have hard-coded start times well in the
past.

This patch gives RTCs the same treatment as GPS - if the time is
earlier than BUILD_EPOCH, we don't use it.

Fixes #7771
Fixes #7750
2025-08-29 13:22:23 -05:00
Ben Meadors
9b41131af8 Backmerge (#7782)
* Merge pull request #7777 from meshtastic/create-pull-request/bump-version

Bump release version

* Only send Neighbours if we have some to send. (#7493)

* Only send Neighbours if we have some to send.

The original intent of NeighborInfo was that when a NeighbourInfo
was sent all of the nodes that saw it would reply with NeighbourInfo.
So, NeighbourInfo was sent even if there were no hop-zero nodes in
the NodeDB.

Since 2023, when this was implemented, our understanding of running city-wide
meshes has improved substantially. We have taken steps to reduce the impact
of NeighborInfo over LoRa.

This change aligns with those ideas: we will now only send NeighborInfo
if we have some neighbors to contribute.

The impact of this change is that a node must first see another directly
connected node in another packet type before NeighborInfo is sent. This means
that a node with no neighbors is no longer able to trigger other nodes
to broadcast NeighborInfo. It will, however, receive the regular periodic
broadcast of NeighborInfo, and will be able to send NeighborInfo if it
has at least 1 neighbor.

* Include all the things

* AvOid memleak

* We don't gotTime if time is 2019. (#7772)

There are certain GPS chips that have a hard-coded time in firmware
that they will return before lock. We set our own hard-coded time,
BUILD_EPOCH, that should be newer and use the comparison to not set
a bad time.

In https://github.com/meshtastic/firmware/pull/7261 we introduced
the RTCSetResult and improved it in https://github.com/meshtastic/firmware/pull/7375 .

However, the original try-fix left logic in GPS.cpp that could
still result in broadcasting the bad time.

Further, as part of our fix we cleared the GPS buffer if we didn't
get a good time. The mesh was hurting at the time, so this was a reasonable
approach. However, given time tends to come in when we're trying to get
early lock, this had the potential side effect of throwing away valuable
information to get position lock.

This change reverses the clearBuffer and changes the logic so if time
is not set it will not be broadcast.

Fixes https://github.com/meshtastic/firmware/issues/7771
Fixes https://github.com/meshtastic/firmware/issues/7750

---------

Co-authored-by: Tom Fifield <tom@tomfifield.net>
2025-08-29 10:31:55 -05:00
Wilson
fb34dac08d Add On-Screen Keyboard for UpDown Encoder and Rotary Encoder. (#7762)
* Add On-Screen Keyboard for UpDownInterrupt. Pls notice the new keyboard layout was inspired and adviced by https://github.com/csrutil

* Add longPress event for RotaryEncoder Press.

* Update UpdownInterrupt UP and DOWN on main UI.

* Change the interrupt trigger mode from rising edge to falling edge to improve button response.
2025-08-29 23:26:27 +08:00
Tom Fifield
5f8503c62d We don't gotTime if time is 2019. (#7772)
There are certain GPS chips that have a hard-coded time in firmware
that they will return before lock. We set our own hard-coded time,
BUILD_EPOCH, that should be newer and use the comparison to not set
a bad time.

In https://github.com/meshtastic/firmware/pull/7261 we introduced
the RTCSetResult and improved it in https://github.com/meshtastic/firmware/pull/7375 .

However, the original try-fix left logic in GPS.cpp that could
still result in broadcasting the bad time.

Further, as part of our fix we cleared the GPS buffer if we didn't
get a good time. The mesh was hurting at the time, so this was a reasonable
approach. However, given time tends to come in when we're trying to get
early lock, this had the potential side effect of throwing away valuable
information to get position lock.

This change reverses the clearBuffer and changes the logic so if time
is not set it will not be broadcast.

Fixes https://github.com/meshtastic/firmware/issues/7771
Fixes https://github.com/meshtastic/firmware/issues/7750
2025-08-29 09:08:33 -05:00
Jason P
dd2f77ea0c BaseUI Show/Hide Frame Functionality (#7382)
* Rename System Frame (from Memory) in code base

* Create menu options to Show/Hide frames: Node Lists, Bearings, Position, LoRa, Clock and Favorites frames

* Move Region Picker into submenu

* Tweak wording for Send Position vs Node Info if the device has GPS
2025-08-28 11:23:24 -05:00
Ben Meadors
46f797c40d Merge pull request #7777 from meshtastic/create-pull-request/bump-version
Bump release version
2025-08-28 06:02:24 -05:00
thebentern
75b01e17bc Automated version bumps 2025-08-28 10:33:30 +00:00
Ben Meadors
8685436cbb Merge pull request #7773 from meshtastic/master
Backmerge
2025-08-27 20:53:55 -05:00
Ben Meadors
67e3a17b28 Merge pull request #7745 from TN666/add-encrypted-packet-tests
Add more test case for encrypted packet test
2025-08-26 06:23:20 -05:00
Ben Meadors
24204feb71 Merge pull request #7747 from m1nl/esp32-pm-capabability-flags
Setup ESP32 PM-specific capability flags
2025-08-25 19:29:25 -05:00
Ben Meadors
4ace2638e1 Merge pull request #7718 from notmasteryet/err7info
Log more information about ignored packet
2025-08-25 14:29:24 -05:00
m1nl
5aa486d6c2 set HAS_32768HZ for Heltec V3 board 2025-08-25 19:56:15 +02:00
m1nl
ba26d03b1b standarize values of HAS_32768HZ capability flag 2025-08-25 19:44:13 +02:00
m1nl
9a1c2c9b61 setup flags which describe framework / device PM capabilties 2025-08-25 19:42:13 +02:00
TN666
5b9db81819 Add more test case for encrypted packet test 2025-08-25 23:35:03 +08:00
Ben Meadors
f2ba7d7851 Merge pull request #7744 from meshtastic/master
Backmerge to develop
2025-08-25 06:58:05 -05:00
Tom Fifield
1eafdfcbc8 Reduce power of EU433 to 10dBm (#7733)
We are currently blocked from making the breaking change to fix
EU_433 channel centres until 3.0 (https://github.com/meshtastic/firmware/issues/3371 )

However, as already updated in https://github.com/meshtastic/meshtastic/pull/919
the documentation, the power limit for EU_433 is 10dBm. We can change
the power limit without breaking anything, so this patch sets the
power limit to match the ETSI spec without changing any other settings.
2025-08-24 14:45:29 -05:00
TN
103ea2f168 Add more text message test cases for meshpacket serializer (#7709)
* Add more text message test cases for meshpacket serializer

* fix the trunk issue
2025-08-24 07:39:50 -05:00
Austin
4fef890466 Renovate: Always use master as the base. (#7726) 2025-08-23 10:41:57 -05:00
renovate[bot]
35f5b7ec03 Update caveman99-stm32-Crypto digest to 1aa30eb (#7725)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 06:48:57 -05:00
renovate[bot]
1037fa5622 Update meshtastic/device-ui digest to 0f32b64 (#7723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 06:31:30 -05:00
notmasteryet
1c329d9ffa Log more information about ignored packet 2025-08-22 08:46:14 -05:00
Ben Meadors
093a37a2b0 On screen keyboard (#7705)
* Add on-screen keyboard implementation on Trackball device.

* Update On-Screen Keyboard to new layout.

* The on-screen keyboard dynamically adjusts the key size based on the screen.

* Improve input box display on small screens.

* Optimize the virtual keyboard layout and cursor movement logic, and adjust the keyboard starting position for small and wide screens.

* Optimize the text alignment of numeric keys on ssd1306.

---------

Co-authored-by: whywilson <m.tools@qq.com>
2025-08-21 06:31:27 -05:00
ford-jones
4dfcd61d46 If specified, Clean out range test results on module init 2025-08-21 20:05:30 +10:00
ford-jones
9d560fe9e1 Enable protobufs to include rangetest deletion configuration 2025-08-21 20:05:30 +10:00
ford-jones
8e32d58077 Check filesystem mounted 2025-08-21 20:05:30 +10:00
ford-jones
7b24d31636 Use string constants in place of char* 2025-08-21 20:05:30 +10:00
ford-jones
35d9e68053 Enabled deletion of files created by the range-test module 2025-08-21 20:05:30 +10:00
ford-jones
caf2180075 If specified, Clean out range test results on module init 2025-08-21 19:28:52 +12:00
ford-jones
236d2b92dc Enable protobufs to include rangetest deletion configuration 2025-08-21 12:12:13 +12:00
ford-jones
e6a2df5b6d Check filesystem mounted 2025-08-21 12:01:45 +12:00
ford-jones
f6bb1977bc Use string constants in place of char* 2025-08-21 12:00:19 +12:00
ford-jones
9b0fbcf1d9 Enabled deletion of files created by the range-test module 2025-08-21 11:55:53 +12:00
182 changed files with 6349 additions and 1564 deletions

View File

@@ -76,7 +76,7 @@ bool loopCanSleep()
// Called just prior to starting Meshtastic. Allows for setting config values before startup.
void lateInitVariant()
{
settingsMap[logoutputlevel] = level_error;
portduino_config.logoutputlevel = level_error;
channelFile.channels[0] = meshtastic_Channel{
.has_settings = true,
.settings =
@@ -132,7 +132,7 @@ int portduino_main(int argc, char **argv); // Renamed "main" function from Mesht
// Start Meshtastic in a thread and wait till it has reached the ON state.
int LLVMFuzzerInitialize(int *argc, char ***argv)
{
settingsMap[maxtophone] = 5;
portduino_config.maxtophone = 5;
meshtasticThread = std::thread([program = *argv[0]]() {
char nodeIdStr[12];

500
.github/workflows/build_one_arch.yml vendored Normal file
View File

@@ -0,0 +1,500 @@
name: Build One Arch
on:
workflow_dispatch:
inputs:
arch:
type: choice
options:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
- native
jobs:
setup:
strategy:
fail-fast: false
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: 3.x
cache: pip
- run: pip install -U platformio
- name: Generate matrix
id: jsonStep
run: |
if [[ "$GITHUB_HEAD_REF" == "" ]]; then
TARGETS=$(./bin/generate_ci_matrix.py ${{inputs.arch}} extra)
else
TARGETS=$(./bin/generate_ci_matrix.py ${{inputs.arch}} pr)
fi
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF Targets: $TARGETS"
echo "${{inputs.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT
outputs:
esp32: ${{ steps.jsonStep.outputs.esp32 }}
esp32s3: ${{ steps.jsonStep.outputs.esp32s3 }}
esp32c3: ${{ steps.jsonStep.outputs.esp32c3 }}
esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }}
nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }}
rp2040: ${{ steps.jsonStep.outputs.rp2040 }}
rp2350: ${{ steps.jsonStep.outputs.rp2350 }}
stm32: ${{ steps.jsonStep.outputs.stm32 }}
check: ${{ steps.jsonStep.outputs.check }}
version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
id: version
env:
BUILD_LOCATION: local
outputs:
long: ${{ steps.version.outputs.long }}
deb: ${{ steps.version.outputs.deb }}
build-esp32:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32'}}
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32
build-esp32s3:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32s3'}}
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32s3
build-esp32c3:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32c3'}}
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32c3
build-esp32c6:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32c6'}}
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32c6
build-nrf52840:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'nrf52840'}}
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: nrf52840
build-rp2040:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'rp2040'}}
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.rp2040) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: rp2040
build-rp2350:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'rp2350'}}
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.rp2350) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: rp2350
build-stm32:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'stm32' }}
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.stm32) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: stm32
build-debian-src:
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/build_debian_src.yml
with:
series: UNRELEASED
build_location: local
secrets: inherit
package-pio-deps-native-tft:
if: ${{ inputs.arch == 'native' }}
uses: ./.github/workflows/package_pio_deps.yml
with:
pio_env: native-tft
secrets: inherit
test-native:
if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' }}
uses: ./.github/workflows/test_native.yml
docker-deb-amd64:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
docker-deb-amd64-tft:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
pio_env: native-tft
docker-alp-amd64:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
docker-alp-amd64-tft:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
pio_env: native-tft
docker-deb-arm64:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm64
runs-on: ubuntu-24.04-arm
push: false
docker-deb-armv7:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm/v7
runs-on: ubuntu-24.04-arm
push: false
gather-artifacts:
permissions:
contents: write
pull-requests: write
strategy:
fail-fast: false
matrix:
arch:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-latest
needs:
[
version,
build-esp32,
build-esp32s3,
build-esp32c3,
build-esp32c6,
build-nrf52840,
build-rp2040,
build-rp2350,
build-stm32,
]
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v5
with:
path: ./
pattern: firmware-${{inputs.arch}}-*
merge-multiple: true
- name: Display structure of downloaded files
run: ls -R
- name: Move files up
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip
uses: actions/upload-artifact@v4
with:
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: |
./firmware-*.bin
./firmware-*.uf2
./firmware-*.hex
./firmware-*-ota.zip
./device-*.sh
./device-*.bat
./littlefs-*.bin
./bleota*bin
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v5
with:
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output
# For diagnostics
- name: Show artifacts
run: ls -lR
- name: Device scripts permissions
run: |
chmod +x ./output/device-install.sh
chmod +x ./output/device-update.sh
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
uses: actions/upload-artifact@v4
with:
name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip
overwrite: true
path: ./*.elf
retention-days: 30
- uses: scruplelesswizard/comment-artifact@main
if: ${{ github.event_name == 'pull_request' }}
with:
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
description: "Download firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }}
release-artifacts:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
needs:
- version
- gather-artifacts
- build-debian-src
- package-pio-deps-native-tft
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Create release
uses: softprops/action-gh-release@v2
id: create_release
with:
draft: true
prerelease: true
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
tag_name: v${{ needs.version.outputs.long }}
body: |
Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb
uses: actions/download-artifact@v5
with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true
path: ./output/debian-src
- name: Download `native-tft` pio deps
uses: actions/download-artifact@v5
with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output/pio-deps-native-tft
- name: Zip Linux sources
working-directory: output
run: |
zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src
zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft
# For diagnostics
- name: Display structure of downloaded files
run: ls -lR
- name: Add Linux sources to GtiHub Release
# Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }}
run: |
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
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-firmware:
strategy:
fail-fast: false
matrix:
arch:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [release-artifacts, version]
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v5
with:
pattern: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output
- name: Display structure of downloaded files
run: ls -lR
- name: Device scripts permissions
run: |
chmod +x ./output/device-install.sh
chmod +x ./output/device-update.sh
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v5
with:
name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip
merge-multiple: true
path: ./elfs
- name: Zip debug elfs
run: zip -j -9 -r ./debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./elfs
# For diagnostics
- name: Display structure of downloaded files
run: ls -lR
- name: Add bins and debug elfs to GitHub Release
# Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }}
run: |
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip
gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-firmware:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [release-firmware, version]
env:
targets: |-
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v5
with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./publish
- name: Publish firmware to meshtastic.github.io
uses: peaceiris/actions-gh-pages@v4
env:
# On event/* branches, use the event name as the destination prefix
DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }}
with:
deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }}
external_repository: meshtastic/meshtastic.github.io
publish_branch: master
publish_dir: ./publish
destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }}
keep_files: true
user_name: github-actions[bot]
user_email: github-actions[bot]@users.noreply.github.com
commit_message: ${{ needs.version.outputs.long }}
enable_jekyll: true

395
.github/workflows/build_one_target.yml vendored Normal file
View File

@@ -0,0 +1,395 @@
name: Build One Target
on:
workflow_dispatch:
inputs:
arch:
type: choice
options:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
- native
target:
type: string
required: false
description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets.
# find-target:
# type: boolean
# default: true
# description: 'Find the available targets'
jobs:
find-targets:
if: ${{ inputs.target == '' }}
strategy:
fail-fast: false
matrix:
arch:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: 3.x
cache: pip
- run: pip install -U platformio
- name: Generate matrix
id: jsonStep
run: |
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} extra)
echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY
echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY
echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
echo "Targets:" >> $GITHUB_STEP_SUMMARY
echo $TARGETS | sed 's/[][]//g; s/", "/\n- /g; s/"//g; s/^/- /' >> $GITHUB_STEP_SUMMARY
version:
if: ${{ inputs.target != '' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
id: version
env:
BUILD_LOCATION: local
outputs:
long: ${{ steps.version.outputs.long }}
deb: ${{ steps.version.outputs.deb }}
build-arch:
if: ${{ inputs.target != '' && inputs.arch != 'native' }}
needs: [version]
strategy:
fail-fast: false
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ inputs.target }}
platform: ${{ inputs.arch }}
build-debian-src:
if: ${{ github.repository == 'meshtastic/firmware' && inputs.arch == 'native' }}
uses: ./.github/workflows/build_debian_src.yml
with:
series: UNRELEASED
build_location: local
secrets: inherit
package-pio-deps-native-tft:
if: ${{ inputs.arch == 'native' }}
uses: ./.github/workflows/package_pio_deps.yml
with:
pio_env: native-tft
secrets: inherit
test-native:
if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' && inputs.target != '' }}
uses: ./.github/workflows/test_native.yml
docker-deb-amd64:
if: ${{ inputs.target != '' && inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
docker-deb-amd64-tft:
if: ${{ inputs.target != '' && inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
pio_env: native-tft
docker-alp-amd64:
if: ${{ inputs.target != '' && inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
docker-alp-amd64-tft:
if: ${{ inputs.target != '' && inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
pio_env: native-tft
docker-deb-arm64:
if: ${{ inputs.target != '' && inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm64
runs-on: ubuntu-24.04-arm
push: false
docker-deb-armv7:
if: ${{ inputs.target != '' && inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm/v7
runs-on: ubuntu-24.04-arm
push: false
gather-artifacts:
permissions:
contents: write
pull-requests: write
strategy:
fail-fast: false
runs-on: ubuntu-latest
needs: [version, build-arch]
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v5
with:
path: ./
pattern: firmware-*-*
merge-multiple: true
- name: Display structure of downloaded files
run: ls -R
- name: Move files up
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip
uses: actions/upload-artifact@v4
with:
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
overwrite: true
path: |
./firmware-*.bin
./firmware-*.uf2
./firmware-*.hex
./firmware-*-ota.zip
./device-*.sh
./device-*.bat
./littlefs-*.bin
./bleota*bin
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v5
with:
pattern: firmware-*-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output
# For diagnostics
- name: Show artifacts
run: ls -lR
- name: Device scripts permissions
run: |
chmod +x ./output/device-install.sh
chmod +x ./output/device-update.sh
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
uses: actions/upload-artifact@v4
with:
name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
overwrite: true
path: ./*.elf
retention-days: 30
- uses: scruplelesswizard/comment-artifact@main
if: ${{ github.event_name == 'pull_request' }}
with:
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
description: "Download firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }}
release-artifacts:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && inputs.target != ''}}
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
needs:
- version
- gather-artifacts
- build-debian-src
- package-pio-deps-native-tft
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Create release
uses: softprops/action-gh-release@v2
id: create_release
with:
draft: true
prerelease: true
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
tag_name: v${{ needs.version.outputs.long }}
body: |
Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb
uses: actions/download-artifact@v5
with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true
path: ./output/debian-src
- name: Download `native-tft` pio deps
uses: actions/download-artifact@v5
with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output/pio-deps-native-tft
- name: Zip Linux sources
working-directory: output
run: |
zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src
zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft
# For diagnostics
- name: Display structure of downloaded files
run: ls -lR
- name: Add Linux sources to GtiHub Release
# Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }}
run: |
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
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-firmware:
strategy:
fail-fast: false
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && inputs.target != ''}}
needs: [release-artifacts, version]
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v5
with:
pattern: firmware-*-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output
- name: Display structure of downloaded files
run: ls -lR
- name: Device scripts permissions
run: |
chmod +x ./output/device-install.sh
chmod +x ./output/device-update.sh
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v5
with:
pattern: debug-elfs-*-${{ needs.version.outputs.long }}.zip
merge-multiple: true
path: ./elfs
- name: Zip debug elfs
run: zip -j -9 -r ./debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./elfs
# For diagnostics
- name: Display structure of downloaded files
run: ls -lR
- name: Add bins and debug elfs to GitHub Release
# Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }}
run: |
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-firmware:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' && inputs.target != '' }}
needs: [release-firmware, version]
env:
targets: |-
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v5
with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./publish
- name: Publish firmware to meshtastic.github.io
uses: peaceiris/actions-gh-pages@v4
env:
# On event/* branches, use the event name as the destination prefix
DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }}
with:
deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }}
external_repository: meshtastic/meshtastic.github.io
publish_branch: master
publish_dir: ./publish
destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }}
keep_files: true
user_name: github-actions[bot]
user_email: github-actions[bot]@users.noreply.github.com
commit_message: ${{ needs.version.outputs.long }}
enable_jekyll: true

View File

@@ -21,18 +21,20 @@ permissions:
jobs:
docker-multiarch:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_manifest.yml
with:
release_channel: daily
secrets: inherit
package-ppa:
if: github.repository == 'meshtastic/firmware'
strategy:
fail-fast: false
matrix:
series:
- jammy # 22.04
- noble # 24.04
- jammy # 22.04 LTS
- noble # 24.04 LTS
- plucky # 25.04
- questing # 25.10
uses: ./.github/workflows/package_ppa.yml
@@ -42,6 +44,7 @@ jobs:
secrets: inherit
package-obs:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/package_obs.yml
with:
obs_project: network:Meshtastic:daily
@@ -49,6 +52,7 @@ jobs:
secrets: inherit
hook-copr:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/hook_copr.yml
with:
copr_project: daily

View File

@@ -27,6 +27,7 @@ on:
jobs:
setup:
if: github.repository == 'meshtastic/firmware'
strategy:
fail-fast: false
matrix:
@@ -48,6 +49,10 @@ jobs:
python-version: 3.x
cache: pip
- run: pip install -U platformio
- name: Uncomment build epoch
shell: bash
run: |
sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
- name: Generate matrix
id: jsonStep
run: |
@@ -70,6 +75,7 @@ jobs:
check: ${{ steps.jsonStep.outputs.check }}
version:
if: github.repository == 'meshtastic/firmware'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -91,7 +97,7 @@ jobs:
matrix: ${{ fromJson(needs.setup.outputs.check) }}
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' }}
if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
steps:
- uses: actions/checkout@v5
- name: Build base
@@ -204,10 +210,11 @@ jobs:
secrets: inherit
test-native:
if: ${{ !contains(github.ref_name, 'event/') }}
if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }}
uses: ./.github/workflows/test_native.yml
docker-deb-amd64:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
@@ -216,6 +223,7 @@ jobs:
push: false
docker-deb-amd64-tft:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
@@ -225,6 +233,7 @@ jobs:
pio_env: native-tft
docker-alp-amd64:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
@@ -233,6 +242,7 @@ jobs:
push: false
docker-alp-amd64-tft:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
@@ -242,6 +252,7 @@ jobs:
pio_env: native-tft
docker-deb-arm64:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
@@ -250,6 +261,7 @@ jobs:
push: false
docker-deb-armv7:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
@@ -259,6 +271,7 @@ jobs:
gather-artifacts:
# trunk-ignore(checkov/CKV2_GHA_1)
if: github.repository == 'meshtastic/firmware'
permissions:
contents: write
pull-requests: write
@@ -358,7 +371,7 @@ jobs:
release-artifacts:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
needs:
@@ -433,7 +446,7 @@ jobs:
- rp2350
- stm32
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware'}}
needs: [release-artifacts, version]
steps:
- name: Checkout

View File

@@ -27,8 +27,8 @@ jobs:
- check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: 3.x
cache: pip
@@ -57,7 +57,7 @@ jobs:
version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
@@ -78,7 +78,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build base
id: base
uses: ./.github/actions/setup-base
@@ -274,12 +274,12 @@ jobs:
]
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
path: ./
pattern: firmware-${{matrix.arch}}-*
@@ -308,7 +308,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -353,10 +353,10 @@ jobs:
- package-pio-deps-native-tft
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.x
@@ -372,14 +372,14 @@ jobs:
Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true
path: ./output/debian-src
- name: Download `native-tft` pio deps
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -422,14 +422,14 @@ jobs:
needs: [release-artifacts, version]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -446,7 +446,7 @@ jobs:
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
merge-multiple: true
@@ -477,14 +477,14 @@ jobs:
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true

View File

@@ -10,7 +10,7 @@ permissions:
jobs:
check-label:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Check for PR labels
uses: actions/github-script@v8

View File

@@ -21,10 +21,10 @@ jobs:
fail-fast: false
matrix:
series:
- jammy # 22.04
- noble # 24.04
- jammy # 22.04 LTS
- noble # 24.04 LTS
- plucky # 25.04
# - questing # 25.10
- questing # 25.10
uses: ./.github/workflows/package_ppa.yml
with:
ppa_repo: |-

View File

@@ -8,25 +8,25 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.471
- renovate@41.115.2
- checkov@3.2.473
- renovate@41.132.5
- prettier@3.6.2
- trufflehog@3.90.6
- trufflehog@3.90.8
- yamllint@1.37.1
- bandit@1.8.6
- trivy@0.66.0
- trivy@0.67.0
- taplo@0.10.0
- ruff@0.13.0
- ruff@0.13.2
- isort@6.0.1
- markdownlint@0.45.0
- oxipng@9.1.5
- svgo@4.0.0
- actionlint@1.7.7
- flake8@7.3.0
- hadolint@2.13.1
- hadolint@2.14.0
- shfmt@3.6.0
- shellcheck@0.11.0
- black@25.1.0
- black@25.9.0
- git-diff-check
- gitleaks@8.28.0
- clang-format@16.0.3

View File

@@ -36,6 +36,7 @@ build_flags =
-DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192
-DESP_OPENSSL_SUPPRESS_LEGACY_WARNING
-DSERIAL_BUFFER_SIZE=4096
-DSERIAL_HAS_ON_RECEIVE
-DLIBPAX_ARDUINO
-DLIBPAX_WIFI
-DLIBPAX_BLE

View File

@@ -2,7 +2,7 @@
[portduino_base]
platform =
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
https://github.com/meshtastic/platform-native/archive/c490bcd019e0658404088a61b96e653c9da22c45.zip
https://github.com/meshtastic/platform-native/archive/d3f6e339534233c7217818867368767590ce549e.zip
framework = arduino
build_src_filter =

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
BPS_RESET=false

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
CHANGE_MODE=false

View File

@@ -87,6 +87,15 @@
</screenshots>
<releases>
<release version="2.7.12" date="2025-10-01">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.12</url>
</release>
<release version="2.7.11" date="2025-09-24">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.11</url>
</release>
<release version="2.7.10" date="2025-09-18">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.10</url>
</release>
<release version="2.7.9" date="2025-09-03">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9</url>
</release>

43
boards/heltec_v4.json Normal file
View File

@@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_qspi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "qspi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_v4"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "heltec_wifi_lora_32 v4 (16 MB FLASH, 2 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 2097152,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "heltec"
}

52
boards/r1-neo.json Normal file
View File

@@ -0,0 +1,52 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x8029"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x239A", "0x802A"]
],
"usb_product": "Muzi R1 Neo",
"mcu": "nrf52840",
"variant": "r1-neo",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino", "freertos"],
"name": "WisCore RAK4631 Board",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://muzi.works/",
"vendor": "Muzi Works"
}

47
debian/changelog vendored
View File

@@ -1,50 +1,13 @@
meshtasticd (2.7.9.0) UNRELEASED; urgency=medium
meshtasticd (2.7.12.0) unstable; urgency=medium
[ Austin Lane ]
* Initial packaging
* GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
* Version 2.5.19
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ GitHub Actions ]
* Version 2.7.12
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ Ubuntu ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
-- <github-actions[bot]@users.noreply.github.com> Wed, 03 Sep 2025 23:39:17 +0000
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 01 Oct 2025 19:51:41 +0000

View File

@@ -1,7 +1,8 @@
#!/usr/bin/bash
export DEBFULLNAME="GitHub Actions"
export DEBEMAIL="github-actions[bot]@users.noreply.github.com"
PKG_VERSION=$(python3 bin/buildinfo.py short)
dch --newversion "$PKG_VERSION.0" \
--distribution UNRELEASED \
"GitHub Actions Automatic version bump"
--distribution unstable \
"Version $PKG_VERSION"

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
# postinst script for meshtasticd
#
# see: dh_installdeb(1)

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
# postrm script for meshtasticd
#
# see: dh_installdeb(1)

View File

@@ -55,6 +55,8 @@ build_flags = -Wno-missing-field-initializers
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
#-D OLED_PL=1
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
#-D DEBUG_LOOP_TIMING=1 ; uncomment to add main loop timing logs
monitor_speed = 115200
monitor_filters = direct
@@ -113,7 +115,7 @@ lib_deps =
[radiolib_base]
lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
jgromes/RadioLib@7.2.1
jgromes/RadioLib@7.3.0
[device-ui_base]
lib_deps =
@@ -124,7 +126,7 @@ lib_deps =
[environmental_base]
lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BusIO packageName=adafruit/library/Adafruit BusIO
adafruit/Adafruit BusIO@1.17.2
adafruit/Adafruit BusIO@1.17.4
# renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor
adafruit/Adafruit Unified Sensor@1.1.15
# renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library

View File

@@ -8,6 +8,7 @@
"replacements:all",
"workarounds:all"
],
"baseBranchPatterns": ["master"],
"forkProcessing": "enabled",
"ignoreDeps": [
"protobufs"

View File

@@ -26,6 +26,7 @@ class AudioThread : public concurrency::OSThread
i2sRtttl->begin(rtttlFile, audioOut);
}
// Also handles actually playing the RTTTL, needs to be called in loop
bool isPlaying()
{
if (i2sRtttl != nullptr) {

View File

@@ -146,7 +146,7 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
{
int result;
#ifdef ARCH_PORTDUINO
bool utf = !settingsMap[ascii_logs];
bool utf = !portduino_config.ascii_logs;
#else
bool utf = true;
#endif

View File

@@ -38,4 +38,46 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
return useShortName ? "Custom" : "Invalid";
break;
}
}
const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role)
{
switch (role) {
case meshtastic_Config_DeviceConfig_Role_CLIENT:
return "Client";
break;
case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE:
return "Client Mute";
break;
case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN:
return "Client Hidden";
break;
case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE:
return "Client Base";
break;
case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND:
return "Lost and Found";
break;
case meshtastic_Config_DeviceConfig_Role_TRACKER:
return "Tracker";
break;
case meshtastic_Config_DeviceConfig_Role_SENSOR:
return "Sensor";
break;
case meshtastic_Config_DeviceConfig_Role_TAK:
return "TAK";
break;
case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER:
return "TAK Tracker";
break;
case meshtastic_Config_DeviceConfig_Role_ROUTER:
return "Router";
break;
case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE:
return "Router Late";
break;
default:
return "Unknown";
break;
}
}

View File

@@ -6,4 +6,5 @@ class DisplayFormatters
public:
static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName,
bool usePreset);
static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role);
};

View File

@@ -22,6 +22,9 @@ class GPSStatus : public Status
meshtastic_Position p = meshtastic_Position_init_default;
/// Time of last valid GPS fix (millis since boot)
uint32_t lastFixMillis = 0;
public:
GPSStatus() { statusType = STATUS_TYPE_GPS; }
@@ -83,6 +86,9 @@ class GPSStatus : public Status
uint32_t getNumSatellites() const { return p.sats_in_view; }
/// Return millis() when the last GPS fix occurred (0 = never)
uint32_t getLastFixMillis() const { return lastFixMillis; }
bool matches(const GPSStatus *newStatus) const
{
#ifdef GPS_DEBUG
@@ -114,6 +120,9 @@ class GPSStatus : public Status
if (isDirty) {
if (hasLock) {
// Record time of last valid GPS fix
lastFixMillis = millis();
// In debug logs, identify position by @timestamp:stage (stage 3 = notify)
LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp,
p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5,

View File

@@ -4,6 +4,7 @@
#include "concurrency/OSThread.h"
#include "configuration.h"
#include "main.h"
#include "memGet.h"
#include "mesh/generated/meshtastic/mesh.pb.h"
#include <assert.h>
#include <cstring>
@@ -57,7 +58,7 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l
#endif
#ifdef ARCH_PORTDUINO
bool color = !settingsMap[ascii_logs];
bool color = !portduino_config.ascii_logs;
#else
bool color = true;
#endif
@@ -99,7 +100,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
size_t r = 0;
#ifdef ARCH_PORTDUINO
bool color = !settingsMap[ascii_logs];
bool color = !portduino_config.ascii_logs;
#else
bool color = true;
#endif
@@ -166,6 +167,16 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
print(thread->ThreadName);
print("] ");
}
#ifdef DEBUG_HEAP
// Add heap free space bytes prefix before every log message
#ifdef ARCH_PORTDUINO
::printf("[heap %u] ", memGet.getFreeHeap());
#else
printf("[heap %u] ", memGet.getFreeHeap());
#endif
#endif // DEBUG_HEAP
r += vprintf(logLevel, format, arg);
}
@@ -288,7 +299,7 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
#if ARCH_PORTDUINO
// level trace is special, two possible ways to handle it.
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
if (settingsStrings[traceFilename] != "") {
if (portduino_config.traceFilename != "") {
va_list arg;
va_start(arg, format);
try {
@@ -297,18 +308,18 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
}
va_end(arg);
}
if (settingsMap[logoutputlevel] < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
delete[] newFormat;
return;
}
}
if (settingsMap[logoutputlevel] < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
delete[] newFormat;
return;
} else if (settingsMap[logoutputlevel] < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
} else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
delete[] newFormat;
return;
} else if (settingsMap[logoutputlevel] < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
} else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
delete[] newFormat;
return;
}

View File

@@ -6,6 +6,14 @@
#include "configuration.h"
#include "time.h"
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
#define IS_USB_SERIAL
#ifdef SERIAL_HAS_ON_RECEIVE
#undef SERIAL_HAS_ON_RECEIVE
#endif
#include "HWCDC.h"
#endif
#ifdef RP2040_SLOW_CLOCK
#define Port Serial2
#else
@@ -22,7 +30,12 @@ SerialConsole *console;
void consoleInit()
{
new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread
auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread
#if defined(SERIAL_HAS_ON_RECEIVE)
// onReceive does only exist for HardwareSerial not for USB CDC serial
Port.onReceive([sc]() { sc->rxInt(); });
#endif
DEBUG_PORT.rpInit(); // Simply sets up semaphore
}
@@ -65,14 +78,21 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
int32_t SerialConsole::runOnce()
{
#ifdef HELTEC_MESH_SOLAR
//After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
if(moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port
&& moduleConfig.serial.mode==meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)
{
// After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port &&
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) {
return 250;
}
#endif
return runOncePart();
int32_t delay = runOncePart();
#if defined(SERIAL_HAS_ON_RECEIVE)
return Port.available() ? delay : INT32_MAX;
#elif defined(IS_USB_SERIAL)
return HWCDC::isPlugged() ? delay : (1000 * 20);
#else
return delay;
#endif
}
void SerialConsole::flush()
@@ -80,6 +100,18 @@ void SerialConsole::flush()
Port.flush();
}
// trigger tx of serial data
void SerialConsole::onNowHasData(uint32_t fromRadioNum)
{
setIntervalFromNow(0);
}
// trigger rx of serial data
void SerialConsole::rxInt()
{
setIntervalFromNow(0);
}
// For the serial port we can't really detect if any client is on the other side, so instead just look for recent messages
bool SerialConsole::checkIsConnected()
{

View File

@@ -32,11 +32,14 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur
virtual int32_t runOnce() override;
void flush();
void rxInt();
protected:
/// Check the current underlying physical link to see if the client is currently connected
virtual bool checkIsConnected() override;
virtual void onNowHasData(uint32_t fromRadioNum) override;
/// Possibly switch to protobufs if we see a valid protobuf message
virtual void log_to_serial(const char *logLevel, const char *format, va_list arg);
};

View File

@@ -5,7 +5,7 @@
BuzzerFeedbackThread *buzzerFeedbackThread;
BuzzerFeedbackThread::BuzzerFeedbackThread() : OSThread("BuzzerFeedback")
BuzzerFeedbackThread::BuzzerFeedbackThread()
{
if (inputBroker)
inputObserver.observe(inputBroker);
@@ -19,20 +19,19 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
return 0; // Let other handlers process the event
}
// Track last event time for potential future use
lastEventTime = millis();
needsUpdate = true;
// Handle different input events with appropriate buzzer feedback
switch (event->inputEvent) {
case INPUT_BROKER_USER_PRESS:
case INPUT_BROKER_ALT_PRESS:
case INPUT_BROKER_SELECT:
case INPUT_BROKER_SELECT_LONG:
playBeep(); // Confirmation feedback
break;
case INPUT_BROKER_UP:
case INPUT_BROKER_UP_LONG:
case INPUT_BROKER_DOWN:
case INPUT_BROKER_DOWN_LONG:
case INPUT_BROKER_LEFT:
case INPUT_BROKER_RIGHT:
playChirp(); // Navigation feedback
@@ -58,15 +57,4 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
}
return 0; // Allow other handlers to process the event
}
int32_t BuzzerFeedbackThread::runOnce()
{
// This thread is primarily event-driven, but we can use runOnce
// for any periodic tasks if needed in the future
needsUpdate = false;
// Run every 100ms when active, less frequently when idle
return needsUpdate ? 100 : 1000;
}
}

View File

@@ -4,7 +4,7 @@
#include "concurrency/OSThread.h"
#include "input/InputBroker.h"
class BuzzerFeedbackThread : public concurrency::OSThread
class BuzzerFeedbackThread
{
CallbackObserver<BuzzerFeedbackThread, const InputEvent *> inputObserver =
CallbackObserver<BuzzerFeedbackThread, const InputEvent *>(this, &BuzzerFeedbackThread::handleInputEvent);
@@ -12,13 +12,6 @@ class BuzzerFeedbackThread : public concurrency::OSThread
public:
BuzzerFeedbackThread();
int handleInputEvent(const InputEvent *event);
protected:
virtual int32_t runOnce() override;
private:
uint32_t lastEventTime = 0;
bool needsUpdate = false;
};
extern BuzzerFeedbackThread *buzzerFeedbackThread;

View File

@@ -90,7 +90,9 @@ void OSThread::run()
if (heap < newHeap)
LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap);
#endif
#ifdef DEBUG_LOOP_TIMING
LOG_DEBUG("====== Thread next run in: %d", newDelay);
#endif
runned();
if (newDelay >= 0)

View File

@@ -117,6 +117,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define SX126X_MAX_POWER 22
#endif
#ifdef HELTEC_V4
// Power Amps are often non-linear, so we can use an array of values for the power curve
#define NUM_PA_POINTS 22
#define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7
#endif
// Default system gain to 0 if not defined
#ifndef TX_GAIN_LORA
#define TX_GAIN_LORA 0
@@ -262,6 +268,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define VEXT_ON_VALUE LOW
#endif
// -----------------------------------------------------------------------------
// Rotary encoder
// -----------------------------------------------------------------------------
#ifndef ROTARY_DELAY
#define ROTARY_DELAY 5
#endif
// -----------------------------------------------------------------------------
// GPS
// -----------------------------------------------------------------------------

View File

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

View File

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

View File

@@ -197,6 +197,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
#ifdef PCF8563_RTC
SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address)
#endif
#ifdef RX8130CE_RTC
SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address)
#endif
case CARDKB_ADDR:
// Do we have the RAK14006 instead?

View File

@@ -516,6 +516,7 @@ bool GPS::setup()
}
}
// Rare Serial Speeds
#ifndef CONFIG_IDF_TARGET_ESP32C6
if (probeTries == GPS_PROBETRIES) {
LOG_DEBUG("Probe for GPS at %d", rareSerialSpeeds[speedSelect]);
gnssModel = probe(rareSerialSpeeds[speedSelect]);
@@ -526,6 +527,7 @@ bool GPS::setup()
}
}
}
#endif
}
if (gnssModel != GNSS_MODEL_UNKNOWN) {
@@ -807,6 +809,14 @@ bool GPS::setup()
} else {
LOG_INFO("GNSS module configuration saved!");
}
} else if (gnssModel == GNSS_MODEL_CM121) {
// only ask for RMC and GGA
// enable GGA
_serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n");
delay(250);
// enable RMC
_serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n");
delay(250);
}
didSerialInit = true;
}
@@ -1094,11 +1104,6 @@ int32_t GPS::runOnce()
publishUpdate();
}
// Repeaters have no need for GPS
if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
return disable();
}
if (whileActive()) {
// if we have received valid NMEA claim we are connected
setConnected();
@@ -1239,9 +1244,15 @@ GnssModel_t GPS::probe(int serialSpeed)
_serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n");
_serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n");
delay(20);
// Close NMEA sequences on CM121
_serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n");
_serial_gps->write("$CFGMSG,0,2,0,1*18\r\n");
_serial_gps->write("$CFGMSG,0,3,0,1*19\r\n");
delay(20);
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A
std::vector<ChipInfo> unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}};
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121
std::vector<ChipInfo> unicore = {
{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}};
PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500);
std::vector<ChipInfo> atgm = {
@@ -1440,7 +1451,7 @@ GPS *GPS::createGps()
_en_gpio = PIN_GPS_EN;
#endif
#ifdef ARCH_PORTDUINO
if (!settingsMap[has_gps])
if (!portduino_config.has_gps)
return nullptr;
#endif
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all

View File

@@ -31,7 +31,8 @@ typedef enum {
GNSS_MODEL_MTK_PA1616S,
GNSS_MODEL_AG3335,
GNSS_MODEL_AG3352,
GNSS_MODEL_LS20031
GNSS_MODEL_LS20031,
GNSS_MODEL_CM121
} GnssModel_t;
typedef enum {

View File

@@ -109,6 +109,35 @@ RTCSetResult readFromRTC()
}
return RTCSetResultSuccess;
}
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {
uint32_t now = millis();
ArtronShop_RX8130CE rtc(&Wire);
tm t;
if (rtc.getTime(&t)) {
tv.tv_sec = gm_mktime(&t);
tv.tv_usec = 0;
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
LOG_DEBUG("Read RTC time from RX8130CE getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900,
t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
#ifdef BUILD_EPOCH
if (tv.tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime;
}
#endif
if (currentQuality == RTCQualityNone) {
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
currentQuality = RTCQualityDevice;
}
return RTCSetResultSuccess;
}
}
#else
if (!gettimeofday(&tv, NULL)) {
uint32_t now = millis();
@@ -214,6 +243,17 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
LOG_DEBUG("PCF8563_RTC setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
}
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {
ArtronShop_RX8130CE rtc(&Wire);
tm *t = gmtime(&tv->tv_sec);
if (rtc.setTime(*t)) {
LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1,
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
} else {
LOG_WARN("Failed to set time for RX8130CE");
}
}
#elif defined(ARCH_ESP32)
settimeofday(tv, NULL);
#endif

View File

@@ -4,6 +4,10 @@
#include "sys/time.h"
#include <Arduino.h>
#ifdef RX8130CE_RTC
#include <ArtronShop_RX8130CE.h>
#endif
enum RTCQuality {
/// We haven't had our RTC set yet

View File

@@ -243,7 +243,7 @@ bool EInkDisplay::connect()
adafruitDisplay->setRotation(1);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
}
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK)
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
{
spi1 = &SPI1;
spi1->begin();

View File

@@ -84,7 +84,7 @@ class EInkDisplay : public OLEDDisplay
SPIClass *hspi = NULL;
#endif
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK)
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
SPIClass *spi1 = NULL;
#endif

View File

@@ -25,6 +25,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "PowerMon.h"
#include "Throttle.h"
#include "configuration.h"
#include "meshUtils.h"
#if HAS_SCREEN
#include <OLEDDisplay.h>
@@ -58,7 +59,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "mesh-pb-constants.h"
#include "mesh/Channels.h"
#include "mesh/generated/meshtastic/deviceonly.pb.h"
#include "meshUtils.h"
#include "modules/ExternalNotificationModule.h"
#include "modules/TextMessageModule.h"
#include "modules/WaypointModule.h"
@@ -83,6 +83,11 @@ extern uint16_t TFT_MESH;
#include "platform/portduino/PortduinoGlue.h"
#endif
#if defined(T_LORA_PAGER)
// KB backlight control
#include "input/cardKbI2cImpl.h"
#endif
using namespace meshtastic; /** @todo remove */
namespace graphics
@@ -216,6 +221,44 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
ui->update();
}
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> textCallback)
{
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
if (NotificationRenderer::virtualKeyboard) {
delete NotificationRenderer::virtualKeyboard;
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::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); });
// Store the message and set the expiration timestamp (use same pattern as other notifications)
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
NotificationRenderer::alertBannerMessage[255] = '\0';
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
NotificationRenderer::pauseBanner = false;
NotificationRenderer::current_notification_type = notificationTypeEnum::text_input;
// Set the overlay using the same pattern as other notification types
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
ui->update();
}
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
uint8_t module_frame;
@@ -340,7 +383,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif ARCH_PORTDUINO
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
if (settingsMap[displayPanel] != no_screen) {
if (portduino_config.displayPanel != no_screen) {
LOG_DEBUG("Make TFTDisplay!");
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
@@ -588,7 +631,7 @@ void Screen::setup()
#if ARCH_PORTDUINO
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
if (settingsMap[touchscreenModule]) {
if (portduino_config.touchscreenModule) {
touchScreenImpl1 =
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
touchScreenImpl1->init();
@@ -617,6 +660,19 @@ void Screen::setup()
MeshModule::observeUIEvents(&uiFrameEventObserver);
}
void Screen::setOn(bool on, FrameCallback einkScreensaver)
{
#if defined(T_LORA_PAGER)
if (cardKbI2cImpl)
cardKbI2cImpl->toggleBacklight(on);
#endif
if (!on)
// We handle off commands immediately, because they might be called because the CPU is shutting down
handleSetOn(false, einkScreensaver);
else
enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON});
}
void Screen::forceDisplay(bool forceUiUpdate)
{
// Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup.
@@ -725,13 +781,19 @@ int32_t Screen::runOnce()
handleSetOn(false);
break;
case Cmd::ON_PRESS:
handleOnPress();
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleOnPress();
}
break;
case Cmd::SHOW_PREV_FRAME:
handleShowPrevFrame();
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleShowPrevFrame();
}
break;
case Cmd::SHOW_NEXT_FRAME:
handleShowNextFrame();
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleShowNextFrame();
}
break;
case Cmd::START_ALERT_FRAME: {
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
@@ -753,7 +815,9 @@ int32_t Screen::runOnce()
NotificationRenderer::pauseBanner = false;
case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
setFrames();
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
setFrames();
}
break;
case Cmd::NOOP:
break;
@@ -789,6 +853,7 @@ int32_t Screen::runOnce()
if (showingNormalScreen) {
// standard screen loop handling here
if (config.display.auto_screen_carousel_secs > 0 &&
NotificationRenderer::current_notification_type != notificationTypeEnum::text_input &&
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
@@ -879,6 +944,11 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
// Called when a frame should be added / removed, or custom frames should be cleared
void Screen::setFrames(FrameFocus focus)
{
// Block setFrames calls when virtual keyboard is active to prevent overlay interference
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return;
}
uint8_t originalPosition = ui->getUiState()->currentFrame;
uint8_t previousFrameCount = framesetInfo.frameCount;
FramesetInfo fsi; // Location of specific frames, for applying focus parameter
@@ -901,75 +971,95 @@ void Screen::setFrames(FrameFocus focus)
}
#if defined(DISPLAY_CLOCK_FRAME)
fsi.positions.clock = numframes;
if (!hiddenFrames.clock) {
fsi.positions.clock = numframes;
#if defined(M5STACK_UNITC6L)
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
#else
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame;
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame;
#endif
indicatorIcons.push_back(digital_icon_clock);
indicatorIcons.push_back(digital_icon_clock);
}
#endif
// Declare this early so its available in FOCUS_PRESERVE block
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
fsi.positions.home = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
indicatorIcons.push_back(icon_home);
if (!hiddenFrames.home) {
fsi.positions.home = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
indicatorIcons.push_back(icon_home);
}
fsi.positions.textMessage = numframes;
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK
fsi.positions.nodelist = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
indicatorIcons.push_back(icon_nodes);
if (!hiddenFrames.nodelist) {
fsi.positions.nodelist = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
indicatorIcons.push_back(icon_nodes);
}
#endif
// Show detailed node views only on E-Ink builds
#ifdef USE_EINK
fsi.positions.nodelist_lastheard = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
indicatorIcons.push_back(icon_nodes);
fsi.positions.nodelist_hopsignal = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
indicatorIcons.push_back(icon_signal);
fsi.positions.nodelist_distance = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
indicatorIcons.push_back(icon_distance);
if (!hiddenFrames.nodelist_lastheard) {
fsi.positions.nodelist_lastheard = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
indicatorIcons.push_back(icon_nodes);
}
if (!hiddenFrames.nodelist_hopsignal) {
fsi.positions.nodelist_hopsignal = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
indicatorIcons.push_back(icon_signal);
}
if (!hiddenFrames.nodelist_distance) {
fsi.positions.nodelist_distance = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
indicatorIcons.push_back(icon_distance);
}
#endif
#if HAS_GPS
fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list);
fsi.positions.gps = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
indicatorIcons.push_back(icon_compass);
if (!hiddenFrames.nodelist_bearings) {
fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list);
}
if (!hiddenFrames.gps) {
fsi.positions.gps = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
indicatorIcons.push_back(icon_compass);
}
#endif
if (RadioLibInterface::instance) {
if (RadioLibInterface::instance && !hiddenFrames.lora) {
fsi.positions.lora = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused;
indicatorIcons.push_back(icon_radio);
}
if (!dismissedFrames.memory) {
fsi.positions.memory = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage;
indicatorIcons.push_back(icon_memory);
if (!hiddenFrames.system) {
fsi.positions.system = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen;
indicatorIcons.push_back(icon_system);
}
#if !defined(DISPLAY_CLOCK_FRAME)
fsi.positions.clock = numframes;
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame;
indicatorIcons.push_back(digital_icon_clock);
if (!hiddenFrames.clock) {
fsi.positions.clock = numframes;
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame;
indicatorIcons.push_back(digital_icon_clock);
}
#endif
if (!hiddenFrames.chirpy) {
fsi.positions.chirpy = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy;
indicatorIcons.push_back(chirpy_small);
}
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
if (!dismissedFrames.wifi && isWifiAvailable()) {
if (!hiddenFrames.wifi && isWifiAvailable()) {
fsi.positions.wifi = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
indicatorIcons.push_back(icon_wifi);
@@ -1011,27 +1101,29 @@ void Screen::setFrames(FrameFocus focus)
if (numMeshNodes > 0)
numMeshNodes--;
// Temporary array to hold favorite node frames
std::vector<FrameCallback> favoriteFrames;
if (!hiddenFrames.show_favorites) {
// Temporary array to hold favorite node frames
std::vector<FrameCallback> favoriteFrames;
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
}
}
}
// Insert favorite frames *after* collecting them all
if (!favoriteFrames.empty()) {
fsi.positions.firstFavorite = numframes;
for (const auto &f : favoriteFrames) {
normalFrames[numframes++] = f;
indicatorIcons.push_back(icon_node);
// Insert favorite frames *after* collecting them all
if (!favoriteFrames.empty()) {
fsi.positions.firstFavorite = numframes;
for (const auto &f : favoriteFrames) {
normalFrames[numframes++] = f;
indicatorIcons.push_back(icon_node);
}
fsi.positions.lastFavorite = numframes - 1;
} else {
fsi.positions.firstFavorite = 255;
fsi.positions.lastFavorite = 255;
}
fsi.positions.lastFavorite = numframes - 1;
} else {
fsi.positions.firstFavorite = 255;
fsi.positions.lastFavorite = 255;
}
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
@@ -1070,7 +1162,7 @@ void Screen::setFrames(FrameFocus focus)
ui->switchToFrame(fsi.positions.clock);
break;
case FOCUS_SYSTEM:
ui->switchToFrame(fsi.positions.memory);
ui->switchToFrame(fsi.positions.system);
break;
case FOCUS_PRESERVE:
@@ -1098,30 +1190,101 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
setFastFramerate();
}
void Screen::toggleFrameVisibility(const std::string &frameName)
{
#ifndef USE_EINK
if (frameName == "nodelist") {
hiddenFrames.nodelist = !hiddenFrames.nodelist;
}
#endif
#ifdef USE_EINK
if (frameName == "nodelist_lastheard") {
hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard;
}
if (frameName == "nodelist_hopsignal") {
hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal;
}
if (frameName == "nodelist_distance") {
hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance;
}
#endif
#if HAS_GPS
if (frameName == "nodelist_bearings") {
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
}
if (frameName == "gps") {
hiddenFrames.gps = !hiddenFrames.gps;
}
#endif
if (frameName == "lora") {
hiddenFrames.lora = !hiddenFrames.lora;
}
if (frameName == "clock") {
hiddenFrames.clock = !hiddenFrames.clock;
}
if (frameName == "show_favorites") {
hiddenFrames.show_favorites = !hiddenFrames.show_favorites;
}
if (frameName == "chirpy") {
hiddenFrames.chirpy = !hiddenFrames.chirpy;
}
}
bool Screen::isFrameHidden(const std::string &frameName) const
{
#ifndef USE_EINK
if (frameName == "nodelist")
return hiddenFrames.nodelist;
#endif
#ifdef USE_EINK
if (frameName == "nodelist_lastheard")
return hiddenFrames.nodelist_lastheard;
if (frameName == "nodelist_hopsignal")
return hiddenFrames.nodelist_hopsignal;
if (frameName == "nodelist_distance")
return hiddenFrames.nodelist_distance;
#endif
#if HAS_GPS
if (frameName == "nodelist_bearings")
return hiddenFrames.nodelist_bearings;
if (frameName == "gps")
return hiddenFrames.gps;
#endif
if (frameName == "lora")
return hiddenFrames.lora;
if (frameName == "clock")
return hiddenFrames.clock;
if (frameName == "show_favorites")
return hiddenFrames.show_favorites;
if (frameName == "chirpy")
return hiddenFrames.chirpy;
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::dismissCurrentFrame()
void Screen::hideCurrentFrame()
{
uint8_t currentFrame = ui->getUiState()->currentFrame;
bool dismissed = false;
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
LOG_INFO("Dismiss 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("Dismiss Waypoint");
LOG_DEBUG("Hide Waypoint");
devicestate.has_rx_waypoint = false;
dismissedFrames.waypoint = true;
hiddenFrames.waypoint = true;
dismissed = true;
} else if (currentFrame == framesetInfo.positions.wifi) {
LOG_DEBUG("Dismiss WiFi Screen");
dismissedFrames.wifi = true;
LOG_DEBUG("Hide WiFi Screen");
hiddenFrames.wifi = true;
dismissed = true;
} else if (currentFrame == framesetInfo.positions.memory) {
LOG_INFO("Dismiss Memory");
dismissedFrames.memory = true;
} else if (currentFrame == framesetInfo.positions.lora) {
LOG_INFO("Hide LoRa");
hiddenFrames.lora = true;
dismissed = true;
}
@@ -1154,7 +1317,8 @@ void Screen::blink()
delay(50);
count = count - 1;
}
// The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in OLEDDisplay.
// The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in
// OLEDDisplay.
dispdev->setBrightness(brightness);
}
@@ -1277,7 +1441,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Outgoing message (likely sent from phone)
devicestate.has_rx_text_message = false;
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
dismissedFrames.textMessage = true;
hiddenFrames.textMessage = true;
hasUnreadMessage = false; // Clear unread state when user replies
setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list
@@ -1343,6 +1507,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Triggered by MeshModules
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
{
// Block UI frame events when virtual keyboard is active
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return 0;
}
if (showingNormalScreen) {
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
@@ -1365,6 +1534,16 @@ int Screen::handleInputEvent(const InputEvent *event)
if (!screenOn)
return 0;
// Handle text input notifications specially - pass input to virtual keyboard
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
NotificationRenderer::inEvent = *event;
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
setFastFramerate(); // Draw ASAP
ui->update();
return 0;
}
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
@@ -1402,7 +1581,7 @@ int Screen::handleInputEvent(const InputEvent *event)
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
menuHandler::homeBaseMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) {
menuHandler::systemBaseMenu();
#if HAS_GPS
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
@@ -1411,7 +1590,7 @@ int Screen::handleInputEvent(const InputEvent *event)
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
menuHandler::clockMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
menuHandler::LoraRegionPicker();
menuHandler::loraMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
if (devicestate.rx_text_message.from) {
menuHandler::messageResponseMenu();
@@ -1479,13 +1658,15 @@ bool shouldWakeOnReceivedMessage()
/*
The goal here is to determine when we do NOT wake up the screen on message received:
- Any ext. notifications are turned on
- If role is not client / client_mute
- If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE
- If the battery level is very low
*/
if (moduleConfig.external_notification.enabled) {
return false;
}
if (!meshtastic_Config_DeviceConfig_Role_CLIENT && !meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE) {
if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT,
meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN,
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
return false;
}
if (powerStatus && powerStatus->getBatteryChargePercent() < 10) {

View File

@@ -12,7 +12,7 @@
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
namespace graphics
{
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker };
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
struct BannerOverlayOptions {
const char *message;
@@ -259,15 +259,7 @@ class Screen : public concurrency::OSThread
void setup();
/// Turns the screen on/off. Optionally, pass a custom screensaver frame for E-Ink
void setOn(bool on, FrameCallback einkScreensaver = NULL)
{
if (!on)
// We handle off commands immediately, because they might be called because the CPU is shutting down
handleSetOn(false, einkScreensaver);
else
enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON});
}
void setOn(bool on, FrameCallback einkScreensaver = NULL);
/**
* Prepare the display for the unit going to the lowest power mode possible. Most screens will just
* poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code
@@ -315,6 +307,8 @@ class Screen : public concurrency::OSThread
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> textCallback);
void requestMenu(graphics::menuHandler::screenMenus menuToShow)
{
@@ -593,7 +587,11 @@ class Screen : public concurrency::OSThread
void setSSLFrames();
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
void dismissCurrentFrame();
void hideCurrentFrame();
// Menu-driven Show / Hide Toggle
void toggleFrameVisibility(const std::string &frameName);
bool isFrameHidden(const std::string &frameName) const;
#ifdef USE_EINK
/// Draw an image to remain on E-Ink display after screen off
@@ -655,7 +653,7 @@ class Screen : public concurrency::OSThread
uint8_t settings = 255;
uint8_t wifi = 255;
uint8_t deviceFocused = 255;
uint8_t memory = 255;
uint8_t system = 255;
uint8_t gps = 255;
uint8_t home = 255;
uint8_t textMessage = 255;
@@ -665,6 +663,7 @@ class Screen : public concurrency::OSThread
uint8_t nodelist_distance = 255;
uint8_t nodelist_bearings = 255;
uint8_t clock = 255;
uint8_t chirpy = 255;
uint8_t firstFavorite = 255;
uint8_t lastFavorite = 255;
uint8_t lora = 255;
@@ -673,12 +672,29 @@ class Screen : public concurrency::OSThread
uint8_t frameCount = 0;
} framesetInfo;
struct DismissedFrames {
struct hiddenFrames {
bool textMessage = false;
bool waypoint = false;
bool wifi = false;
bool memory = false;
} dismissedFrames;
bool system = false;
bool home = false;
bool clock = false;
#ifndef USE_EINK
bool nodelist = false;
#endif
#ifdef USE_EINK
bool nodelist_lastheard = false;
bool nodelist_hopsignal = false;
bool nodelist_distance = false;
#endif
#if HAS_GPS
bool nodelist_bearings = false;
bool gps = false;
#endif
bool lora = false;
bool show_favorites = false;
bool chirpy = true;
} hiddenFrames;
/// Try to start drawing ASAP
void setFastFramerate();

View File

@@ -1,6 +1,7 @@
#include "graphics/SharedUIDisplay.h"
#include "RTC.h"
#include "graphics/ScreenFonts.h"
#include "graphics/draw/UIRenderer.h"
#include "main.h"
#include "meshtastic/config.pb.h"
#include "power.h"
@@ -16,6 +17,10 @@ void determineResolution(int16_t screenheight, int16_t screenwidth)
isHighResolution = true;
}
if (screenwidth > 128 && screenheight <= 64) {
isHighResolution = false;
}
// Special case for Heltec Wireless Tracker v1.1
if (screenwidth == 160 && screenheight == 80) {
isHighResolution = false;
@@ -53,7 +58,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
// *************************
// * Common Header Drawing *
// *************************
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only)
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date)
{
constexpr int HEADER_OFFSET_Y = 1;
y += HEADER_OFFSET_Y;
@@ -69,7 +74,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
const int screenW = display->getWidth();
const int screenH = display->getHeight();
if (!battery_only) {
if (!force_no_invert) {
// === Inverted Header Background ===
if (isInverted) {
display->setColor(BLACK);
@@ -187,13 +192,28 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
int timeX = screenW - xOffset - timeStrWidth + 4;
if (rtc_sec > 0 && !battery_only) {
if (rtc_sec > 0) {
// === Build Time String ===
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
int hour = hms / SEC_PER_HOUR;
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
// === Build Date String ===
char datetimeStr[25];
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false);
char dateLine[40];
if (isHighResolution) {
snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr);
} else {
if (hasUnreadMessage) {
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]);
} else {
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]);
}
}
if (config.display.use_12h_clock) {
bool isPM = hour >= 12;
hour %= 12;
@@ -202,7 +222,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
}
timeStrWidth = display->getStringWidth(timeStr);
if (show_date) {
timeStrWidth = display->getStringWidth(dateLine);
} else {
timeStrWidth = display->getStringWidth(timeStr);
}
timeX = screenW - xOffset - timeStrWidth + 3;
// === Show Mail or Mute Icon to the Left of Time ===
@@ -229,7 +253,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int iconW = 16, iconH = 12;
int iconX = iconRightEdge - iconW;
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
if (isInverted) {
if (isInverted && !force_no_invert) {
display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
display->setColor(BLACK);
@@ -244,7 +268,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
} else {
int iconX = iconRightEdge - (mail_width - 2);
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
if (isInverted) {
if (isInverted && !force_no_invert) {
display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
display->setColor(BLACK);
@@ -260,7 +284,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
if (isInverted) {
if (isInverted && !force_no_invert) {
display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
display->setColor(BLACK);
@@ -287,10 +311,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
}
}
// === Draw Time ===
display->drawString(timeX, textY, timeStr);
if (isBold)
display->drawString(timeX - 1, textY, timeStr);
if (show_date) {
// === Draw Date ===
display->drawString(timeX, textY, dateLine);
if (isBold)
display->drawString(timeX - 1, textY, dateLine);
} else {
// === Draw Time ===
display->drawString(timeX, textY, timeStr);
if (isBold)
display->drawString(timeX - 1, textY, timeStr);
}
} else {
// === No Time Available: Mail/Mute Icon Moves to Far Right ===

View File

@@ -49,7 +49,8 @@ void determineResolution(int16_t screenheight, int16_t screenwidth);
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
// Shared battery/time/mail header
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false);
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false,
bool show_date = false);
const int *getTextPositions(OLEDDisplay *display);

View File

@@ -767,24 +767,24 @@ class LGFX : public lgfx::LGFX_Device
LGFX(void)
{
if (settingsMap[displayPanel] == st7789)
if (portduino_config.displayPanel == st7789)
_panel_instance = new lgfx::Panel_ST7789;
else if (settingsMap[displayPanel] == st7735)
else if (portduino_config.displayPanel == st7735)
_panel_instance = new lgfx::Panel_ST7735;
else if (settingsMap[displayPanel] == st7735s)
else if (portduino_config.displayPanel == st7735s)
_panel_instance = new lgfx::Panel_ST7735S;
else if (settingsMap[displayPanel] == st7796)
else if (portduino_config.displayPanel == st7796)
_panel_instance = new lgfx::Panel_ST7796;
else if (settingsMap[displayPanel] == ili9341)
else if (portduino_config.displayPanel == ili9341)
_panel_instance = new lgfx::Panel_ILI9341;
else if (settingsMap[displayPanel] == ili9342)
else if (portduino_config.displayPanel == ili9342)
_panel_instance = new lgfx::Panel_ILI9342;
else if (settingsMap[displayPanel] == ili9488)
else if (portduino_config.displayPanel == ili9488)
_panel_instance = new lgfx::Panel_ILI9488;
else if (settingsMap[displayPanel] == hx8357d)
else if (portduino_config.displayPanel == hx8357d)
_panel_instance = new lgfx::Panel_HX8357D;
#if defined(LGFX_SDL)
else if (settingsMap[displayPanel] == x11) {
else if (portduino_config.displayPanel == x11) {
_panel_instance = new lgfx::Panel_sdl;
}
#endif
@@ -795,61 +795,61 @@ class LGFX : public lgfx::LGFX_Device
auto buscfg = _bus_instance.config();
buscfg.spi_mode = 0;
buscfg.spi_host = settingsMap[displayspidev];
buscfg.spi_host = portduino_config.display_spi_dev_int;
buscfg.pin_dc = settingsMap[displayDC]; // Set SPI DC pin number (-1 = disable)
buscfg.pin_dc = portduino_config.displayDC.pin; // Set SPI DC pin number (-1 = disable)
_bus_instance.config(buscfg); // applies the set value to the bus.
_panel_instance->setBus(&_bus_instance); // set the bus on the panel.
auto cfg = _panel_instance->config(); // Gets a structure for display panel settings.
LOG_DEBUG("Width: %d, Height: %d", settingsMap[displayWidth], settingsMap[displayHeight]);
cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable)
cfg.pin_rst = settingsMap[displayReset];
if (settingsMap[displayRotate]) {
cfg.panel_width = settingsMap[displayHeight]; // actual displayable width
cfg.panel_height = settingsMap[displayWidth]; // actual displayable height
LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight);
cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable)
cfg.pin_rst = portduino_config.displayReset.pin;
if (portduino_config.displayRotate) {
cfg.panel_width = portduino_config.displayHeight; // actual displayable width
cfg.panel_height = portduino_config.displayWidth; // actual displayable height
} else {
cfg.panel_width = settingsMap[displayWidth]; // actual displayable width
cfg.panel_height = settingsMap[displayHeight]; // actual displayable height
cfg.panel_width = portduino_config.displayWidth; // actual displayable width
cfg.panel_height = portduino_config.displayHeight; // actual displayable height
}
cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction
cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction
cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored)
cfg.invert = settingsMap[displayInvert]; // Set to true if the light/darkness of the panel is reversed
cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction
cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction
cfg.offset_rotation = portduino_config.displayOffsetRotate; // Rotation direction value offset 0~7 (4~7 is mirrored)
cfg.invert = portduino_config.displayInvert; // Set to true if the light/darkness of the panel is reversed
_panel_instance->config(cfg);
// Configure settings for touch control.
if (settingsMap[touchscreenModule]) {
if (settingsMap[touchscreenModule] == xpt2046) {
if (portduino_config.touchscreenModule) {
if (portduino_config.touchscreenModule == xpt2046) {
_touch_instance = new lgfx::Touch_XPT2046;
} else if (settingsMap[touchscreenModule] == stmpe610) {
} else if (portduino_config.touchscreenModule == stmpe610) {
_touch_instance = new lgfx::Touch_STMPE610;
} else if (settingsMap[touchscreenModule] == ft5x06) {
} else if (portduino_config.touchscreenModule == ft5x06) {
_touch_instance = new lgfx::Touch_FT5x06;
}
auto touch_cfg = _touch_instance->config();
touch_cfg.pin_cs = settingsMap[touchscreenCS];
touch_cfg.pin_cs = portduino_config.touchscreenCS.pin;
touch_cfg.x_min = 0;
touch_cfg.x_max = settingsMap[displayHeight] - 1;
touch_cfg.x_max = portduino_config.displayHeight - 1;
touch_cfg.y_min = 0;
touch_cfg.y_max = settingsMap[displayWidth] - 1;
touch_cfg.pin_int = settingsMap[touchscreenIRQ];
touch_cfg.y_max = portduino_config.displayWidth - 1;
touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin;
touch_cfg.bus_shared = true;
touch_cfg.offset_rotation = settingsMap[touchscreenRotate];
if (settingsMap[touchscreenI2CAddr] != -1) {
touch_cfg.i2c_addr = settingsMap[touchscreenI2CAddr];
touch_cfg.offset_rotation = portduino_config.touchscreenRotate;
if (portduino_config.touchscreenI2CAddr != -1) {
touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr;
} else {
touch_cfg.spi_host = settingsMap[touchscreenspidev];
touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int;
}
_touch_instance->config(touch_cfg);
_panel_instance->setTouch(_touch_instance);
}
#if defined(LGFX_SDL)
if (settingsMap[displayPanel] == x11) {
if (portduino_config.displayPanel == x11) {
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance;
sdl_panel_->setup();
sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER);
@@ -1115,10 +1115,10 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g
backlightEnable = p;
#if ARCH_PORTDUINO
if (settingsMap[displayRotate]) {
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]);
if (portduino_config.displayRotate) {
setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth);
} else {
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]);
setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight);
}
#elif defined(SCREEN_ROTATE)
@@ -1240,7 +1240,7 @@ void TFTDisplay::sdlLoop()
#if defined(LGFX_SDL)
static int lastPressed = 0;
static int shuttingDown = false;
if (settingsMap[displayPanel] == x11) {
if (portduino_config.displayPanel == x11) {
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance;
if (sdl_panel_->loop() && !shuttingDown) {
LOG_WARN("Window Closed!");
@@ -1288,8 +1288,8 @@ void TFTDisplay::sendCommand(uint8_t com)
backlightEnable->set(true);
#if ARCH_PORTDUINO
display(true);
if (settingsMap[displayBacklight] > 0)
digitalWrite(settingsMap[displayBacklight], TFT_BACKLIGHT_ON);
if (portduino_config.displayBacklight.pin > 0)
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->wakeup();
tft->powerSaveOff();
@@ -1312,8 +1312,8 @@ void TFTDisplay::sendCommand(uint8_t com)
backlightEnable->set(false);
#if ARCH_PORTDUINO
tft->clear();
if (settingsMap[displayBacklight] > 0)
digitalWrite(settingsMap[displayBacklight], !TFT_BACKLIGHT_ON);
if (portduino_config.displayBacklight.pin > 0)
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->sleep();
tft->powerSaveOn();

View File

@@ -0,0 +1,738 @@
#include "VirtualKeyboard.h"
#include "configuration.h"
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "main.h"
#include <Arduino.h>
#include <vector>
namespace graphics
{
VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis())
{
initializeKeyboard();
// Set cursor to H(2, 5)
cursorRow = 2;
cursorCol = 5;
}
VirtualKeyboard::~VirtualKeyboard() {}
void VirtualKeyboard::initializeKeyboard()
{
// New 4 row, 11 column keyboard layout:
static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'},
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'},
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '},
{'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}};
// Derive layout dimensions and assert they match the configured keyboard grid
constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0]));
constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0]));
static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS");
static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS");
// Initialize all keys to empty first
for (int row = 0; row < LAYOUT_ROWS; row++) {
for (int col = 0; col < LAYOUT_COLS; col++) {
keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0};
}
}
// Fill keyboard from the 2D layout
for (int row = 0; row < LAYOUT_ROWS; row++) {
for (int col = 0; col < LAYOUT_COLS; col++) {
char ch = LAYOUT[row][col];
// No empty slots in the simplified layout
VirtualKeyType type = VK_CHAR;
if (ch == '\b') {
type = VK_BACKSPACE;
} else if (ch == '\n') {
type = VK_ENTER;
} else if (ch == '\x1b') { // ESC
type = VK_ESC;
} else if (ch == ' ') {
type = VK_SPACE;
}
// Make action keys wider to fit text while keeping the last column aligned
uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH;
keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT};
}
}
}
void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY)
{
// Repeat ticking is driven by NotificationRenderer once per frame
// Base styles
display->setColor(WHITE);
display->setFont(FONT_SMALL);
// Screen geometry
const int screenW = display->getWidth();
const int screenH = display->getHeight();
// Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels
// Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide
const bool isWide = screenW >= 200;
// Determine last-column label max width
display->setFont(FONT_SMALL);
const int wENTER = display->getStringWidth("ENTER");
int lastColLabelW = wENTER; // ENTER is usually the widest
// Smaller padding on very small screens to avoid excessive whitespace
const int lastColPad = (screenW <= 128 ? 2 : 6);
const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys
// Always reserve width for the rightmost text column to avoid overlap on small screens
int cellW = 0;
int leftoverW = 0;
{
const int leftCols = KEYBOARD_COLS - 1; // 10 input characters
int usableW = screenW - reservedLastColW;
if (usableW < leftCols) {
// Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely)
usableW = leftCols;
}
cellW = usableW / leftCols;
leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right)
}
// Dynamic key geometry
int cellH = KEY_HEIGHT;
int keyboardStartY = 0;
if (screenH <= 64) {
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2);
const int gapBelowHeader = 0;
const int singleLineBoxHeight = FONT_HEIGHT_SMALL;
const int gapAboveKeyboard = 0;
keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard;
if (keyboardStartY < 0)
keyboardStartY = 0;
if (keyboardStartY > screenH)
keyboardStartY = screenH;
int keyboardHeight = screenH - keyboardStartY;
cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS);
} else if (isWide) {
// For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width.
cellH = std::max((int)KEY_HEIGHT, cellW);
// Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed.
// Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1
display->setFont(FONT_SMALL);
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1);
const int headerToBoxGap = 1;
const int gapAboveKb = 1;
const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom
int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb);
int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS;
if (maxCellHAllowed < (int)KEY_HEIGHT)
maxCellHAllowed = KEY_HEIGHT;
if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) {
cellH = maxCellHAllowed;
}
// Keyboard placement from bottom for wide screens
int keyboardHeight = KEYBOARD_ROWS * cellH;
keyboardStartY = screenH - keyboardHeight;
if (keyboardStartY < 0)
keyboardStartY = 0;
} else {
// Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom
cellH = KEY_HEIGHT;
int keyboardHeight = KEYBOARD_ROWS * cellH;
keyboardStartY = screenH - keyboardHeight;
if (keyboardStartY < 0)
keyboardStartY = 0;
}
// Draw input area above keyboard
drawInputArea(display, offsetX, offsetY, keyboardStartY);
// Precompute per-column x and width with leftover distributed over left columns for even spacing
int colX[KEYBOARD_COLS];
int colW[KEYBOARD_COLS];
int runningX = offsetX;
for (int col = 0; col < KEYBOARD_COLS - 1; ++col) {
int wcol = cellW + (col < leftoverW ? 1 : 0);
colX[col] = runningX;
colW[col] = wcol;
runningX += wcol;
}
// Last column
colX[KEYBOARD_COLS - 1] = runningX;
colW[KEYBOARD_COLS - 1] = reservedLastColW;
// Draw keyboard grid
for (int row = 0; row < KEYBOARD_ROWS; row++) {
for (int col = 0; col < KEYBOARD_COLS; col++) {
const VirtualKey &k = keyboard[row][col];
if (k.character != 0 || k.type != VK_CHAR) {
const bool isLastCol = (col == KEYBOARD_COLS - 1);
int x = colX[col];
int w = colW[col];
int y = offsetY + keyboardStartY + row * cellH;
int h = cellH;
bool selected = (row == cursorRow && col == cursorCol);
drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol);
}
}
}
}
void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY)
{
display->setColor(WHITE);
const int screenWidth = display->getWidth();
const int screenHeight = display->getHeight();
// Use the standard small font metrics for input box sizing (restore original size)
const int inputLineH = FONT_HEIGHT_SMALL;
// Header uses the standard small (which may be larger on big screens)
display->setFont(FONT_SMALL);
int headerHeight = 0;
if (!headerText.empty()) {
// Draw header and reserve exact font height (plus a tighter gap) to maximize input area
display->drawString(offsetX + 2, offsetY, headerText.c_str());
if (screenHeight <= 64) {
headerHeight = FONT_HEIGHT_SMALL - 2; // 11px
} else {
headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in
}
}
const int boxX = offsetX;
const int boxWidth = screenWidth;
int boxY;
int boxHeight;
if (screenHeight <= 64) {
const int gapBelowHeader = 0;
const int fixedBoxHeight = inputLineH;
const int gapAboveKeyboard = 0;
boxY = offsetY + headerHeight + gapBelowHeader;
boxHeight = fixedBoxHeight;
if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) {
int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY;
boxHeight = std::max(1, fixedBoxHeight - over);
}
} else {
const int gapBelowHeader = 1;
int gapAboveKeyboard = 1;
int tmpBoxY = offsetY + headerHeight + gapBelowHeader;
const int minBoxHeight = inputLineH + 2;
int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard;
if (availableH < minBoxHeight)
availableH = minBoxHeight;
boxY = tmpBoxY;
boxHeight = availableH;
}
// Draw box border
display->drawRect(boxX, boxY, boxWidth, boxHeight);
display->setFont(FONT_SMALL);
// Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis
const int textX = boxX + 2;
const int maxTextWidth = boxWidth - 4;
const int maxLines = (boxHeight - 2) / inputLineH;
if (maxLines >= 2) {
// Inner bounds for caret clamping
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;
// Wrap text greedily into lines that fit maxTextWidth
std::vector<std::string> lines;
{
std::string remaining = inputText;
while (!remaining.empty()) {
int bestLen = 0;
for (int len = 1; len <= (int)remaining.size(); ++len) {
int w = display->getStringWidth(remaining.substr(0, len).c_str());
if (w <= maxTextWidth)
bestLen = len;
else
break;
}
if (bestLen == 0) {
// At least show one character to make progress
bestLen = 1;
}
lines.emplace_back(remaining.substr(0, bestLen));
remaining.erase(0, bestLen);
}
}
const bool scrolledUp = ((int)lines.size() > maxLines);
int caretX = textX;
int caretY = innerTop;
// Leave a small top gap to render '...' without replacing the first line
const int topInset = 2;
const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height
int lineY = innerTop + topInset;
if (scrolledUp) {
// Draw three small dots centered horizontally, vertically at the midpoint of the gap
// between the inner top and the first line's top baseline. This avoids using a tall glyph.
const int firstLineTop = lineY; // baseline top for the first visible line
const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested
const int centerX = boxX + boxWidth / 2;
const int dotSpacing = 3; // px between dots
const int dotSize = 1; // small square dot
display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize);
display->fillRect(centerX, gapMidY, dotSize, dotSize);
display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize);
}
// How many lines fit with our top inset and tighter step
const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep);
const int linesToShow = std::min((int)lines.size(), linesCapacity);
const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0;
for (int i = 0; i < linesToShow; ++i) {
const std::string &chunk = lines[startIndex + i];
display->drawString(textX, lineY, chunk.c_str());
caretX = textX + display->getStringWidth(chunk.c_str());
caretY = lineY;
lineY += lineStep;
}
// Draw caret at end of the last visible line
int caretPadY = 2;
if (boxHeight >= inputLineH + 4)
caretPadY = 3;
int cursorTop = caretY + caretPadY;
// Use lineStep so caret height matches the row spacing
int cursorH = lineStep - caretPadY * 2;
if (cursorH < 1)
cursorH = 1;
// Clamp vertical bounds to stay inside the inner rect
if (cursorTop < innerTop)
cursorTop = innerTop;
if (cursorTop + cursorH - 1 > innerBottom)
cursorH = innerBottom - cursorTop + 1;
if (cursorH < 1)
cursorH = 1;
// Only draw if cursor is inside inner bounds
if (caretX >= innerLeft && caretX <= innerRight) {
display->drawVerticalLine(caretX, cursorTop, cursorH);
}
} else {
std::string displayText = inputText;
int textW = display->getStringWidth(displayText.c_str());
std::string scrolled = displayText;
if (textW > maxTextWidth) {
// Trim from the left until it fits
while (textW > maxTextWidth && !scrolled.empty()) {
scrolled.erase(0, 1);
textW = display->getStringWidth(scrolled.c_str());
}
// Add leading ellipsis and ensure it still fits
if (scrolled != displayText) {
scrolled = "..." + scrolled;
textW = display->getStringWidth(scrolled.c_str());
// If adding ellipsis causes overflow, trim more after the ellipsis
while (textW > maxTextWidth && scrolled.size() > 3) {
scrolled.erase(3, 1); // remove chars after the ellipsis
textW = display->getStringWidth(scrolled.c_str());
}
}
} else {
// Keep textW in sync with what we draw
textW = display->getStringWidth(scrolled.c_str());
}
int textY;
if (screenHeight <= 64) {
textY = boxY + (boxHeight - inputLineH) / 2;
} else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;
// Center text vertically within inner box for single-line, then clamp so it never overlaps borders
int innerH = innerBottom - innerTop + 1;
textY = innerTop + std::max(0, (innerH - inputLineH) / 2);
// Clamp fully inside the inner rect
if (textY < innerTop)
textY = innerTop;
int maxTop = innerBottom - inputLineH + 1;
if (textY > maxTop)
textY = maxTop;
}
if (!scrolled.empty()) {
display->drawString(textX, textY, scrolled.c_str());
}
int cursorX = textX + textW;
if (screenHeight > 64) {
const int innerRight = boxX + boxWidth - 2;
if (cursorX > innerRight)
cursorX = innerRight;
}
int cursorTop, cursorH;
if (screenHeight <= 64) {
cursorH = 10;
cursorTop = boxY + (boxHeight - cursorH) / 2;
} else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;
cursorTop = boxY + 2;
cursorH = boxHeight - 4;
if (cursorH < 1)
cursorH = 1;
if (cursorTop < innerTop)
cursorTop = innerTop;
if (cursorTop + cursorH - 1 > innerBottom)
cursorH = innerBottom - cursorTop + 1;
if (cursorH < 1)
cursorH = 1;
if (cursorX < innerLeft || cursorX > innerRight)
return;
}
display->drawVerticalLine(cursorX, cursorTop, cursorH);
}
}
void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width,
uint8_t height, bool isLastCol)
{
// Draw key content
display->setFont(FONT_SMALL);
const int fontH = FONT_HEIGHT_SMALL;
// Build label and metrics first
std::string keyText;
if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) {
// Keep literal text labels for the action keys on the rightmost column
keyText = (key.type == VK_BACKSPACE) ? "BACK"
: (key.type == VK_ENTER) ? "ENTER"
: (key.type == VK_SPACE) ? "SPACE"
: (key.type == VK_ESC) ? "ESC"
: "";
} else {
char c = getCharForKey(key, false);
if (c >= 'a' && c <= 'z') {
c = c - 'a' + 'A';
}
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
}
int textWidth = display->getStringWidth(keyText.c_str());
// Label alignment
// - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly.
// - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths.
int textX;
if (isLastCol) {
const int rightPad = 1;
textX = x + width - textWidth - rightPad;
if (textX < x)
textX = x; // guard
} else {
if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) {
textX = x + (width - textWidth + 1) / 2;
} else {
textX = x + (width - textWidth) / 2;
}
}
int contentTop = y;
int contentH = height;
if (selected) {
display->setColor(WHITE);
bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC);
if (display->getHeight() <= 64 && !isAction) {
display->fillRect(x, y, width, height);
} else if (isAction) {
const int padX = 1;
const int padY = 2;
int hlW = textWidth + padX * 2;
int hlX = textX - padX;
if (hlX < x) {
hlW -= (x - hlX);
hlX = x;
}
int maxW = (x + width) - hlX;
if (hlW > maxW)
hlW = maxW;
if (hlW < 1)
hlW = 1;
int hlH = std::min(fontH + padY * 2, (int)height);
int hlY = y + (height - hlH) / 2;
display->fillRect(hlX, hlY, hlW, hlH);
contentTop = hlY;
contentH = hlH;
} else {
display->fillRect(x, y, width, height);
}
display->setColor(BLACK);
} else {
display->setColor(WHITE);
}
int centeredTextY;
if (display->getHeight() <= 64) {
centeredTextY = y + (height - fontH) / 2;
} else {
centeredTextY = contentTop + (contentH - fontH) / 2;
}
if (display->getHeight() > 64) {
if (centeredTextY < contentTop)
centeredTextY = contentTop;
if (centeredTextY + fontH > contentTop + contentH)
centeredTextY = std::max(contentTop, contentTop + contentH - fontH);
}
if (display->getHeight() <= 64 && keyText.size() == 1) {
char ch = keyText[0];
if (ch == '.' || ch == ',' || ch == ';') {
centeredTextY -= 1;
}
}
display->drawString(textX, centeredTextY, keyText.c_str());
}
char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
{
if (key.type != VK_CHAR) {
return key.character;
}
char c = key.character;
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
if (isLongPress && c >= 'a' && c <= 'z') {
c = (char)(c - 'a' + 'A');
}
return c;
}
void VirtualKeyboard::moveCursorDelta(int dRow, int dCol)
{
resetTimeout();
// wrap around rows and cols in the 4x11 grid
int r = (int)cursorRow + dRow;
int c = (int)cursorCol + dCol;
if (r < 0)
r = KEYBOARD_ROWS - 1;
else if (r >= KEYBOARD_ROWS)
r = 0;
if (c < 0)
c = KEYBOARD_COLS - 1;
else if (c >= KEYBOARD_COLS)
c = 0;
cursorRow = (uint8_t)r;
cursorCol = (uint8_t)c;
}
void VirtualKeyboard::moveCursorUp()
{
moveCursorDelta(-1, 0);
}
void VirtualKeyboard::moveCursorDown()
{
moveCursorDelta(1, 0);
}
void VirtualKeyboard::moveCursorLeft()
{
resetTimeout();
if (cursorCol > 0) {
cursorCol--;
} else {
if (cursorRow > 0) {
cursorRow--;
cursorCol = KEYBOARD_COLS - 1;
} else {
cursorRow = KEYBOARD_ROWS - 1;
cursorCol = KEYBOARD_COLS - 1;
}
}
}
void VirtualKeyboard::moveCursorRight()
{
resetTimeout();
if (cursorCol < KEYBOARD_COLS - 1) {
cursorCol++;
} else {
if (cursorRow < KEYBOARD_ROWS - 1) {
cursorRow++;
cursorCol = 0;
} else {
cursorRow = 0;
cursorCol = 0;
}
}
}
void VirtualKeyboard::handlePress()
{
resetTimeout(); // Reset timeout on any input activity
const VirtualKey &key = keyboard[cursorRow][cursorCol];
// Don't handle press if the key is empty (but allow special keys)
if (key.character == 0 && key.type == VK_CHAR) {
return;
}
// For character keys, insert lowercase character
if (key.type == VK_CHAR) {
insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char
return;
}
// Handle non-character keys immediately
switch (key.type) {
case VK_BACKSPACE:
deleteCharacter();
break;
case VK_ENTER:
submitText();
break;
case VK_SPACE:
insertCharacter(' ');
break;
case VK_ESC:
if (onTextEntered) {
std::function<void(const std::string &)> callback = onTextEntered;
onTextEntered = nullptr;
inputText = "";
callback("");
}
return;
default:
break;
}
}
void VirtualKeyboard::handleLongPress()
{
resetTimeout(); // Reset timeout on any input activity
const VirtualKey &key = keyboard[cursorRow][cursorCol];
// Don't handle press if the key is empty (but allow special keys)
if (key.character == 0 && key.type == VK_CHAR) {
return;
}
// For character keys, insert uppercase/alternate character
if (key.type == VK_CHAR) {
insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char
return;
}
switch (key.type) {
case VK_BACKSPACE:
// One-shot: delete up to 5 characters on long press
for (int i = 0; i < 5; ++i) {
if (inputText.empty())
break;
deleteCharacter();
}
break;
case VK_ENTER:
submitText();
break;
case VK_SPACE:
insertCharacter(' ');
break;
case VK_ESC:
if (onTextEntered) {
onTextEntered("");
}
break;
default:
break;
}
}
void VirtualKeyboard::insertCharacter(char c)
{
if (inputText.length() < 160) { // Reasonable text length limit
inputText += c;
}
}
void VirtualKeyboard::deleteCharacter()
{
if (!inputText.empty()) {
inputText.pop_back();
}
}
void VirtualKeyboard::submitText()
{
LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str());
// Only submit if text is not empty
if (!inputText.empty() && onTextEntered) {
// Store callback and text to submit before clearing callback
std::function<void(const std::string &)> callback = onTextEntered;
std::string textToSubmit = inputText;
onTextEntered = nullptr;
// Don't clear inputText here - let the calling module handle cleanup
// inputText = ""; // Removed: keep text visible until module cleans up
callback(textToSubmit);
} else if (inputText.empty()) {
// For empty text, just ignore the submission - don't clear callback
// This keeps the virtual keyboard responsive for further input
LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active");
} else {
// No callback available
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
}
}
void VirtualKeyboard::setInputText(const std::string &text)
{
inputText = text;
}
std::string VirtualKeyboard::getInputText() const
{
return inputText;
}
void VirtualKeyboard::setHeader(const std::string &header)
{
headerText = header;
}
void VirtualKeyboard::setCallback(std::function<void(const std::string &)> callback)
{
onTextEntered = callback;
}
void VirtualKeyboard::resetTimeout()
{
lastActivityTime = millis();
}
bool VirtualKeyboard::isTimedOut() const
{
return (millis() - lastActivityTime) > TIMEOUT_MS;
}
} // namespace graphics

View File

@@ -0,0 +1,80 @@
#pragma once
#include "configuration.h"
#include <OLEDDisplay.h>
#include <functional>
#include <string>
namespace graphics
{
enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE };
struct VirtualKey {
char character;
VirtualKeyType type;
uint8_t x;
uint8_t y;
uint8_t width;
uint8_t height;
};
class VirtualKeyboard
{
public:
VirtualKeyboard();
~VirtualKeyboard();
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
void setInputText(const std::string &text);
std::string getInputText() const;
void setHeader(const std::string &header);
void setCallback(std::function<void(const std::string &)> callback);
// Navigation methods for encoder input
void moveCursorUp();
void moveCursorDown();
void moveCursorLeft();
void moveCursorRight();
void handlePress();
void handleLongPress();
// Timeout management
void resetTimeout();
bool isTimedOut() const;
private:
static const uint8_t KEYBOARD_ROWS = 4;
static const uint8_t KEYBOARD_COLS = 11;
static const uint8_t KEY_WIDTH = 9;
static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays
static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom
VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS];
std::string inputText;
std::string headerText;
std::function<void(const std::string &)> onTextEntered;
uint8_t cursorRow;
uint8_t cursorCol;
// Timeout management for auto-exit
uint32_t lastActivityTime;
static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout
void initializeKeyboard();
void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h,
bool isLastCol);
void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY);
// Unified cursor movement helper
void moveCursorDelta(int dRow, int dCol);
char getCharForKey(const VirtualKey &key, bool isLongPress = false);
void insertCharacter(char c);
void deleteCharacter();
void submitText();
};
} // namespace graphics

View File

@@ -8,6 +8,7 @@
#include "gps/RTC.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h"
#include "graphics/emotes.h"
#include "graphics/images.h"
#include "main.h"
@@ -190,7 +191,8 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
// === Set Title, Blank for Clock
const char *titleStr = "";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true);
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
#ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
@@ -294,6 +296,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
isPM ? "pm" : "am");
}
#ifndef USE_EINK
xOffset = (isHighResolution) ? 18 : 10;
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
@@ -313,7 +316,8 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// === Set Title, Blank for Clock
const char *titleStr = "";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true);
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
#ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {

View File

@@ -330,8 +330,7 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
#if HAS_GPS
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
// Line 3
if (config.display.gps_format !=
meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude
UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
// Line 4
@@ -395,8 +394,18 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int textWidth = display->getStringWidth(shortnameble);
int nameX = (SCREEN_WIDTH - textWidth);
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
// === Second Row: Radio Preset ===
// === Second Row: Role ===
auto role = DisplayFormatters::getDeviceRole(config.device.role);
char device_role[25];
snprintf(device_role, sizeof(device_role), "Role: %s", role);
textWidth = display->getStringWidth(device_role);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], device_role);
// === Third Row: Radio Preset ===
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
char regionradiopreset[25];
const char *region = myRegion ? myRegion->name : NULL;
if (region != nullptr) {
@@ -410,7 +419,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset);
// === Third Row: Frequency / ChanNum ===
// === Fourth Row: Frequency / ChanNum ===
char frequencyslot[35];
char freqStr[16];
float freq = RadioLibInterface::instance->getFreq();
@@ -437,7 +446,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
#if !defined(M5STACK_UNITC6L)
// === Fourth Row: Channel Utilization ===
// === Fifth Row: Channel Utilization ===
const char *chUtil = "ChUtil:";
char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
@@ -454,7 +463,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
int starting_position = centerofscreen - total_line_content_width;
display->drawString(starting_position, getTextPositions(display)[line++], chUtil);
display->drawString(starting_position, getTextPositions(display)[line], chUtil);
// Force 56% or higher to show a full 100% bar, text would still show related percent.
if (chutil_percent >= 61) {
@@ -491,7 +500,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
}
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4],
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++],
chUtilPercentage);
#endif
}
@@ -499,7 +508,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
// ****************************
// * System Screen *
// ****************************
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear();
display->setFont(FONT_SMALL);
@@ -655,6 +664,34 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
}
#endif
}
// ****************************
// * Chirpy Screen *
// ****************************
void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
int line = 1;
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
int textX_offset = 10;
if (isHighResolution) {
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
textX_offset = textX_offset * 4;
display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
} else {
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
}
int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2);
display->drawString(textX, getTextPositions(display)[line++], "Hello");
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
display->drawString(textX, getTextPositions(display)[line++], "World!");
}
} // namespace DebugRenderer
} // namespace graphics
#endif

View File

@@ -31,8 +31,10 @@ void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state
// LoRa information display
void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Memory screen display
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// System screen display
void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Chirpy screen display
void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
} // namespace DebugRenderer
} // namespace graphics

View File

@@ -10,7 +10,10 @@
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#include "main.h"
#include "mesh/MeshTypes.h"
#include "modules/AdminModule.h"
#include "modules/CannedMessageModule.h"
#include "modules/KeyVerificationModule.h"
@@ -26,6 +29,28 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none;
bool test_enabled = false;
uint8_t test_count = 0;
void menuHandler::loraMenu()
{
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"};
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "LoRa Actions";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 4;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
// No action
} else if (selected == device_role_picker) {
menuHandler::menuQueue = menuHandler::device_role_picker;
} else if (selected == radio_preset_picker) {
menuHandler::menuQueue = menuHandler::radio_preset_picker;
} else if (selected == lora_picker) {
menuHandler::menuQueue = menuHandler::lora_picker;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::OnboardMessage()
{
static const char *optionsArray[] = {"OK", "Got it!"};
@@ -123,6 +148,87 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::DeviceRolePicker()
{
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
enum optionsNumbers {
Back = 0,
devicerole_client = 1,
devicerole_clientmute = 2,
devicerole_lostandfound = 3,
devicerole_tracker = 4
};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Device Role";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 5;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
screen->runNow();
return;
} else if (selected == devicerole_client) {
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
} else if (selected == devicerole_clientmute) {
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE;
} else if (selected == devicerole_lostandfound) {
config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND;
} else if (selected == devicerole_tracker) {
config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER;
}
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::RadioPresetPicker()
{
static const char *optionsArray[] = {"Back", "LongSlow", "LongModerate", "LongFast", "MediumSlow",
"MediumFast", "ShortSlow", "ShortFast", "ShortTurbo"};
enum optionsNumbers {
Back = 0,
radiopreset_LongSlow = 1,
radiopreset_LongModerate = 2,
radiopreset_LongFast = 3,
radiopreset_MediumSlow = 4,
radiopreset_MediumFast = 5,
radiopreset_ShortSlow = 6,
radiopreset_ShortFast = 7,
radiopreset_ShortTurbo = 8
};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Radio Preset";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 9;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
screen->runNow();
return;
} else if (selected == radiopreset_LongSlow) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW;
} else if (selected == radiopreset_LongModerate) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE;
} else if (selected == radiopreset_LongFast) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
} else if (selected == radiopreset_MediumSlow) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW;
} else if (selected == radiopreset_MediumFast) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
} else if (selected == radiopreset_ShortSlow) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW;
} else if (selected == radiopreset_ShortFast) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST;
} else if (selected == radiopreset_ShortTurbo) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
}
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::TwelveHourPicker()
{
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
@@ -320,7 +426,7 @@ void menuHandler::messageResponseMenu()
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Dismiss) {
screen->dismissCurrentFrame();
screen->hideCurrentFrame();
} else if (selected == Preset) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
@@ -361,8 +467,11 @@ void menuHandler::homeBaseMenu()
optionsArray[options] = "Sleep Screen";
optionsEnumArray[options++] = Sleep;
#endif
optionsArray[options] = "Send Position";
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
optionsArray[options] = "Send Position";
} else {
optionsArray[options] = "Send Node Info";
}
optionsEnumArray[options++] = Position;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "New Preset";
@@ -455,7 +564,7 @@ void menuHandler::textMessageBaseMenu()
void menuHandler::systemBaseMenu()
{
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, Test, enumEnd };
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, FrameToggles, Test, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
@@ -467,6 +576,9 @@ void menuHandler::systemBaseMenu()
optionsArray[options] = "Screen Options";
optionsEnumArray[options++] = ScreenOptions;
#endif
optionsArray[options] = "Frame Visiblity Toggle";
optionsEnumArray[options++] = FrameToggles;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "Bluetooth";
#else
@@ -504,6 +616,9 @@ void menuHandler::systemBaseMenu()
} else if (selected == PowerMenu) {
menuHandler::menuQueue = menuHandler::power_menu;
screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == Test) {
menuHandler::menuQueue = menuHandler::test_menu;
screen->runNow();
@@ -570,16 +685,17 @@ void menuHandler::favoriteBaseMenu()
void menuHandler::positionBaseMenu()
{
enum optionsNumbers { Back, GPSToggle, CompassMenu, CompassCalibrate, enumEnd };
enum optionsNumbers { Back, GPSToggle, GPSFormat, CompassMenu, CompassCalibrate, enumEnd };
static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "Compass"};
static int optionsEnumArray[enumEnd] = {Back, GPSToggle, CompassMenu};
int options = 3;
static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "GPS Format", "Compass"};
static int optionsEnumArray[enumEnd] = {Back, GPSToggle, GPSFormat, CompassMenu};
int options = 4;
if (accelerometerThread) {
optionsArray[options] = "Compass Calibrate";
optionsEnumArray[options++] = CompassCalibrate;
}
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Position Action";
bannerOptions.optionsArrayPtr = optionsArray;
@@ -589,6 +705,9 @@ void menuHandler::positionBaseMenu()
if (selected == GPSToggle) {
menuQueue = gps_toggle_menu;
screen->runNow();
} else if (selected == GPSFormat) {
menuQueue = gps_format_menu;
screen->runNow();
} else if (selected == CompassMenu) {
menuQueue = compass_point_north_menu;
screen->runNow();
@@ -715,6 +834,58 @@ void menuHandler::GPSToggleMenu()
bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2;
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::GPSFormatMenu()
{
static const char *optionsArray[] = {"Back",
isHighResolution ? "Decimal Degrees" : "DEC",
isHighResolution ? "Degrees Minutes Seconds" : "DMS",
isHighResolution ? "Universal Transverse Mercator" : "UTM",
isHighResolution ? "Military Grid Reference System" : "MGRS",
isHighResolution ? "Open Location Code" : "OLC",
isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR",
isHighResolution ? "Maidenhead Locator" : "MLS"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "GPS Format";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 8;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 2) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 3) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 4) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 5) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 6) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 7) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else {
menuQueue = position_base_menu;
screen->runNow();
}
};
bannerOptions.InitialSelected = uiconfig.gps_format + 1;
screen->showOverlayBanner(bannerOptions);
}
#endif
void menuHandler::BluetoothToggleMenu()
@@ -1010,16 +1181,33 @@ void menuHandler::traceRouteMenu()
void menuHandler::testMenu()
{
static const char *optionsArray[] = {"Back", "Number Picker"};
enum optionsNumbers { Back, NumberPicker, ShowChirpy };
static const char *optionsArray[4] = {"Back"};
static int optionsEnumArray[4] = {Back};
int options = 1;
optionsArray[options] = "Number Picker";
optionsEnumArray[options++] = NumberPicker;
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
optionsEnumArray[options++] = ShowChirpy;
BannerOverlayOptions bannerOptions;
std::string message = "Test to Run?\n";
bannerOptions.message = message.c_str();
bannerOptions.message = "Hidden Test Menu";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
if (selected == NumberPicker) {
menuQueue = number_test;
screen->runNow();
} else if (selected == ShowChirpy) {
screen->toggleFrameVisibility("chirpy");
screen->setFrames(Screen::FOCUS_SYSTEM);
} else {
menuQueue = system_base_menu;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
@@ -1220,6 +1408,116 @@ void menuHandler::keyVerificationFinalPrompt()
}
}
void menuHandler::FrameToggles_menu()
{
enum optionsNumbers {
Finish,
nodelist,
nodelist_lastheard,
nodelist_hopsignal,
nodelist_distance,
nodelist_bearings,
gps,
lora,
clock,
show_favorites,
enumEnd
};
static const char *optionsArray[enumEnd] = {"Finish"};
static int optionsEnumArray[enumEnd] = {Finish};
int options = 1;
// Track last selected index (not enum value!)
static int lastSelectedIndex = 0;
#ifndef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List";
optionsEnumArray[options++] = nodelist;
#endif
#ifdef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
optionsEnumArray[options++] = nodelist_lastheard;
optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
optionsEnumArray[options++] = nodelist_hopsignal;
optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
optionsEnumArray[options++] = nodelist_distance;
#endif
#if HAS_GPS
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings";
optionsEnumArray[options++] = nodelist_bearings;
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
optionsEnumArray[options++] = gps;
#endif
optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa";
optionsEnumArray[options++] = lora;
optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock";
optionsEnumArray[options++] = clock;
optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites";
optionsEnumArray[options++] = show_favorites;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Show/Hide Frames";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value
bannerOptions.bannerCallback = [options](int selected) mutable -> void {
// Find the index of selected in optionsEnumArray
int idx = 0;
for (; idx < options; ++idx) {
if (optionsEnumArray[idx] == selected)
break;
}
lastSelectedIndex = idx;
if (selected == Finish) {
screen->setFrames(Screen::FOCUS_DEFAULT);
} else if (selected == nodelist) {
screen->toggleFrameVisibility("nodelist");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_lastheard) {
screen->toggleFrameVisibility("nodelist_lastheard");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_hopsignal) {
screen->toggleFrameVisibility("nodelist_hopsignal");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_distance) {
screen->toggleFrameVisibility("nodelist_distance");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_bearings) {
screen->toggleFrameVisibility("nodelist_bearings");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == gps) {
screen->toggleFrameVisibility("gps");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == lora) {
screen->toggleFrameVisibility("lora");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == clock) {
screen->toggleFrameVisibility("clock");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == show_favorites) {
screen->toggleFrameVisibility("show_favorites");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
{
if (menuQueue != menu_none)
@@ -1227,9 +1525,18 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
switch (menuQueue) {
case menu_none:
break;
case lora_Menu:
loraMenu();
break;
case lora_picker:
LoraRegionPicker();
break;
case device_role_picker:
DeviceRolePicker();
break;
case radio_preset_picker:
RadioPresetPicker();
break;
case no_timeout_lora_picker:
LoraRegionPicker(0);
break;
@@ -1255,6 +1562,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case gps_toggle_menu:
GPSToggleMenu();
break;
case gps_format_menu:
GPSFormatMenu();
break;
#endif
case compass_point_north_menu:
compassNorthMenu();
@@ -1316,6 +1626,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case power_menu:
powerMenu();
break;
case FrameToggles:
FrameToggles_menu();
break;
case throttle_message:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break;

View File

@@ -9,7 +9,10 @@ class menuHandler
public:
enum screenMenus {
menu_none,
lora_Menu,
lora_picker,
device_role_picker,
radio_preset_picker,
no_timeout_lora_picker,
TZ_picker,
twelve_hour_picker,
@@ -17,6 +20,7 @@ class menuHandler
clock_menu,
position_base_menu,
gps_toggle_menu,
gps_format_menu,
compass_point_north_menu,
reset_node_db_menu,
buzzermodemenupicker,
@@ -39,11 +43,15 @@ class menuHandler
key_verification_final_prompt,
trace_route_menu,
throttle_message,
FrameToggles
};
static screenMenus menuQueue;
static void OnboardMessage();
static void LoraRegionPicker(uint32_t duration = 30000);
static void loraMenu();
static void DeviceRolePicker();
static void RadioPresetPicker();
static void handleMenuSwitch(OLEDDisplay *display);
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
static void clockMenu();
@@ -58,6 +66,7 @@ class menuHandler
static void positionBaseMenu();
static void compassNorthMenu();
static void GPSToggleMenu();
static void GPSFormatMenu();
static void BuzzerModeMenu();
static void switchToMUIMenu();
static void TFTColorPickerMenu(OLEDDisplay *display);
@@ -76,6 +85,7 @@ class menuHandler
static void notificationsMenu();
static void screenOptionsMenu();
static void powerMenu();
static void FrameToggles_menu();
static void textMessageMenu();
private:

View File

@@ -7,10 +7,18 @@
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#if HAS_BUTTON
#include "input/ButtonThread.h"
#endif
#include "main.h"
#include <algorithm>
#include <string>
#include <vector>
#if HAS_TRACKBALL
#include "input/TrackballInterruptImpl1.h"
#endif
#ifdef ARCH_ESP32
#include "esp_task_wdt.h"
@@ -18,6 +26,11 @@
using namespace meshtastic;
#if HAS_BUTTON
// Global button thread pointer defined in main.cpp
extern ::ButtonThread *UserButtonThread;
#endif
// External references to global variables from Screen.cpp
extern std::vector<std::string> functionSymbol;
extern std::string functionSymbolString;
@@ -38,6 +51,8 @@ bool NotificationRenderer::pauseBanner = false;
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
uint32_t NotificationRenderer::numDigits = 0;
uint32_t NotificationRenderer::currentNumber = 0;
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
uint32_t pow_of_10(uint32_t n)
{
@@ -89,14 +104,33 @@ void NotificationRenderer::resetBanner()
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
{
if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0')
resetBanner();
if (!isOverlayBannerShowing() || pauseBanner)
// Handle text_input notifications first - they have their own timeout/banner logic
if (current_notification_type == notificationTypeEnum::text_input) {
// Check for timeout and reset if needed for text input
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
resetBanner();
return;
}
drawTextInput(display, state);
return;
}
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
resetBanner();
}
// Exit if no banner is showing or banner is paused
if (!isOverlayBannerShowing() || pauseBanner) {
return;
}
switch (current_notification_type) {
case notificationTypeEnum::none:
// Do nothing - no notification to display
break;
case notificationTypeEnum::text_input:
// Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch.
break;
case notificationTypeEnum::text_banner:
case notificationTypeEnum::selection_picker:
drawAlertBannerOverlay(display, state);
@@ -216,9 +250,11 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
}
// Handle input
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
alertBannerCallback(selectedNodenum);
@@ -267,12 +303,9 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
} else {
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
}
// make temp buffer for name
// fi
if (i == curSelected) {
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
if (isHighResolution) {
@@ -286,7 +319,8 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
}
scratchLineBuffer[scratchLineNum][39] = '\0';
} else {
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36);
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39);
scratchLineBuffer[scratchLineNum][39] = '\0';
}
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
}
@@ -333,9 +367,11 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
// Handle input
if (alertBannerOptions > 0) {
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
if (optionsEnumPtr != nullptr) {
@@ -705,6 +741,99 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi
"Please be patient and do not power off.");
}
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
{
if (virtualKeyboard) {
// Check for timeout and auto-exit if needed
if (virtualKeyboard->isTimedOut()) {
LOG_INFO("Virtual keyboard timeout - auto-exiting");
// Cancel virtual keyboard - call callback with empty string to indicate timeout
auto callback = textInputCallback; // Store callback before clearing
// Clean up first to prevent re-entry
delete virtualKeyboard;
virtualKeyboard = nullptr;
textInputCallback = nullptr;
resetBanner();
// Call callback after cleanup
if (callback) {
callback("");
}
// Restore normal overlays
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
return;
}
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
if (inEvent.inputEvent == INPUT_BROKER_UP) {
// high frequency for move cursor left/right than up/down with encoders
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;
delete virtualKeyboard;
virtualKeyboard = nullptr;
textInputCallback = nullptr;
resetBanner();
if (callback) {
callback("");
}
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
return;
}
// Consume the event after processing for virtual keyboard
inEvent.inputEvent = INPUT_BROKER_NONE;
}
// Clear the screen to avoid overlapping with underlying frames or overlays
display->setColor(BLACK);
display->fillRect(0, 0, display->getWidth(), display->getHeight());
display->setColor(WHITE);
// Draw the virtual keyboard
virtualKeyboard->draw(display, 0, 0);
} else {
// If virtualKeyboard is null, reset the banner to avoid getting stuck
LOG_INFO("Virtual keyboard is null - resetting banner");
resetBanner();
}
}
bool NotificationRenderer::isOverlayBannerShowing()
{
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);

View File

@@ -3,6 +3,9 @@
#include "OLEDDisplay.h"
#include "OLEDDisplayUi.h"
#include "graphics/Screen.h"
#include "graphics/VirtualKeyboard.h"
#include <functional>
#include <string>
#define MAX_LINES 5
namespace graphics
@@ -22,6 +25,8 @@ class NotificationRenderer
static std::function<void(int)> alertBannerCallback;
static uint32_t numDigits;
static uint32_t currentNumber;
static VirtualKeyboard *virtualKeyboard;
static std::function<void(const std::string &)> textInputCallback;
static bool pauseBanner;
@@ -30,6 +35,7 @@ class NotificationRenderer
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);

View File

@@ -20,7 +20,9 @@
// External variables
extern graphics::Screen *screen;
#if defined(M5STACK_UNITC6L)
static uint32_t lastSwitchTime = 0;
#endif
namespace graphics
{
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
@@ -116,64 +118,124 @@ void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, con
}
// Draw GPS status coordinates
void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps,
const char *mode)
{
auto gpsFormat = config.display.gps_format;
auto gpsFormat = uiconfig.gps_format;
char displayLine[32];
if (!gps->getIsConnected() && !config.position.fixed_position) {
strcpy(displayLine, "No GPS present");
display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine);
if (strcmp(mode, "line1") == 0) {
strcpy(displayLine, "No GPS present");
display->drawString(x, y, displayLine);
}
} else if (!gps->getHasLock() && !config.position.fixed_position) {
strcpy(displayLine, "No GPS Lock");
display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine);
if (strcmp(mode, "line1") == 0) {
strcpy(displayLine, "No GPS Lock");
display->drawString(x, y, displayLine);
}
} else {
geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude()));
if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) {
char coordinateLine[22];
if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees
snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7,
geoCoord.getLongitude() * 1e-7);
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator
snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(),
geoCoord.getUTMEasting(), geoCoord.getUTMNorthing());
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System
snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(),
geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(),
geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing());
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code
geoCoord.getOLCCode(coordinateLine);
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference
if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region
snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary");
else
snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(),
geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing());
if (gpsFormat != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) {
char coordinateLine_1[22];
char coordinateLine_2[22];
if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7);
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7);
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(),
geoCoord.getUTMBand(), geoCoord.getUTMEasting());
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing());
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(),
geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k());
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(),
geoCoord.getMGRSNorthing());
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC) { // Open Location Code
geoCoord.getOLCCode(coordinateLine_1);
coordinateLine_2[0] = '\0';
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference
if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary");
coordinateLine_2[0] = '\0';
} else {
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%1c%1c", geoCoord.getOSGRE100k(),
geoCoord.getOSGRN100k());
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(),
geoCoord.getOSGRNorthing());
}
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // Maidenhead Locator System
double lat = geoCoord.getLatitude() * 1e-7;
double lon = geoCoord.getLongitude() * 1e-7;
// Normalize
if (lat > 90.0)
lat = 90.0;
if (lat < -90.0)
lat = -90.0;
while (lon < -180.0)
lon += 360.0;
while (lon >= 180.0)
lon -= 360.0;
double adjLon = lon + 180.0;
double adjLat = lat + 90.0;
char maiden[10]; // enough for 8-char + null
// Field (2 letters)
int lonField = int(adjLon / 20.0);
int latField = int(adjLat / 10.0);
adjLon -= lonField * 20.0;
adjLat -= latField * 10.0;
// Square (2 digits)
int lonSquare = int(adjLon / 2.0);
int latSquare = int(adjLat / 1.0);
adjLon -= lonSquare * 2.0;
adjLat -= latSquare * 1.0;
// Subsquare (2 letters)
double lonUnit = 2.0 / 24.0;
double latUnit = 1.0 / 24.0;
int lonSub = int(adjLon / lonUnit);
int latSub = int(adjLat / latUnit);
snprintf(maiden, sizeof(maiden), "%c%c%c%c%c%c", 'A' + lonField, 'A' + latField, '0' + lonSquare, '0' + latSquare,
'A' + lonSub, 'A' + latSub);
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "MH: %s", maiden);
coordinateLine_2[0] = '\0'; // only need one line
}
// If fixed position, display text "Fixed GPS" alternating with the coordinates.
if (config.position.fixed_position) {
if ((millis() / 10000) % 2) {
display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y,
coordinateLine);
} else {
display->drawString(x + (display->getWidth() - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS");
if (strcmp(mode, "line1") == 0) {
display->drawString(x, y, coordinateLine_1);
} else if (strcmp(mode, "line2") == 0) {
display->drawString(x, y, coordinateLine_2);
} else if (strcmp(mode, "combined") == 0) {
display->drawString(x, y, coordinateLine_1);
if (coordinateLine_2[0] != '\0') {
display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2);
}
} else {
display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine);
}
} else {
char latLine[22];
char lonLine[22];
snprintf(latLine, sizeof(latLine), "%2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(),
geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP());
snprintf(lonLine, sizeof(lonLine), "%3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(),
geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP());
display->drawString(x + (display->getWidth() - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1,
latLine);
display->drawString(x + (display->getWidth() - (display->getStringWidth(lonLine))) / 2, y, lonLine);
char coordinateLine_1[22];
char coordinateLine_2[22];
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(),
geoCoord.getDMSLatMin(), geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP());
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(),
geoCoord.getDMSLonMin(), geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP());
if (strcmp(mode, "line1") == 0) {
display->drawString(x, y, coordinateLine_1);
} else if (strcmp(mode, "line2") == 0) {
display->drawString(x, y, coordinateLine_2);
} else { // both
display->drawString(x, y, coordinateLine_1);
display->drawString(x, y + 10, coordinateLine_2);
}
}
}
}
@@ -229,9 +291,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite)
return;
uint32_t now = millis();
display->clear();
#if defined(M5STACK_UNITC6L)
uint32_t now = millis();
if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒
{
display->display();
@@ -676,7 +738,6 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int textWidth = 0;
int nameX = 0;
int yOffset = (isHighResolution) ? 0 : 5;
const char *longName = nullptr;
std::string longNameStr;
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
@@ -937,7 +998,26 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
config.display.heading_bold = false;
const char *displayLine = ""; // Initialize to empty string by default
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
bool usePhoneGPS = (ourNode && nodeDB->hasValidPosition(ourNode) &&
config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED);
if (usePhoneGPS) {
// Phone-provided GPS is active
displayLine = "Phone GPS";
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
// GPS disabled / not present
if (config.position.fixed_position) {
displayLine = "Fixed GPS";
} else {
@@ -954,6 +1034,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else {
// Onboard GPS
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
}
@@ -980,46 +1061,62 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// If GPS is off, no need to display these parts
if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) {
// === Second Row: Last GPS Fix ===
if (gpsStatus->getLastFixMillis() > 0) {
uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix
uint32_t days = delta / 86400;
uint32_t hours = (delta % 86400) / 3600;
uint32_t mins = (delta % 3600) / 60;
uint32_t secs = delta % 60;
// === Second Row: Date ===
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
char datetimeStr[25];
bool showTime = false; // set to true for full datetime
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime);
char fullLine[40];
snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr);
#if !defined(M5STACK_UNITC6L)
display->drawString(0, getTextPositions(display)[line++], fullLine);
char buf[32];
#if defined(USE_EINK)
// E-Ink: skip seconds, show only days/hours/mins
if (days > 0) {
snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours);
} else if (hours > 0) {
snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins);
} else {
snprintf(buf, sizeof(buf), "Last: %um", mins);
}
#else
// Non E-Ink: include seconds where useful
if (days > 0) {
snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours);
} else if (hours > 0) {
snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins);
} else if (mins > 0) {
snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs);
} else {
snprintf(buf, sizeof(buf), "Last: %us", secs);
}
#endif
// === Third Row: Latitude ===
char latStr[32];
#if defined(M5STACK_UNITC6L)
snprintf(latStr, sizeof(latStr), "Lat:%.5f", geoCoord.getLatitude() * 1e-7);
display->drawString(x, getTextPositions(display)[line++] + 2, latStr);
#else
snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7);
display->drawString(x, getTextPositions(display)[line++], latStr);
#endif
// === Fourth Row: Longitude ===
char lonStr[32];
#if defined(M5STACK_UNITC6L)
snprintf(lonStr, sizeof(lonStr), "Lon:%.3f", geoCoord.getLongitude() * 1e-7);
display->drawString(x, getTextPositions(display)[line++] + 4, lonStr);
#else
snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7);
display->drawString(x, getTextPositions(display)[line++], lonStr);
// === Fifth Row: Altitude ===
char DisplayLineTwo[32] = {0};
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET);
display->drawString(0, getTextPositions(display)[line++], buf);
} else {
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude());
display->drawString(0, getTextPositions(display)[line++], "Last: ?");
}
display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo);
#endif
// === Third Row: Line 1 GPS Info ===
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1");
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC &&
uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) {
// === Fourth Row: Line 2 GPS Info ===
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2");
}
// === Final Row: Altitude ===
char altitudeLine[32] = {0};
int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode))
? ourNode->position.altitude
: geoCoord.getAltitude();
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET);
} else {
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt);
}
display->drawString(x, getTextPositions(display)[line++], altitudeLine);
}
#if !defined(M5STACK_UNITC6L)
// === Draw Compass if heading is valid ===
@@ -1104,7 +1201,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
}
}
#endif
#endif
#endif // HAS_GPS
}
#ifdef USERPREFS_OEM_TEXT
@@ -1197,14 +1294,13 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
// Only show bar briefly after switching frames
static uint32_t navBarLastShown = 0;
static bool cosmeticRefreshDone = false;
bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
#if defined(USE_EINK)
// Only show bar briefly after switching frames
static uint32_t navBarLastShown = 0;
static bool cosmeticRefreshDone = false;
static bool navBarPrevVisible = false;
if (navBarVisible && !navBarPrevVisible) {

View File

@@ -1,5 +1,6 @@
#pragma once
#include "NodeDB.h"
#include "graphics/Screen.h"
#include "graphics/emotes.h"
#include <OLEDDisplay.h>
@@ -37,7 +38,8 @@ class UIRenderer
// GPS status functions
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus,
const char *mode = "line1");
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);

View File

@@ -118,8 +118,8 @@ const uint8_t icon_radio[] PROGMEM = {
0xA9 // Row 7: #..#.#.#
};
// 🪙 Memory Icon
const uint8_t icon_memory[] PROGMEM = {
// 🪙 System Icon
const uint8_t icon_system[] PROGMEM = {
0x24, // Row 0: ..#..#..
0x3C, // Row 1: ..####..
0xC3, // Row 2: ##....##
@@ -287,6 +287,79 @@ const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101
#define analog_icon_clock_height 8
const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000,
0b00100100, 0b01000010, 0b01000010, 0b11111111};
#define chirpy_width 38
#define chirpy_height 50
const uint8_t chirpy[] = {
0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01,
0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00,
0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f,
0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc,
0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0,
0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1,
0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff,
0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3,
0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03,
0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0,
0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01,
0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf};
#define chirpy_width_hirez 76
#define chirpy_height_hirez 100
const uint8_t chirpy_hirez[] = {
0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00,
0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc,
0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03,
0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0,
0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff,
0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f,
0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0,
0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff,
0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00,
0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc,
0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03,
0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0,
0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff,
0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f,
0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc,
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03,
0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0,
0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00,
0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f,
0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c,
0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00,
0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00,
0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc,
0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03,
0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00,
0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0,
0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3};
#define chirpy_small_image_width 8
#define chirpy_small_image_height 8
const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
#ifdef M5STACK_UNITC6L
#include "img/icon_small.xbm"
#else

View File

@@ -41,78 +41,78 @@ void tftSetup(void)
PacketAPI::create(PacketServer::init());
deviceScreen->init(new PacketClient);
#else
if (settingsMap[displayPanel] != no_screen) {
if (portduino_config.displayPanel != no_screen) {
DisplayDriverConfig displayConfig;
static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S",
"ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"};
static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"};
#if defined(USE_X11)
if (settingsMap[displayPanel] == x11) {
if (settingsMap[displayWidth] && settingsMap[displayHeight])
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)settingsMap[displayWidth],
(uint16_t)settingsMap[displayHeight]);
if (portduino_config.displayPanel == x11) {
if (portduino_config.displayWidth && portduino_config.displayHeight)
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)portduino_config.displayWidth,
(uint16_t)portduino_config.displayHeight);
else
displayConfig.device(DisplayDriverConfig::device_t::X11);
} else
#elif defined(USE_FRAMEBUFFER)
if (settingsMap[displayPanel] == fb) {
if (settingsMap[displayWidth] && settingsMap[displayHeight])
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)settingsMap[displayWidth],
(uint16_t)settingsMap[displayHeight]);
if (portduino_config.displayPanel == fb) {
if (portduino_config.displayWidth && portduino_config.displayHeight)
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth,
(uint16_t)portduino_config.displayHeight);
else
displayConfig.device(DisplayDriverConfig::device_t::FB);
} else
#endif
{
displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT)
.panel(DisplayDriverConfig::panel_config_t{.type = panels[settingsMap[displayPanel]],
.panel_width = (uint16_t)settingsMap[displayWidth],
.panel_height = (uint16_t)settingsMap[displayHeight],
.rotation = (bool)settingsMap[displayRotate],
.pin_cs = (int16_t)settingsMap[displayCS],
.pin_rst = (int16_t)settingsMap[displayReset],
.offset_x = (uint16_t)settingsMap[displayOffsetX],
.offset_y = (uint16_t)settingsMap[displayOffsetY],
.offset_rotation = (uint8_t)settingsMap[displayOffsetRotate],
.invert = settingsMap[displayInvert] ? true : false,
.rgb_order = (bool)settingsMap[displayRGBOrder],
.dlen_16bit = settingsMap[displayPanel] == ili9486 ||
settingsMap[displayPanel] == ili9488})
.bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)settingsMap[displayBusFrequency],
.panel(DisplayDriverConfig::panel_config_t{.type = panels[portduino_config.displayPanel],
.panel_width = (uint16_t)portduino_config.displayWidth,
.panel_height = (uint16_t)portduino_config.displayHeight,
.rotation = (bool)portduino_config.displayRotate,
.pin_cs = (int16_t)portduino_config.displayCS.pin,
.pin_rst = (int16_t)portduino_config.displayReset.pin,
.offset_x = (uint16_t)portduino_config.displayOffsetX,
.offset_y = (uint16_t)portduino_config.displayOffsetY,
.offset_rotation = (uint8_t)portduino_config.displayOffsetRotate,
.invert = portduino_config.displayInvert ? true : false,
.rgb_order = (bool)portduino_config.displayRGBOrder,
.dlen_16bit = portduino_config.displayPanel == ili9486 ||
portduino_config.displayPanel == ili9488})
.bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)portduino_config.displayBusFrequency,
.freq_read = 16000000,
.spi{.pin_dc = (int8_t)settingsMap[displayDC],
.spi{.pin_dc = (int8_t)portduino_config.displayDC.pin,
.use_lock = true,
.spi_host = (uint16_t)settingsMap[displayspidev]}})
.input(DisplayDriverConfig::input_config_t{.keyboardDevice = settingsStrings[keyboardDevice],
.pointerDevice = settingsStrings[pointerDevice]})
.light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)settingsMap[displayBacklight],
.pwm_channel = (int8_t)settingsMap[displayBacklightPWMChannel],
.invert = (bool)settingsMap[displayBacklightInvert]});
if (settingsMap[touchscreenI2CAddr] == -1) {
.spi_host = (uint16_t)portduino_config.display_spi_dev_int}})
.input(DisplayDriverConfig::input_config_t{.keyboardDevice = portduino_config.keyboardDevice,
.pointerDevice = portduino_config.pointerDevice})
.light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)portduino_config.displayBacklight.pin,
.pwm_channel = (int8_t)portduino_config.displayBacklightPWMChannel.pin,
.invert = (bool)portduino_config.displayBacklightInvert});
if (portduino_config.touchscreenI2CAddr == -1) {
displayConfig.touch(
DisplayDriverConfig::touch_config_t{.type = touch[settingsMap[touchscreenModule]],
.freq = (uint32_t)settingsMap[touchscreenBusFrequency],
.pin_int = (int16_t)settingsMap[touchscreenIRQ],
.offset_rotation = (uint8_t)settingsMap[touchscreenRotate],
DisplayDriverConfig::touch_config_t{.type = touch[portduino_config.touchscreenModule],
.freq = (uint32_t)portduino_config.touchscreenBusFrequency,
.pin_int = (int16_t)portduino_config.touchscreenIRQ.pin,
.offset_rotation = (uint8_t)portduino_config.touchscreenRotate,
.spi{
.spi_host = (int8_t)settingsMap[touchscreenspidev],
.spi_host = (int8_t)portduino_config.touchscreen_spi_dev_int,
},
.pin_cs = (int16_t)settingsMap[touchscreenCS]});
.pin_cs = (int16_t)portduino_config.touchscreenCS.pin});
} else {
displayConfig.touch(DisplayDriverConfig::touch_config_t{
.type = touch[settingsMap[touchscreenModule]],
.freq = (uint32_t)settingsMap[touchscreenBusFrequency],
.type = touch[portduino_config.touchscreenModule],
.freq = (uint32_t)portduino_config.touchscreenBusFrequency,
.x_min = 0,
.x_max =
(int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayWidth] : settingsMap[displayHeight]) -
1),
.x_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayWidth
: portduino_config.displayHeight) -
1),
.y_min = 0,
.y_max =
(int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayHeight] : settingsMap[displayWidth]) -
1),
.pin_int = (int16_t)settingsMap[touchscreenIRQ],
.offset_rotation = (uint8_t)settingsMap[touchscreenRotate],
.i2c{.i2c_addr = (uint8_t)settingsMap[touchscreenI2CAddr]}});
.y_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayHeight
: portduino_config.displayWidth) -
1),
.pin_int = (int16_t)portduino_config.touchscreenIRQ.pin,
.offset_rotation = (uint8_t)portduino_config.touchscreenRotate,
.i2c{.i2c_addr = (uint8_t)portduino_config.touchscreenI2CAddr}});
}
}
deviceScreen = &DeviceScreen::create(&displayConfig);

View File

@@ -274,7 +274,12 @@ int32_t ButtonThread::runOnce()
}
}
btnEvent = BUTTON_EVENT_NONE;
return 50;
// only pull when the button is pressed, we get notified via IRQ on a new press
if (!userButton.isIdle() || waitingForLongPress) {
return 50;
}
return 100; // FIXME: Why can't we rely on interrupts and use INT32_MAX here?
}
/*

View File

@@ -76,6 +76,9 @@ class ButtonThread : public Observable<const InputEvent *>, public concurrency::
return digitalRead(buttonPin); // Most buttons are active low by default
}
// Returns true while this thread's button is physically held down
bool isHeld() { return isButtonPressed(_pinNum); }
// Disconnect and reconnect interrupts for light sleep
#ifdef ARCH_ESP32
int beforeLightSleep(void *unused);

View File

@@ -188,7 +188,7 @@ void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length)
// Feed input to the canned messages module
void ExpressLRSFiveWay::sendKey(input_broker_event key)
{
InputEvent e;
InputEvent e = {};
e.source = inputSourceName;
e.inputEvent = key;
notifyObservers(&e);

View File

@@ -4,6 +4,9 @@
enum input_broker_event {
INPUT_BROKER_NONE = 0,
INPUT_BROKER_SELECT = 10,
INPUT_BROKER_SELECT_LONG = 11,
INPUT_BROKER_UP_LONG = 12,
INPUT_BROKER_DOWN_LONG = 13,
INPUT_BROKER_UP = 17,
INPUT_BROKER_DOWN = 18,
INPUT_BROKER_LEFT = 19,

View File

@@ -33,9 +33,9 @@ int32_t LinuxInput::runOnce()
{
if (firstTime) {
if (settingsStrings[keyboardDevice] == "")
if (portduino_config.keyboardDevice == "")
return disable();
fd = open(settingsStrings[keyboardDevice].c_str(), O_RDWR);
fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR);
if (fd < 0)
return disable();
ret = ioctl(fd, EVIOCGRAB, (void *)1);
@@ -73,7 +73,7 @@ int32_t LinuxInput::runOnce()
int rd = read(events[i].data.fd, ev, sizeof(ev));
assert(rd > ((signed int)sizeof(struct input_event)));
for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) {
InputEvent e;
InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
e.kbchar = 0;

View File

@@ -8,15 +8,17 @@ RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concu
void RotaryEncoderInterruptBase::init(
uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
input_broker_event eventPressed,
input_broker_event eventPressed, input_broker_event eventPressedLong,
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress) :
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)())
{
this->_pinA = pinA;
this->_pinB = pinB;
this->_pinPress = pinPress;
this->_eventCw = eventCw;
this->_eventCcw = eventCcw;
this->_eventPressed = eventPressed;
this->_eventPressedLong = eventPressedLong;
bool isRAK = false;
#ifdef RAK_4631
@@ -25,7 +27,7 @@ void RotaryEncoderInterruptBase::init(
if (!isRAK || pinPress != 0) {
pinMode(pinPress, INPUT_PULLUP);
attachInterrupt(pinPress, onIntPress, RISING);
attachInterrupt(pinPress, onIntPress, CHANGE);
}
if (!isRAK || this->_pinA != 0) {
pinMode(this->_pinA, INPUT_PULLUP);
@@ -43,13 +45,40 @@ void RotaryEncoderInterruptBase::init(
int32_t RotaryEncoderInterruptBase::runOnce()
{
InputEvent e;
InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
unsigned long now = millis();
// Handle press long/short detection
if (this->action == ROTARY_ACTION_PRESSED) {
LOG_DEBUG("Rotary event Press");
e.inputEvent = this->_eventPressed;
bool buttonPressed = !digitalRead(_pinPress);
if (!pressDetected && buttonPressed) {
pressDetected = true;
pressStartTime = now;
}
if (pressDetected) {
uint32_t duration = now - pressStartTime;
if (!buttonPressed) {
// released -> if short press, send short, else already sent long
if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
lastPressKeyTime = now;
LOG_DEBUG("Rotary event Press short");
e.inputEvent = this->_eventPressed;
}
pressDetected = false;
pressStartTime = 0;
lastPressLongEventTime = 0;
this->action = ROTARY_ACTION_NONE;
} else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE &&
lastPressLongEventTime == 0) {
// fire single-shot long press
lastPressLongEventTime = now;
LOG_DEBUG("Rotary event Press long");
e.inputEvent = this->_eventPressedLong;
}
}
} else if (this->action == ROTARY_ACTION_CW) {
LOG_DEBUG("Rotary event CW");
e.inputEvent = this->_eventCw;
@@ -62,7 +91,9 @@ int32_t RotaryEncoderInterruptBase::runOnce()
this->notifyObservers(&e);
}
this->action = ROTARY_ACTION_NONE;
if (!pressDetected) {
this->action = ROTARY_ACTION_NONE;
}
return INT32_MAX;
}
@@ -70,7 +101,7 @@ int32_t RotaryEncoderInterruptBase::runOnce()
void RotaryEncoderInterruptBase::intPressHandler()
{
this->action = ROTARY_ACTION_PRESSED;
setIntervalFromNow(20); // TODO: this modifies a non-volatile variable!
setIntervalFromNow(20); // start checking for long/short
}
void RotaryEncoderInterruptBase::intAHandler()
@@ -120,7 +151,7 @@ RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler(bool
// Logic to prevent bouncing.
newState = ROTARY_EVENT_CLEARED;
}
setIntervalFromNow(50); // TODO: this modifies a non-volatile variable!
setIntervalFromNow(ROTARY_DELAY); // TODO: this modifies a non-volatile variable!
return newState;
}

View File

@@ -13,7 +13,7 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
public:
explicit RotaryEncoderInterruptBase(const char *name);
void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
input_broker_event eventPressed,
input_broker_event eventPressed, input_broker_event eventPressedLong,
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress);
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)());
void intPressHandler();
@@ -33,10 +33,22 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE;
private:
// pins and events
uint8_t _pinA = 0;
uint8_t _pinB = 0;
uint8_t _pinPress = 0;
input_broker_event _eventCw = INPUT_BROKER_NONE;
input_broker_event _eventCcw = INPUT_BROKER_NONE;
input_broker_event _eventPressed = INPUT_BROKER_NONE;
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
const char *_originName;
// Long press detection variables
uint32_t pressStartTime = 0;
bool pressDetected = false;
uint32_t lastPressLongEventTime = 0;
unsigned long lastPressKeyTime = 0;
static const uint32_t LONG_PRESS_DURATION = 300; // ms
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 0; // 0 = single-shot for rotary select
const unsigned long pressDebounceMs = 200; // ms
};

View File

@@ -1,5 +1,6 @@
#include "RotaryEncoderInterruptImpl1.h"
#include "InputBroker.h"
extern bool osk_found;
RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
@@ -19,12 +20,14 @@ bool RotaryEncoderInterruptImpl1::init()
input_broker_event eventCw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_cw);
input_broker_event eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw);
input_broker_event eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
// moduleConfig.canned_message.ext_notification_module_output
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed,
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong,
RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB,
RotaryEncoderInterruptImpl1::handleIntPressed);
inputBroker->registerSource(this);
osk_found = true;
return true;
}

View File

@@ -49,7 +49,7 @@ bool SeesawRotary::init()
int32_t SeesawRotary::runOnce()
{
InputEvent e;
InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE;
bool currentlyPressed = !ss.digitalRead(SS_SWITCH);

View File

@@ -29,7 +29,7 @@ SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name)
void SerialKeyboard::erase()
{
InputEvent e;
InputEvent e = {};
e.inputEvent = INPUT_BROKER_BACK;
e.kbchar = 0x08;
e.source = this->_originName;
@@ -80,7 +80,7 @@ int32_t SerialKeyboard::runOnce()
if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but
// shouldn't be a limitation
InputEvent e;
InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
// SELECT OR SEND OR CANCEL EVENT

View File

@@ -200,6 +200,11 @@ uint8_t TCA8418KeyboardBase::flush()
return count;
}
void TCA8418KeyboardBase::clearInt()
{
writeRegister(TCA8418_REG_INT_STAT, 3);
}
uint8_t TCA8418KeyboardBase::digitalRead(uint8_t pinnum) const
{
if (pinnum > TCA8418_COL9)

View File

@@ -37,6 +37,8 @@ class TCA8418KeyboardBase
virtual void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = TCA8418_KB_ADDR);
virtual void reset(void);
void clearInt(void);
virtual void trigger(void);
virtual void setBacklight(bool on);

View File

@@ -105,7 +105,14 @@ void TLoraPagerKeyboard::trigger()
void TLoraPagerKeyboard::setBacklight(bool on)
{
toggleBacklight(!on);
uint32_t _brightness = 0;
if (on)
_brightness = brightness;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
ledcWrite(KB_BL_PIN, _brightness);
#else
ledcWrite(LEDC_BACKLIGHT_CHANNEL, _brightness);
#endif
}
void TLoraPagerKeyboard::pressed(uint8_t key)
@@ -192,7 +199,6 @@ void TLoraPagerKeyboard::hapticFeedback()
// toggle brightness of the backlight in three steps
void TLoraPagerKeyboard::toggleBacklight(bool off)
{
static uint32_t brightness = 0;
if (off) {
brightness = 0;
} else {
@@ -206,11 +212,7 @@ void TLoraPagerKeyboard::toggleBacklight(bool off)
}
LOG_DEBUG("Toggle backlight: %d", brightness);
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
ledcWrite(KB_BL_PIN, brightness);
#else
ledcWrite(LEDC_BACKLIGHT_CHANNEL, brightness);
#endif
setBacklight(true);
}
void TLoraPagerKeyboard::updateModifierFlag(uint8_t key)

View File

@@ -26,4 +26,5 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
uint32_t brightness = 0;
};

View File

@@ -18,7 +18,7 @@ TouchScreenImpl1::TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTo
void TouchScreenImpl1::init()
{
#if ARCH_PORTDUINO
if (settingsMap[touchscreenModule]) {
if (portduino_config.touchscreenModule) {
TouchScreenBase::init(true);
inputBroker->registerSource(this);
} else {
@@ -47,7 +47,7 @@ bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y)
*/
void TouchScreenImpl1::onEvent(const TouchEvent &event)
{
InputEvent e;
InputEvent e = {};
e.source = event.source;
e.kbchar = 0;
e.touchX = event.x;

View File

@@ -1,12 +1,14 @@
#include "TrackballInterruptBase.h"
#include "configuration.h"
extern bool osk_found;
TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {}
void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress,
input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft,
input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(),
void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)())
input_broker_event eventRight, input_broker_event eventPressed,
input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(),
void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)())
{
this->_pinDown = pinDown;
this->_pinUp = pinUp;
@@ -18,6 +20,7 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
this->_eventLeft = eventLeft;
this->_eventRight = eventRight;
this->_eventPressed = eventPressed;
this->_eventPressedLong = eventPressedLong;
if (pinPress != 255) {
pinMode(pinPress, INPUT_PULLUP);
@@ -40,20 +43,57 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION);
}
LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight,
pinPress);
LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown,
this->_pinLeft, this->_pinRight, pinPress);
osk_found = true;
this->setInterval(100);
}
int32_t TrackballInterruptBase::runOnce()
{
InputEvent e;
InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE;
// Handle long press detection for press button
if (pressDetected && pressStartTime > 0) {
uint32_t pressDuration = millis() - pressStartTime;
bool buttonStillPressed = false;
#if defined(T_DECK)
buttonStillPressed = (this->action == TB_ACTION_PRESSED);
#else
buttonStillPressed = !digitalRead(_pinPress);
#endif
if (!buttonStillPressed) {
// Button released
if (pressDuration < LONG_PRESS_DURATION) {
// Short press
e.inputEvent = this->_eventPressed;
}
// Reset state
pressDetected = false;
pressStartTime = 0;
lastLongPressEventTime = 0;
this->action = TB_ACTION_NONE;
} else if (pressDuration >= LONG_PRESS_DURATION) {
// Long press detected
uint32_t currentTime = millis();
// Only trigger long press event if enough time has passed since the last one
if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
e.inputEvent = this->_eventPressedLong;
lastLongPressEventTime = currentTime;
}
this->action = TB_ACTION_PRESSED_LONG;
}
}
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
if (this->action == TB_ACTION_PRESSED) {
// LOG_DEBUG("Trackball event Press");
e.inputEvent = this->_eventPressed;
if (this->action == TB_ACTION_PRESSED && !pressDetected) {
// Start long press detection
pressDetected = true;
pressStartTime = millis();
// Don't send event yet, wait to see if it's a long press
} else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) {
// LOG_DEBUG("Trackball event UP");
e.inputEvent = this->_eventUp;
@@ -68,9 +108,11 @@ int32_t TrackballInterruptBase::runOnce()
e.inputEvent = this->_eventRight;
}
#else
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) {
// LOG_DEBUG("Trackball event Press");
e.inputEvent = this->_eventPressed;
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) {
// Start long press detection
pressDetected = true;
pressStartTime = millis();
// Don't send event yet, wait to see if it's a long press
} else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) {
// LOG_DEBUG("Trackball event UP");
e.inputEvent = this->_eventUp;
@@ -91,10 +133,16 @@ int32_t TrackballInterruptBase::runOnce()
e.kbchar = 0x00;
this->notifyObservers(&e);
}
lastEvent = action;
this->action = TB_ACTION_NONE;
return 100;
// Only update lastEvent for non-press actions or completed press actions
if (this->action != TB_ACTION_PRESSED || !pressDetected) {
lastEvent = action;
if (!pressDetected) {
this->action = TB_ACTION_NONE;
}
}
return 50; // Check more frequently for better long press detection
}
void TrackballInterruptBase::intPressHandler()

View File

@@ -6,7 +6,7 @@
#ifndef TB_DIRECTION
#if ARCH_PORTDUINO
#include "PortduinoGlue.h"
#define TB_DIRECTION (PinStatus) settingsMap[tbDirection]
#define TB_DIRECTION (PinStatus) portduino_config.lora_usb_vid
#else
#define TB_DIRECTION RISING
#endif
@@ -18,8 +18,8 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
explicit TrackballInterruptBase(const char *name);
void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown,
input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight,
input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(),
void (*onIntPress)());
input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(),
void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)());
void intPressHandler();
void intDownHandler();
void intUpHandler();
@@ -33,6 +33,7 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
enum TrackballInterruptBaseActionType {
TB_ACTION_NONE,
TB_ACTION_PRESSED,
TB_ACTION_PRESSED_LONG,
TB_ACTION_UP,
TB_ACTION_DOWN,
TB_ACTION_LEFT,
@@ -46,12 +47,20 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE;
// Long press detection for press button
uint32_t pressStartTime = 0;
bool pressDetected = false;
uint32_t lastLongPressEventTime = 0;
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
private:
input_broker_event _eventDown = INPUT_BROKER_NONE;
input_broker_event _eventUp = INPUT_BROKER_NONE;
input_broker_event _eventLeft = INPUT_BROKER_NONE;
input_broker_event _eventRight = INPUT_BROKER_NONE;
input_broker_event _eventPressed = INPUT_BROKER_NONE;
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
const char *_originName;
TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE;
};

View File

@@ -13,11 +13,12 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe
input_broker_event eventLeft = INPUT_BROKER_LEFT;
input_broker_event eventRight = INPUT_BROKER_RIGHT;
input_broker_event eventPressed = INPUT_BROKER_SELECT;
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight,
eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp,
TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight,
TrackballInterruptImpl1::handleIntPressed);
eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown,
TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft,
TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed);
inputBroker->registerSource(this);
}

View File

@@ -7,14 +7,22 @@ UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThre
}
void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown,
input_broker_event eventUp, input_broker_event eventPressed, void (*onIntDown)(),
input_broker_event eventUp, input_broker_event eventPressed, input_broker_event eventPressedLong,
input_broker_event eventUpLong, input_broker_event eventDownLong, void (*onIntDown)(),
void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs)
{
this->_pinDown = pinDown;
this->_pinUp = pinUp;
this->_pinPress = pinPress;
this->_eventDown = eventDown;
this->_eventUp = eventUp;
this->_eventPressed = eventPressed;
this->_eventPressedLong = eventPressedLong;
this->_eventUpLong = eventUpLong;
this->_eventDownLong = eventDownLong;
// Store debounce configuration passed by caller
this->updownDebounceMs = updownDebounceMs;
bool isRAK = false;
#ifdef RAK_4631
isRAK = true;
@@ -22,44 +30,109 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress,
if (!isRAK || pinPress != 0) {
pinMode(pinPress, INPUT_PULLUP);
attachInterrupt(pinPress, onIntPress, RISING);
attachInterrupt(pinPress, onIntPress, FALLING);
}
if (!isRAK || this->_pinDown != 0) {
pinMode(this->_pinDown, INPUT_PULLUP);
attachInterrupt(this->_pinDown, onIntDown, RISING);
attachInterrupt(this->_pinDown, onIntDown, FALLING);
}
if (!isRAK || this->_pinUp != 0) {
pinMode(this->_pinUp, INPUT_PULLUP);
attachInterrupt(this->_pinUp, onIntUp, RISING);
attachInterrupt(this->_pinUp, onIntUp, FALLING);
}
LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress);
this->setInterval(100);
this->setInterval(20);
}
int32_t UpDownInterruptBase::runOnce()
{
InputEvent e;
InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE;
unsigned long now = millis();
if (this->action == UPDOWN_ACTION_PRESSED) {
if (now - lastPressKeyTime >= pressDebounceMs) {
lastPressKeyTime = now;
LOG_DEBUG("GPIO event Press");
e.inputEvent = this->_eventPressed;
// Read all button states once at the beginning
bool pressButtonPressed = !digitalRead(_pinPress);
bool upButtonPressed = !digitalRead(_pinUp);
bool downButtonPressed = !digitalRead(_pinDown);
// Handle initial button press detection - only if not already detected
if (this->action == UPDOWN_ACTION_PRESSED && pressButtonPressed && !pressDetected) {
pressDetected = true;
pressStartTime = now;
} else if (this->action == UPDOWN_ACTION_UP && upButtonPressed && !upDetected) {
upDetected = true;
upStartTime = now;
} else if (this->action == UPDOWN_ACTION_DOWN && downButtonPressed && !downDetected) {
downDetected = true;
downStartTime = now;
}
// Handle long press detection for press button
if (pressDetected && pressStartTime > 0) {
uint32_t pressDuration = now - pressStartTime;
if (!pressButtonPressed) {
// Button released
if (pressDuration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
lastPressKeyTime = now;
e.inputEvent = this->_eventPressed;
}
// Reset state
pressDetected = false;
pressStartTime = 0;
lastPressLongEventTime = 0;
} else if (pressDuration >= LONG_PRESS_DURATION && lastPressLongEventTime == 0) {
// First long press event only - avoid repeated events causing lag
e.inputEvent = this->_eventPressedLong;
lastPressLongEventTime = now;
}
} else if (this->action == UPDOWN_ACTION_UP) {
if (now - lastUpKeyTime >= updownDebounceMs) {
lastUpKeyTime = now;
LOG_DEBUG("GPIO event Up");
e.inputEvent = this->_eventUp;
}
// Handle long press detection for up button
if (upDetected && upStartTime > 0) {
uint32_t upDuration = now - upStartTime;
if (!upButtonPressed) {
// Button released
if (upDuration < LONG_PRESS_DURATION && now - lastUpKeyTime >= updownDebounceMs) {
lastUpKeyTime = now;
e.inputEvent = this->_eventUp;
}
// Reset state
upDetected = false;
upStartTime = 0;
lastUpLongEventTime = 0;
} else if (upDuration >= LONG_PRESS_DURATION) {
// Auto-repeat long press events
if (lastUpLongEventTime == 0 || (now - lastUpLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
e.inputEvent = this->_eventUpLong;
lastUpLongEventTime = now;
}
}
} else if (this->action == UPDOWN_ACTION_DOWN) {
if (now - lastDownKeyTime >= updownDebounceMs) {
lastDownKeyTime = now;
LOG_DEBUG("GPIO event Down");
e.inputEvent = this->_eventDown;
}
// Handle long press detection for down button
if (downDetected && downStartTime > 0) {
uint32_t downDuration = now - downStartTime;
if (!downButtonPressed) {
// Button released
if (downDuration < LONG_PRESS_DURATION && now - lastDownKeyTime >= updownDebounceMs) {
lastDownKeyTime = now;
e.inputEvent = this->_eventDown;
}
// Reset state
downDetected = false;
downStartTime = 0;
lastDownLongEventTime = 0;
} else if (downDuration >= LONG_PRESS_DURATION) {
// Auto-repeat long press events
if (lastDownLongEventTime == 0 || (now - lastDownLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
e.inputEvent = this->_eventDownLong;
lastDownLongEventTime = now;
}
}
}
@@ -69,8 +142,11 @@ int32_t UpDownInterruptBase::runOnce()
this->notifyObservers(&e);
}
this->action = UPDOWN_ACTION_NONE;
return 100;
if (!pressDetected && !upDetected && !downDetected) {
this->action = UPDOWN_ACTION_NONE;
}
return 20; // This will control how the input frequency
}
void UpDownInterruptBase::intPressHandler()

View File

@@ -8,7 +8,8 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, public concur
public:
explicit UpDownInterruptBase(const char *name);
void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp,
input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(),
input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong,
input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(),
unsigned long updownDebounceMs = 50);
void intPressHandler();
void intDownHandler();
@@ -17,16 +18,41 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, public concur
int32_t runOnce() override;
protected:
enum UpDownInterruptBaseActionType { UPDOWN_ACTION_NONE, UPDOWN_ACTION_PRESSED, UPDOWN_ACTION_UP, UPDOWN_ACTION_DOWN };
enum UpDownInterruptBaseActionType {
UPDOWN_ACTION_NONE,
UPDOWN_ACTION_PRESSED,
UPDOWN_ACTION_PRESSED_LONG,
UPDOWN_ACTION_UP,
UPDOWN_ACTION_UP_LONG,
UPDOWN_ACTION_DOWN,
UPDOWN_ACTION_DOWN_LONG
};
volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE;
// Long press detection variables
uint32_t pressStartTime = 0;
uint32_t upStartTime = 0;
uint32_t downStartTime = 0;
bool pressDetected = false;
bool upDetected = false;
bool downDetected = false;
uint32_t lastPressLongEventTime = 0;
uint32_t lastUpLongEventTime = 0;
uint32_t lastDownLongEventTime = 0;
static const uint32_t LONG_PRESS_DURATION = 300;
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300;
private:
uint8_t _pinDown = 0;
uint8_t _pinUp = 0;
uint8_t _pinPress = 0;
input_broker_event _eventDown = INPUT_BROKER_NONE;
input_broker_event _eventUp = INPUT_BROKER_NONE;
input_broker_event _eventPressed = INPUT_BROKER_NONE;
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
input_broker_event _eventUpLong = INPUT_BROKER_NONE;
input_broker_event _eventDownLong = INPUT_BROKER_NONE;
const char *_originName;
unsigned long lastUpKeyTime = 0;

View File

@@ -1,5 +1,6 @@
#include "UpDownInterruptImpl1.h"
#include "InputBroker.h"
extern bool osk_found;
UpDownInterruptImpl1 *upDownInterruptImpl1;
@@ -17,13 +18,18 @@ bool UpDownInterruptImpl1::init()
uint8_t pinDown = moduleConfig.canned_message.inputbroker_pin_b;
uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press;
input_broker_event eventDown = INPUT_BROKER_DOWN;
input_broker_event eventUp = INPUT_BROKER_UP;
input_broker_event eventDown = INPUT_BROKER_USER_PRESS; // acts like RIGHT/DOWN
input_broker_event eventUp = INPUT_BROKER_ALT_PRESS; // acts like LEFT/UP
input_broker_event eventPressed = INPUT_BROKER_SELECT;
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
input_broker_event eventUpLong = INPUT_BROKER_UP_LONG;
input_broker_event eventDownLong = INPUT_BROKER_DOWN_LONG;
UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, UpDownInterruptImpl1::handleIntDown,
UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed);
UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, eventPressedLong, eventUpLong,
eventDownLong, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp,
UpDownInterruptImpl1::handleIntPressed);
inputBroker->registerSource(this);
osk_found = true;
return true;
}

View File

@@ -90,7 +90,7 @@ int32_t KbI2cBase::runOnce()
while (keyCount--) {
const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent();
if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) {
InputEvent e;
InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
switch (key.key) {
@@ -187,7 +187,7 @@ int32_t KbI2cBase::runOnce()
}
case 0x37: { // MPR121
MPRkeyboard.trigger();
InputEvent e;
InputEvent e = {};
while (MPRkeyboard.hasEvent()) {
char nextEvent = MPRkeyboard.dequeueEvent();
@@ -250,7 +250,7 @@ int32_t KbI2cBase::runOnce()
}
case 0x84: { // Adafruit TCA8418
TCAKeyboard.trigger();
InputEvent e;
InputEvent e = {};
while (TCAKeyboard.hasEvent()) {
char nextEvent = TCAKeyboard.dequeueEvent();
e.inputEvent = INPUT_BROKER_ANYKEY;
@@ -333,6 +333,7 @@ int32_t KbI2cBase::runOnce()
}
TCAKeyboard.trigger();
}
TCAKeyboard.clearInt();
break;
}
case 0x02: {
@@ -350,7 +351,7 @@ int32_t KbI2cBase::runOnce()
}
if (PrintDataBuf != 0) {
LOG_DEBUG("RAK14004 key 0x%x pressed", PrintDataBuf);
InputEvent e;
InputEvent e = {};
e.inputEvent = INPUT_BROKER_MATRIXKEY;
e.source = this->_originName;
e.kbchar = PrintDataBuf;
@@ -365,7 +366,7 @@ int32_t KbI2cBase::runOnce()
if (i2cBus->available()) {
char c = i2cBus->read();
InputEvent e;
InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
switch (c) {
@@ -519,4 +520,11 @@ int32_t KbI2cBase::runOnce()
LOG_WARN("Unknown kb_model 0x%02x", kb_model);
}
return 300;
}
}
void KbI2cBase::toggleBacklight(bool on)
{
#if defined(T_LORA_PAGER)
TCAKeyboard.setBacklight(on);
#endif
}

View File

@@ -12,6 +12,7 @@ class KbI2cBase : public Observable<const InputEvent *>, public concurrency::OST
{
public:
explicit KbI2cBase(const char *name);
void toggleBacklight(bool on);
protected:
virtual int32_t runOnce() override;

View File

@@ -72,7 +72,7 @@ int32_t KbMatrixBase::runOnce()
if (key != 0) {
LOG_DEBUG("Key 0x%x pressed", key);
// reset shift now that we have a keypress
InputEvent e;
InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
switch (key) {

View File

@@ -192,6 +192,8 @@ ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE;
uint8_t kb_model;
// global bool to record that a kb is present
bool kb_found = false;
// global bool to record that on-screen keyboard (OSK) is present
bool osk_found = false;
// The I2C address of the RTC Module (if found)
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
@@ -295,6 +297,12 @@ void printInfo()
#ifndef PIO_UNIT_TESTING
void setup()
{
#if defined(R1_NEO)
pinMode(DCDC_EN_HOLD, OUTPUT);
digitalWrite(DCDC_EN_HOLD, HIGH);
pinMode(NRF_ON, OUTPUT);
digitalWrite(NRF_ON, HIGH);
#endif
#if defined(PIN_POWER_EN)
pinMode(PIN_POWER_EN, OUTPUT);
@@ -367,6 +375,7 @@ void setup()
digitalWrite(SDCARD_CS, HIGH);
pinMode(TFT_CS, OUTPUT);
digitalWrite(TFT_CS, HIGH);
pinMode(KB_INT, INPUT_PULLUP);
// io expander
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
@@ -387,7 +396,7 @@ void setup()
#endif
concurrency::hasBeenSetup = true;
#if ARCH_PORTDUINO
SPISettings spiSettings(settingsMap[spiSpeed], MSBFIRST, SPI_MODE0);
SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
#else
SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0);
#endif
@@ -532,9 +541,9 @@ void setup()
#elif defined(I2C_SDA) && !defined(ARCH_RP2040)
Wire.begin(I2C_SDA, I2C_SCL);
#elif defined(ARCH_PORTDUINO)
if (settingsStrings[i2cdev] != "") {
LOG_INFO("Use %s as I2C device", settingsStrings[i2cdev].c_str());
Wire.begin(settingsStrings[i2cdev].c_str());
if (portduino_config.i2cdev != "") {
LOG_INFO("Use %s as I2C device", portduino_config.i2cdev.c_str());
Wire.begin(portduino_config.i2cdev.c_str());
} else {
LOG_INFO("No I2C device configured, Skip");
}
@@ -586,7 +595,7 @@ void setup()
#if defined(I2C_SDA)
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE);
#elif defined(ARCH_PORTDUINO)
if (settingsStrings[i2cdev] != "") {
if (portduino_config.i2cdev != "") {
LOG_INFO("Scan for i2c devices");
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE);
}
@@ -790,14 +799,7 @@ void setup()
}
#endif
// If we're taking on the repeater role, use NextHopRouter and turn off 3V3_S rail because peripherals are not needed
if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
router = new NextHopRouter();
#ifdef PIN_3V3_EN
digitalWrite(PIN_3V3_EN, LOW);
#endif
} else
router = new ReliableRouter();
router = new ReliableRouter();
// only play start melody when role is not tracker or sensor
if (config.power.is_power_saving == true &&
@@ -859,7 +861,7 @@ void setup()
SPI.begin(false);
#endif // HW_SPI1_DEVICE
#elif ARCH_PORTDUINO
if (settingsStrings[spidev] != "ch341") {
if (portduino_config.lora_spi_dev != "ch341") {
SPI.begin();
}
#elif !defined(ARCH_ESP32) // ARCH_RP2040
@@ -886,7 +888,7 @@ void setup()
defined(USE_SPISSD1306)
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
#elif defined(ARCH_PORTDUINO)
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) &&
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) &&
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
}
@@ -923,8 +925,7 @@ void setup()
if (sensor_detected == false) {
#endif
if (HAS_GPS) {
if (config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER &&
config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
gps = GPS::createGps();
if (gps) {
gpsStatus->observe(&gps->newStatus);
@@ -987,18 +988,19 @@ void setup()
#endif
#if defined(ARCH_PORTDUINO)
if (settingsMap.count(userButtonPin) != 0 && settingsMap[userButtonPin] != RADIOLIB_NC) {
if (portduino_config.userButtonPin.enabled) {
LOG_DEBUG("Use GPIO%02d for button", settingsMap[userButtonPin]);
LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin);
UserButtonThread = new ButtonThread("UserButton");
if (screen) {
ButtonConfig config;
config.pinNumber = (uint8_t)settingsMap[userButtonPin];
config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin;
config.activeLow = true;
config.activePullup = true;
config.pullupSense = INPUT_PULLUP;
config.intRoutine = []() {
UserButtonThread->userButton.tick();
UserButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
mainDelay.interruptFromISR(&higherWake);
@@ -1019,6 +1021,7 @@ void setup()
touchConfig.pullupSense = pullup_sense;
touchConfig.intRoutine = []() {
TouchButtonThread->userButton.tick();
TouchButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
mainDelay.interruptFromISR(&higherWake);
@@ -1038,6 +1041,7 @@ void setup()
cancelConfig.pullupSense = pullup_sense;
cancelConfig.intRoutine = []() {
CancelButtonThread->userButton.tick();
CancelButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
mainDelay.interruptFromISR(&higherWake);
@@ -1058,6 +1062,7 @@ void setup()
backConfig.pullupSense = pullup_sense;
backConfig.intRoutine = []() {
BackButtonThread->userButton.tick();
BackButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
mainDelay.interruptFromISR(&higherWake);
@@ -1092,6 +1097,7 @@ void setup()
userConfig.pullupSense = pullup_sense;
userConfig.intRoutine = []() {
UserButtonThread->userButton.tick();
UserButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
mainDelay.interruptFromISR(&higherWake);
@@ -1109,6 +1115,7 @@ void setup()
userConfigNoScreen.pullupSense = pullup_sense;
userConfigNoScreen.intRoutine = []() {
UserButtonThread->userButton.tick();
UserButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
mainDelay.interruptFromISR(&higherWake);
@@ -1151,7 +1158,7 @@ void setup()
if (screen)
screen->setup();
#elif defined(ARCH_PORTDUINO)
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) &&
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) &&
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
screen->setup();
}
@@ -1167,15 +1174,10 @@ void setup()
#endif
#ifdef ARCH_PORTDUINO
const struct {
configNames cfgName;
std::string strName;
} loraModules[] = {{use_rf95, "RF95"}, {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"}, {use_sx1280, "sx1280"},
{use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}};
// as one can't use a function pointer to the class constructor:
auto loraModuleInterface = [](configNames cfgName, LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq,
RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) {
switch (cfgName) {
auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
RADIOLIB_PIN_TYPE busy) {
switch (portduino_config.lora_module) {
case use_rf95:
return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy);
case use_sx1262:
@@ -1192,31 +1194,34 @@ void setup()
return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy);
case use_llcc68:
return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy);
case use_simradio:
return (RadioInterface *)new SimRadio;
default:
assert(0); // shouldn't happen
return (RadioInterface *)nullptr;
}
};
for (auto &loraModule : loraModules) {
if (settingsMap[loraModule.cfgName] && !rIf) {
LOG_DEBUG("Activate %s radio on SPI port %s", loraModule.strName.c_str(), settingsStrings[spidev].c_str());
if (settingsStrings[spidev] == "ch341") {
RadioLibHAL = ch341Hal;
} else {
RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
}
rIf = loraModuleInterface(loraModule.cfgName, (LockingArduinoHal *)RadioLibHAL, settingsMap[cs_pin],
settingsMap[irq_pin], settingsMap[reset_pin], settingsMap[busy_pin]);
if (!rIf->init()) {
LOG_WARN("No %s radio", loraModule.strName.c_str());
delete rIf;
rIf = NULL;
exit(EXIT_FAILURE);
} else {
LOG_INFO("%s init success", loraModule.strName.c_str());
}
}
LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(),
portduino_config.lora_spi_dev.c_str());
if (portduino_config.lora_spi_dev == "ch341") {
RadioLibHAL = ch341Hal;
} else {
RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
}
rIf =
loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin,
portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin);
if (!rIf->init()) {
LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str());
delete rIf;
rIf = NULL;
exit(EXIT_FAILURE);
} else {
LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str());
}
#elif defined(HW_SPI1_DEVICE)
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings);
#else // HW_SPI1_DEVICE
@@ -1238,20 +1243,6 @@ void setup()
}
#endif
#if defined(ARCH_PORTDUINO)
if (!rIf) {
rIf = new SimRadio;
if (!rIf->init()) {
LOG_WARN("No simulated radio");
delete rIf;
rIf = NULL;
} else {
LOG_INFO("Use SIMULATED radio!");
radioType = SIM_RADIO;
}
}
#endif
#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1);
@@ -1455,6 +1446,10 @@ void setup()
#endif
#endif
#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
osk_found = true;
#endif
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER
// Start web server thread.
webServerThread = new WebServerThread();
@@ -1462,7 +1457,7 @@ void setup()
#ifdef ARCH_PORTDUINO
#if __has_include(<ulfius.h>)
if (settingsMap[webserverport] != -1) {
if (portduino_config.webserverport != -1) {
piwebServerThread = new PiWebServerThread();
std::atexit([] { delete piwebServerThread; });
}
@@ -1616,6 +1611,9 @@ void loop()
// We want to sleep as long as possible here - because it saves power
if (!runASAP && loopCanSleep()) {
#ifdef DEBUG_LOOP_TIMING
LOG_DEBUG("main loop delay: %d", delayMsec);
#endif
mainDelay.delay(delayMsec);
}
}

View File

@@ -32,6 +32,7 @@ extern ScanI2C::DeviceAddress screen_found;
extern ScanI2C::DeviceAddress cardkb_found;
extern uint8_t kb_model;
extern bool kb_found;
extern bool osk_found;
extern ScanI2C::DeviceAddress rtc_found;
extern ScanI2C::DeviceAddress accelerometer_found;
extern ScanI2C::FoundDevice rgb_found;

View File

@@ -423,6 +423,33 @@ bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash)
}
}
bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash)
{
// Iterate all known presets
for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX;
++preset) {
const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false,
config.lora.use_preset);
if (!name)
continue;
if (strcmp(name, "Invalid") == 0)
continue; // skip invalid placeholder
uint8_t h = xorHash((const uint8_t *)name, strlen(name));
// Expand default PSK alias 1 to actual bytes and xor into hash
uint8_t tmp = h ^ xorHash(defaultpsk, sizeof(defaultpsk));
if (tmp == channelHash) {
// Set crypto to defaultpsk and report success
CryptoKey k;
memcpy(k.bytes, defaultpsk, sizeof(defaultpsk));
k.length = sizeof(defaultpsk);
crypto->setKey(k);
LOG_INFO("Matched default preset '%s' for hash 0x%x; set default PSK", name, channelHash);
return true;
}
}
return false;
}
/** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured)
*
* This method is called before encoding outbound packets
@@ -432,4 +459,15 @@ bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash)
int16_t Channels::setActiveByIndex(ChannelIndex channelIndex)
{
return setCrypto(channelIndex);
}
int8_t Channels::getIndexByHash(ChannelHash channelHash)
{
// Iterate through all channels to find the one with matching hash
for (ChannelIndex i = 0; i < getNumChannels(); i++) {
if (getHash(i) == channelHash) {
return i;
}
}
return -1; // Hash not found
}

View File

@@ -94,6 +94,11 @@ class Channels
bool ensureLicensedOperation();
bool setDefaultPresetCryptoForHash(ChannelHash channelHash);
/** Return the channel index for the specified channel hash, or -1 for not found */
int8_t getIndexByHash(ChannelHash channelHash);
private:
/** Given a channel index, change to use the crypto key specified by that index
*
@@ -101,9 +106,6 @@ class Channels
*/
int16_t setCrypto(ChannelIndex chIndex);
/** Return the channel index for the specified channel hash, or -1 for not found */
int8_t getIndexByHash(ChannelHash channelHash);
/** Given a channel number, return the (0 to 255) hash for that channel
* If no suitable channel could be found, return -1
*

View File

@@ -1,7 +1,12 @@
#include "FloodingRouter.h"
#include "MeshTypes.h"
#include "NodeDB.h"
#include "configuration.h"
#include "mesh-pb-constants.h"
#include "meshUtils.h"
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
#include "modules/TraceRouteModule.h"
#endif
FloodingRouter::FloodingRouter() {}
@@ -21,7 +26,41 @@ ErrorCode FloodingRouter::send(meshtastic_MeshPacket *p)
bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
{
if (wasSeenRecently(p)) { // Note: this will also add a recent packet record
bool wasUpgraded = false;
bool seenRecently =
wasSeenRecently(p, true, nullptr, nullptr, &wasUpgraded); // Updates history; returns false when an upgrade is detected
// Handle hop_limit upgrade scenario for rebroadcasters
// isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages
if (wasUpgraded && isRebroadcaster() && iface && p->hop_limit > 0) {
// wasSeenRecently() reports false in upgrade cases so we handle replacement before the duplicate short-circuit
// If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to
// rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead.
uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining
if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) {
LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id,
p->hop_limit, dropThreshold);
if (nodeDB)
nodeDB->updateFrom(*p);
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP)
traceRouteModule->processUpgradedPacket(*p);
#endif
perhapsRebroadcast(p);
// We already enqueued the improved copy, so make sure the incoming packet stops here.
return true;
}
// No queue entry was replaced by this upgraded copy, so treat it as a duplicate to avoid
// delivering the same packet to applications/phone twice with different hop limits.
seenRecently = true;
}
if (seenRecently) {
printPacket("Ignore dupe incoming msg", p);
rxDupe++;
@@ -43,13 +82,30 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
return Router::shouldFilterReceived(p);
}
bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p)
{
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
// ROUTER, ROUTER_LATE should never cancel relaying a packet (i.e. we should always rebroadcast),
// even if we've heard another station rebroadcast it already.
return false;
}
if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
// CLIENT_BASE: if the packet is from or to a favorited node,
// we should act like a ROUTER and should never cancel a rebroadcast (i.e. we should always rebroadcast),
// even if we've heard another station rebroadcast it already.
return !nodeDB->isFromOrToFavoritedNode(*p);
}
// All other roles (such as CLIENT) should cancel a rebroadcast if they hear another station's rebroadcast.
return true;
}
void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p)
{
if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE &&
p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) {
// cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater!
if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && roleAllowsCancelingDupe(p)) {
// cancel rebroadcast of this message *if* there was already one, unless we're a router!
// But only LoRa packets should be able to trigger this.
if (Router::cancelSending(p->from, p->id))
txRelayCanceled++;
@@ -72,7 +128,12 @@ void FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
if (isRebroadcaster()) {
meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it
tosend->hop_limit--; // bump down the hop count
// Use shared logic to determine if hop_limit should be decremented
if (shouldDecrementHopLimit(p)) {
tosend->hop_limit--; // bump down the hop count
} else {
LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE flood: preserving hop_limit");
}
#if USERPREFS_EVENT_MODE
if (tosend->hop_limit > 2) {
// if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away.
@@ -80,12 +141,12 @@ void FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
tosend->hop_limit = 2;
}
#endif
tosend->next_hop = NO_NEXT_HOP_PREFERENCE; // this should already be the case, but just in case
LOG_INFO("Rebroadcast received floodmsg");
// Note: we are careful to resend using the original senders node id
// We are careful not to call our hooked version of send() - because we don't want to check this again
Router::send(tosend);
send(tosend);
} else {
LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE");
}
@@ -109,4 +170,4 @@ void FloodingRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas
// handle the packet as normal
Router::sniffReceived(p, c);
}
}

View File

@@ -59,6 +59,10 @@ class FloodingRouter : public Router
*/
virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override;
// Return false for roles like ROUTER which should always rebroadcast even when we've heard another rebroadcast of
// the same packet
bool roleAllowsCancelingDupe(const meshtastic_MeshPacket *p);
/* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */
void perhapsCancelDupe(const meshtastic_MeshPacket *p);

View File

@@ -21,7 +21,7 @@ static const Module::RfSwitchMode_t rfswitch_table[] = {
// Particular boards might define a different max power based on what their hardware can do, default to max power output if not
// specified (may be dangerous if using external PA and LR11x0 power config forgotten)
#if ARCH_PORTDUINO
#define LR1110_MAX_POWER settingsMap[lr1110_max_power]
#define LR1110_MAX_POWER portduino_config.lr1110_max_power
#endif
#ifndef LR1110_MAX_POWER
#define LR1110_MAX_POWER 22
@@ -30,7 +30,7 @@ static const Module::RfSwitchMode_t rfswitch_table[] = {
// the 2.4G part maxes at 13dBm
#if ARCH_PORTDUINO
#define LR1120_MAX_POWER settingsMap[lr1120_max_power]
#define LR1120_MAX_POWER portduino_config.lr1120_max_power
#endif
#ifndef LR1120_MAX_POWER
#define LR1120_MAX_POWER 13
@@ -55,7 +55,7 @@ template <typename T> bool LR11x0Interface<T>::init()
#endif
#if ARCH_PORTDUINO
float tcxoVoltage = (float)settingsMap[dio3_tcxo_voltage] / 1000;
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE
#elif !defined(LR11X0_DIO3_TCXO_VOLTAGE)
float tcxoVoltage =
@@ -155,7 +155,7 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
err = lora.setBandwidth(bw);
err = lora.setBandwidth(bw, wideLora() && (getFreq() > 1000.0f));
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);

View File

@@ -103,12 +103,26 @@ meshtastic_MeshPacket *MeshPacketQueue::getFront()
return p;
}
/** Attempt to find and remove a packet from this queue. Returns a pointer to the removed packet, or NULL if not found */
meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool tx_normal, bool tx_late)
/** Get a packet from this queue. Returns a pointer to the packet, or NULL if not found. */
meshtastic_MeshPacket *MeshPacketQueue::getPacketFromQueue(NodeNum from, PacketId id)
{
for (auto it = queue.begin(); it != queue.end(); it++) {
auto p = (*it);
if (getFrom(p) == from && p->id == id && ((tx_normal && !p->tx_after) || (tx_late && p->tx_after))) {
if (getFrom(p) == from && p->id == id) {
return p;
}
}
return NULL;
}
/** Attempt to find and remove a packet from this queue. Returns a pointer to the removed packet, or NULL if not found */
meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool tx_normal, bool tx_late, uint8_t hop_limit_lt)
{
for (auto it = queue.begin(); it != queue.end(); it++) {
auto p = (*it);
if (getFrom(p) == from && p->id == id && ((tx_normal && !p->tx_after) || (tx_late && p->tx_after)) &&
(!hop_limit_lt || p->hop_limit < hop_limit_lt)) {
queue.erase(it);
return p;
}
@@ -120,14 +134,7 @@ meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool t
/* Attempt to find a packet from this queue. Return true if it was found. */
bool MeshPacketQueue::find(const NodeNum from, const PacketId id)
{
for (auto it = queue.begin(); it != queue.end(); it++) {
const auto *p = *it;
if (getFrom(p) == from && p->id == id) {
return true;
}
}
return false;
return getPacketFromQueue(from, id) != NULL;
}
/**

View File

@@ -35,8 +35,12 @@ class MeshPacketQueue
meshtastic_MeshPacket *getFront();
/** Get a packet from this queue. Returns a pointer to the packet, or NULL if not found. */
meshtastic_MeshPacket *getPacketFromQueue(NodeNum from, PacketId id);
/** Attempt to find and remove a packet from this queue. Returns the packet which was removed from the queue */
meshtastic_MeshPacket *remove(NodeNum from, PacketId id, bool tx_normal = true, bool tx_late = true);
meshtastic_MeshPacket *remove(NodeNum from, PacketId id, bool tx_normal = true, bool tx_late = true,
uint8_t hop_limit_lt = 0);
/* Attempt to find a packet from this queue. Return true if it was found. */
bool find(const NodeNum from, const PacketId id);

View File

@@ -85,12 +85,11 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp)
powerFSM.trigger(EVENT_PACKET_FOR_PHONE); // Possibly keep the node from sleeping
nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio
bool isPreferredRebroadcaster =
IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_REPEATER);
bool isPreferredRebroadcaster = config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER;
if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) {
LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); // because this potentially a Repeater which will
// ignore our request for its NodeInfo
LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo");
// ignore our request for its NodeInfo
} else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user &&
nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) {
if (airTime->isTxAllowedChannelUtil(true)) {
@@ -453,4 +452,4 @@ uint32_t MeshService::GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp)
delta = 0;
return delta;
}
}

View File

@@ -190,4 +190,4 @@ class MeshService
friend class RoutingModule;
};
extern MeshService *service;
extern MeshService *service;

View File

@@ -1,4 +1,10 @@
#include "NextHopRouter.h"
#include "MeshTypes.h"
#include "meshUtils.h"
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
#include "modules/TraceRouteModule.h"
#endif
#include "NodeDB.h"
NextHopRouter::NextHopRouter() {}
@@ -32,7 +38,39 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
{
bool wasFallback = false;
bool weWereNextHop = false;
if (wasSeenRecently(p, true, &wasFallback, &weWereNextHop)) { // Note: this will also add a recent packet record
bool wasUpgraded = false;
bool seenRecently = wasSeenRecently(p, true, &wasFallback, &weWereNextHop,
&wasUpgraded); // Updates history; returns false when an upgrade is detected
// Handle hop_limit upgrade scenario for rebroadcasters
// isRebroadcaster() is duplicated in perhapsRelay(), but this avoids confusing log messages
if (wasUpgraded && isRebroadcaster() && iface && p->hop_limit > 0) {
// Upgrade detection bypasses the duplicate short-circuit so we replace the queued packet before exiting
uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining
if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) {
LOG_DEBUG("Processing upgraded packet 0x%08x for relay with hop limit %d (dropping queued < %d)", p->id, p->hop_limit,
dropThreshold);
if (nodeDB)
nodeDB->updateFrom(*p);
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP)
traceRouteModule->processUpgradedPacket(*p);
#endif
perhapsRelay(p);
// We already enqueued the improved copy, so make sure the incoming packet stops here.
return true;
}
// No queue entry was replaced by this upgraded copy, so treat it as a duplicate to avoid
// delivering the same packet to applications/phone twice with different hop limits.
seenRecently = true;
}
if (seenRecently) {
printPacket("Ignore dupe incoming msg", p);
if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) {
@@ -76,11 +114,14 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
if (origTx) {
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came directly
// from the destination
if (wasRelayer(p->relay_node, p->decoded.request_id, p->to) ||
(p->hop_start != 0 && p->hop_start == p->hop_limit &&
wasSoleRelayer(ourRelayID, p->decoded.request_id, p->to))) {
bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to);
bool weWereSoleRelayer = false;
bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer);
if ((weWereRelayer && wasAlreadyRelayer) ||
(p->hop_start != 0 && p->hop_start == p->hop_limit && weWereSoleRelayer)) {
if (origTx->next_hop != p->relay_node) { // Not already set
LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply", p->from, p->relay_node);
LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from,
p->relay_node, wasAlreadyRelayer, weWereSoleRelayer);
origTx->next_hop = p->relay_node;
}
}
@@ -108,7 +149,13 @@ bool NextHopRouter::perhapsRelay(const meshtastic_MeshPacket *p)
meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it
LOG_INFO("Relaying received message coming from %x", p->relay_node);
tosend->hop_limit--; // bump down the hop count
// Use shared logic to determine if hop_limit should be decremented
if (shouldDecrementHopLimit(p)) {
tosend->hop_limit--; // bump down the hop count
} else {
LOG_INFO("Router/CLIENT_BASE-to-favorite-router/CLIENT_BASE relay: preserving hop_limit");
}
NextHopRouter::send(tosend);
return true;
@@ -127,7 +174,6 @@ bool NextHopRouter::perhapsRelay(const meshtastic_MeshPacket *p)
*/
uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node)
{
// When we're a repeater router->sniffReceived will call NextHopRouter directly without checking for broadcast
if (isBroadcast(to))
return NO_NEXT_HOP_PREFERENCE;
@@ -161,6 +207,15 @@ bool NextHopRouter::stopRetransmission(NodeNum from, PacketId id)
return stopRetransmission(key);
}
bool NextHopRouter::roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p)
{
// Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once)
// Return false for roles like ROUTER, ROUTER_LATE which should always transmit the packet at least once.
return roleAllowsCancelingDupe(p); // same logic as FloodingRouter::roleAllowsCancelingDupe
}
bool NextHopRouter::stopRetransmission(GlobalPacketId key)
{
auto old = findPendingPacket(key);
@@ -169,10 +224,8 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key)
/* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue
to avoid canceling a transmission if it was ACKed super fast via MQTT */
if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) {
// We only cancel it if we are the original sender or if we're not a router(_late)/repeater
if (isFromUs(p) || (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) {
// We only cancel it if we are the original sender or if we're not a router(_late)
if (isFromUs(p) || roleAllowsCancelingFromTxQueue(p)) {
// remove the 'original' (identified by originator and packet->id) from the txqueue and free it
cancelSending(getFrom(p), p->id);
}
@@ -284,4 +337,4 @@ void NextHopRouter::setNextTx(PendingPacket *pending)
LOG_DEBUG("Setting next retransmission in %u msecs: ", d);
printPacket("", pending->packet);
setReceivedMessage(); // Run ASAP, so we can figure out our correct sleep time
}
}

View File

@@ -121,6 +121,9 @@ class NextHopRouter : public FloodingRouter
*/
PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX);
// Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once)
bool roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p);
/**
* Stop any retransmissions we are doing of the specified node/packet ID pair
*

View File

@@ -204,7 +204,7 @@ NodeDB::NodeDB()
int saveWhat = 0;
// Get device unique id
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
uint32_t unique_id[4];
// ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series
// This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us
@@ -554,10 +554,9 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
#endif
#ifdef USERPREFS_CONFIG_DEVICE_ROLE
// Restrict ROUTER*, LOST AND FOUND, and REPEATER roles for security reasons
// Restrict ROUTER*, LOST AND FOUND roles for security reasons
if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER,
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_REPEATER,
meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND)) {
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND)) {
LOG_WARN("ROUTER roles are restricted, falling back to CLIENT role");
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
} else {
@@ -673,7 +672,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
#endif
#elif ARCH_PORTDUINO
bool hasScreen = false;
if (settingsMap[displayPanel])
if (portduino_config.displayPanel)
hasScreen = true;
else
hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C;
@@ -701,7 +700,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
#ifdef USERPREFS_NETWORK_ENABLED_PROTOCOLS
config.network.enabled_protocols = USERPREFS_NETWORK_ENABLED_PROTOCOLS;
#else
config.network.enabled_protocols = 1;
config.network.enabled_protocols = 0;
#endif
#endif
@@ -906,11 +905,6 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role)
moduleConfig.telemetry.device_update_interval = ONE_DAY;
owner.has_is_unmessagable = true;
owner.is_unmessagable = true;
} else if (role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
owner.has_is_unmessagable = true;
owner.is_unmessagable = true;
config.display.screen_on_secs = 1;
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY;
} else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) {
owner.has_is_unmessagable = true;
owner.is_unmessagable = true;
@@ -1334,8 +1328,8 @@ void NodeDB::loadFromDisk()
}
#if ARCH_PORTDUINO
// set any config overrides
if (settingsMap[has_configDisplayMode]) {
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)settingsMap[configDisplayMode];
if (portduino_config.has_configDisplayMode) {
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode;
}
#endif
@@ -1603,9 +1597,18 @@ void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxS
void NodeDB::addFromContact(meshtastic_SharedContact contact)
{
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(contact.node_num);
if (!info) {
if (!info || !contact.has_user) {
return;
}
// If the local node has this node marked as manually verified
// and the client does not, do not allow the client to update the
// saved public key.
if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) {
if (contact.user.public_key.size != info->user.public_key.size ||
memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) {
return;
}
}
info->num = contact.node_num;
info->has_user = true;
info->user = TypeConversions::ConvertToUserLite(contact.user);
@@ -1620,10 +1623,12 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
} else {
info->last_heard = getValidTime(RTCQualityNTP);
info->is_favorite = true;
info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
// As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified
if (contact.manually_verified) {
info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
}
// Mark the node's key as manually verified to indicate trustworthiness.
updateGUIforNode = info;
// powerFSM.trigger(EVENT_NODEDB_UPDATED); This event has been retired
sortMeshDB();
notifyObservers(true); // Force an update whether or not our node counts have changed
}
@@ -1667,9 +1672,6 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
return false;
}
LOG_INFO("Public Key set for node, not updating!");
// we copy the key into the incoming packet, to prevent overwrite
p.public_key.size = 32;
memcpy(p.public_key.bytes, info->user.public_key.bytes, 32);
} else if (p.public_key.size == 32) {
LOG_INFO("Update Node Pubkey!");
}
@@ -1750,6 +1752,65 @@ void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId)
}
}
bool NodeDB::isFavorite(uint32_t nodeId)
{
// returns true if nodeId is_favorite; false if not or not found
// NODENUM_BROADCAST will never be in the DB
if (nodeId == NODENUM_BROADCAST)
return false;
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
if (lite) {
return lite->is_favorite;
}
return false;
}
bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p)
{
// This method is logically equivalent to:
// return isFavorite(p.from) || isFavorite(p.to);
// but is more efficient by:
// 1. doing only one pass through the database, instead of two
// 2. exiting early when a favorite is found, or if both from and to have been seen
if (p.to == NODENUM_BROADCAST)
return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from
meshtastic_NodeInfoLite *lite = NULL;
bool seenFrom = false;
bool seenTo = false;
for (int i = 0; i < numMeshNodes; i++) {
lite = &meshNodes->at(i);
if (lite->num == p.from) {
if (lite->is_favorite)
return true;
seenFrom = true;
}
if (lite->num == p.to) {
if (lite->is_favorite)
return true;
seenTo = true;
}
if (seenFrom && seenTo)
return false; // we've seen both, and neither is a favorite, so we can stop searching early
// Note: if we knew that sortMeshDB was always called after any change to is_favorite, we could exit early after searching
// all favorited nodes first.
}
return false;
}
void NodeDB::pause_sort(bool paused)
{
sortingIsPaused = paused;

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