Compare commits

...
Author SHA1 Message Date
Ben Meadors 38c4001f52 Merge branch 'develop' into feature/rak4631-lockdown 2026-03-02 06:40:58 -06:00
m1nlandBen Meadors 80af726877 avoid memory leak when possibly malformed packet received (#9781)
getByIndex allocates memory and returns dummy channel whenever
chIndex validation fails. Comment implies this may happen
when malformed packet is received. The fix changes implementation
so static dummyChannel is returned in such case.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-28 18:37:41 -06:00
m1nlandBen Meadors 563adc6aaa enhancement(mesh): remove late packets from tx queue when full (#9779)
* enhance tx queue priority management

In busy environments, especially for ROUTER_LATE role, tx queue
fills very quickly. Delayed packets became late but new packets
to be retransmitted won't be put into the tx queue as old ones
stay there for a very long time (even a minute or more). This change
makes meshtastic prioritize new packets over late packets from tx queue
and allows to remove late packet from back of tx queue when there
is no space for a new one.

* apply copilot recommendation for cast

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-28 17:17:29 -06:00
Ben Meadors 67f806b0a5 Merge remote-tracking branch 'origin/master' into develop 2026-02-28 11:08:15 -06:00
niccellular 067d6834c7 fix audit findings from Opus 4.6 specifically looking for race conditions and logic bugs
Three-agent security audit of the LOCKDOWN build identified race conditions,
  incomplete key zeroing, and missing memory hygiene. All findings except HIGH-1
  are addressed here. HIGH-1 (revokeAllAuth not resetting per-instance
  isAdminAuthorized) was reviewed and closed as wontfix: PKC admin key holders
  have persistent access regardless, so the 7-second pre-reboot window changes
  nothing about the threat model.

  HIGH-2: lockNow() previously only zeroed dek[]. Now also zeroes kek[],
  ephemeralKek[], and their derived flags so no key material survives a lock
  event in a RAM dump.

  HIGH-3: isAdminAuthorized (per-instance) and s_localAdminAuthorized (static)
  changed from plain bool to std::atomic<bool>. Both flags are written from the
  NimBLE FreeRTOS task and read from the main loop — non-atomic reads were a
  data race.

  MED-1: readAndDecrypt() and encryptAndWrite() now snapshot dek[] into a stack-
  local dekSnapshot[] on entry before acquiring any lock. A concurrent lockNow()
  that zeros dek[] mid-operation either zeroes the snapshot (HMAC fails securely)
  or has no effect on an already-captured copy. dekSnapshot is zeroed on all exit
  paths.

  MED-2: close() and handleStartConfig() no longer clear s_localAdminAuthorized.
  Resetting it on every disconnect was an auth DoS — any new client connection
  would silently revoke the passphrase-authenticated state, forcing the operator
  to re-enter the passphrase on every reconnect. Device-level auth now persists
  until explicit Lock Now (revokeAllAuth()).

  MED-3: readAndDecrypt() scoped the spiLock LockGuard to a nested block
  covering only the FSCom.open/read/close sequence. The lock is released before
  any HMAC or AES-CTR work. spiLock is a non-recursive binary semaphore;
  holding it across CC310 calls created a re-entry deadlock risk.

  MED-4: AdminModule zeros the passphrase bytes in toRadioScratch via a volatile
  pointer cast before returning from both the Lock Now and provision/unlock
  paths. Prevents the passphrase lingering in the scratch buffer until the next
  ToRadio message overwrites it.

  MED-5: readAndDecrypt() was missing memset(fileBuf) on the hmacData OOM early-
  exit path, leaving ciphertext in the heap until the allocator reuses it.

  MED-6: readAndDecrypt() now sets outLen=0 on entry so callers never observe a
  stale length value on any early failure return.

  MED-7: readAndDecrypt() calls memset(outBuf, 0, ciphertextLen) on AES-CTR
  failure to clear partial plaintext that may have been written to the caller's
  buffer before the error was detected.

  MED-8: Passphrase length limit corrected from 64 to 32 bytes throughout
  (EncryptedStorage.cpp provisionPassphrase/unlockWithPassphrase, AdminModule.cpp
  ppLen range check, EncryptedStorage.h doc comments). The proto private_key field
  is 32 bytes; accepting 33-64 silently failed inside EncryptedStorage while
  appearing to succeed at the AdminModule gate.

  L-1: readAndDecrypt() now rejects files larger than 65536+54 bytes before
  allocating the heap buffer, preventing OOM or integer overflow on corrupt or
  attacker-supplied oversized files.

  L-2: migrateFile() has a precondition comment that spiLock must not be held
  by the caller; both isEncrypted() and encryptAndWrite() acquire it internally.
2026-02-28 11:57:47 -05:00
Ben Meadors c7dc536640 Merge remote-tracking branch 'origin/develop' 2026-02-28 09:39:25 -06:00
Ben Meadors 6920a5f8b1 Increase PSRAM malloc threshold from 256 bytes to 2048 bytes (#9758) 2026-02-28 09:34:26 -06:00
github-actions[bot]andvidplace7 2b7a230977 Upgrade trunk (#9752)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-28 08:41:34 -06:00
renovate[bot] 1e16185fdd chore(deps): update platformio/ststm32 to v19.5.0 (#9764)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 08:41:21 -06:00
renovate[bot] 60c2592694 chore(deps): update adafruit dps310 to v1.1.6 (#9763)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 08:41:01 -06:00
renovate[bot] fc0ad4956e chore(deps): update adafruit ahtx0 to v2.0.6 (#9766)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 08:40:15 -06:00
Ben Meadors 43aa90fc3f Unlock 0x8B5 register macro guard for SX162 (#9777) 2026-02-28 08:38:08 -06:00
Ben Meadors c28bdbd7e6 Fix mqtt test 2026-02-28 08:32:05 -06:00
Jonathan BennettandBen Meadors 857c7b3a3a Don't launch canned message when waking screen or silencing notification (#9762)
* Don't launch canned message when waking screen or silencing notification

* Add screen ifdefs

* Get the #if right

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-27 17:07:03 -06:00
niccellularandClaude Sonnet 4.6 3f9d76f054 addressing copilot comments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 16:35:18 -05:00
niccellular 545ae99ff8 Merge branch 'develop' of https://github.com/meshtastic/firmware into develop 2026-02-27 08:00:19 -05:00
Andrew Yong 83cac93ca8 fix(MQTT): Send first MapReport as soon as possible (#8872)
* fix(MQTT): First MapReport does not get sent

Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs) is
used to maintain the map reporting interval, but because last_report_to_map has
an initial value of 0, the map report routine does not start until the system
millis() time has passed map_publish_interval_msecs.

Fix this by adding a check that last_report_to_map is not 0.

Signed-off-by: Andrew Yong <me@ndoo.sg>

* feat(MQTT): Send MapReporting immediately upon location fix

Do not update last_report_to_map when Map Report is attempted without a valid
location, as this results in waiting up to an hour (or configured Map Report
interval).

That usually happens because most nodes do not keep GPS warm, so GPS usually
locks after the first attempt at Map Report.

This change also results in the log WARNing message getting spammed until a
location is obtained, so remove the message for now.

Signed-off-by: Andrew Yong <me@ndoo.sg>

* feat(MQTT): Throttled warning when position is not available for MapReport

Signed-off-by: Andrew Yong <me@ndoo.sg>

---------

Signed-off-by: Andrew Yong <me@ndoo.sg>
2026-02-27 05:16:55 -06:00
niccellular fd7fc40255 Add rak4631_lockdown hardened build variant
Introduces a new RAK4631 build variant (-DMESHTASTIC_LOCKDOWN=1) with
encrypted flash storage, BLE/USB config access control, and hardened
defaults.

New files:
- variants/nrf52840/rak4631_lockdown/ — PlatformIO env, variant header
- src/security/EncryptedStorage.h/.cpp — AES-128-CTR + HMAC-SHA256
  encrypted proto storage using CC310 hardware, passphrase-gated DEK,
  TTL/boot-count unlock token, backoff on failed attempts
- src/security/APProtect.h/.cpp — UICR APPROTECT (SWD lockout)

Modified files:
- configuration.h — MESHTASTIC_LOCKDOWN flag derives
  PHONEAPI_ACCESS_CONTROL, ENCRYPTED_STORAGE, ENABLE_APPROTECT,
  HARDENED_DEFAULTS (nRF52 only)
- NodeDB.cpp/.h — encrypted loadProto/saveProto, locked-boot early
  return, migration of plaintext files to MENC, hardened defaults,
  saveToDiskNoRetry guard prevents FSCom.format() when storage locked
- PhoneAPI.cpp/.h — per-connection auth reset, redacts channel PSKs
  and security keys for unauthorized clients, revokeAllAuth() for
  Lock Now, post-config TAK_LOCKED/TAK_NEEDS_PROVISION notification
- AdminModule.cpp — passphrase delivery via set_config(security),
  provision/unlock/re-verify flow, Lock Now sentinel (0xFF),
  PKC admin auth callback to PhoneAPI
- PowerFSM.cpp — guard serialEnter() BLE disable on nRF52
2026-02-26 22:15:40 -05:00
renovate[bot] d34fa3e7df chore(deps): update platformio/nordicnrf52 to v10.11.0 (#9760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 08:11:00 -06:00
renovate[bot] 75cb8bb5ea chore(deps): update platformio/espressif32 to v6.13.0 (#9759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 08:09:44 -06:00
renovate[bot] 2a99cc6a25 chore(deps): update adafruit mlx90614 to v2.1.6 (#9756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 08:01:28 -06:00
renovate[bot] b5f98d1270 chore(deps): update adafruit_tsl2561 to v1.1.3 (#9757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 08:01:03 -06:00
Ben Meadors ef697319da Merge remote-tracking branch 'origin/develop' 2026-02-26 07:53:34 -06:00
Ben Meadors efd68f9fad Fix Bluetooth on RAK Ethernet Gateway by removing MESHTASTIC_EXCLUDE_POWER_FSM from build_flags (#9755) 2026-02-26 07:38:13 -06:00
Ben MeadorsandCopilot 3a74e049ab Add Transmit history persistence for respecting traffic intervals between reboots (#9748)
* Add transmit history for throttling that persists between reboots

* Fix RAK long press detection to prevent phantom shutdowns from floating pins

* Update test/test_transmit_history/test_main.cpp

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

* Test fixes and placeholder for content handler tests

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 20:41:07 -06:00
KokoandBen Meadors 3b1b308357 platform: nrf52: Fix typo in BLEDfuSecure filename (#9709)
Change file name from BLEDfuScure.cpp to BLEDfuSecure.cpp. Fix filenames
in documentation.

Signed-off-by: Koko <github@kokosoftware.pl>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-25 18:00:02 -06:00
Jonathan BennettandBen Meadors 5a068431ed Add GPIO_DETECT_PA portduino config, and support 13302 detection with it (#9741)
* Add GPIO_DETECT_PA portduino config, and support 13302 detection with it.

* Tweak PA detect gpio to use pinMapping

* minor yaml output fixes

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-25 14:18:07 -06:00
github-actions[bot]andvidplace7 25f086a64f Upgrade trunk (#9744)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-25 08:30:32 -06:00
Ben Meadors a93d4d7e9d Revert "Fix/rak3401 button (#9668)" (#9747)
This reverts commit a5523b04ef.
2026-02-25 08:26:49 -06:00
Thomas Göttgens 54781cf51a hopefully fix remaining cppcheck issues (#9745) 2026-02-25 10:27:21 +01:00
Wessel ad7d19c317 Remove unused global rIf that shadows locals and fails cppcheck (#9743)
I noticed because my PR failed
2026-02-24 20:57:58 -06:00
Thomas Göttgens 58496e56d2 fail cppcheck on low already. Code quality for the win. 2026-02-24 22:45:38 +01:00
Ben Meadors 2c18a41613 Merge remote-tracking branch 'origin/master' into develop 2026-02-24 14:28:15 -06:00
github-actions[bot]andthebentern c65eecc295 Update protobufs (#9739)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-02-24 14:27:23 -06:00
Ben Meadors cac45d9ced Align telemetry broadcast want_response behavior with traceroute (#9717)
* Align telemetry broadcast want_response behavior with traceroute

* Fixes

* Reduce side-effects by making the telemetry modules handle the ignorerequest

* Remove unnecessary ignoreRequest flag

* Try inheriting from MeshModule

* Add exclusion for sensor/router roles and add base telem module
2026-02-24 13:26:47 -06:00
Wessel 02d42f87d3 Implement 'agc' reset for SX126x & LR11x0 chip families (#9705)
* Implement 'agc' reset for SX126x chip family

There's no actual agc on SX126x chips but you can reset the analog
registers by doing a warm sleep & running calibration.

* Address PR comments & implement for LR11x0 too

* calibrate for configured frequency band

* Gate LR11X0_AGC_RESET
2026-02-24 08:03:25 -06:00
Ben Meadors 9c439f8d0a FIx loophole with telemetry coercion 2026-02-24 06:01:52 -06:00
github-actions[bot]andvidplace7 cf998f03b6 Upgrade trunk (#9731)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-24 05:47:18 -06:00
renovate[bot] f9d1f2414a chore(deps): update sensorlib to v0.3.4 (#9727)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 05:46:27 -06:00
renovate[bot] 13b25c0614 chore(deps): update meshtastic-st7789 digest to 9ee76d6 (#9729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 20:42:52 -06:00
Austin 607b631114 meshtasticd: Add Luckfox Lyra Hat pinmaps (#9730)
Luckfox Lyra Plus + Waveshare Pi Pico Hat
Luckfox Lyra Ultra + wehooper4's "Luckfox Ultra" hat.
2026-02-23 20:42:41 -06:00
ManuelandCopilot 99e88bb499 Support mini ePaper S3 Kit (#9335)
* initial draft

* update DC pin

* add ADC channel

* fix e-ink display

* update GxEPD2 reference

* Apply suggestion from @Copilot

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

* Apply suggestion from @Copilot

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

* Apply suggestion from @Copilot

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-23 12:08:24 -06:00
github-actions[bot]andvidplace7 4f84cf011d Upgrade trunk (#9724)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-23 05:38:33 -06:00
oscgonfer 417ca86c30 Split module includes for AQ module (#9711) 2026-02-22 21:07:32 -06:00
renovate[bot] 7df37f8254 chore(deps): update radiolib to v7.6.0 (#9695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-22 18:53:33 +01:00
renovate[bot] 0726a9f774 chore(deps): update radiolib to v7.6.0 (#9695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-22 18:46:39 +01:00
Ben Meadors f8cc1d6046 Merge remote-tracking branch 'origin/develop' 2026-02-22 06:10:05 -06:00
Wessel 9383d0bcfc Apply SX1262 register 0x8B5 patch for improved GC1109 RX sensitivity (#9571)
* Apply SX1262 register 0x8B5 patch for improved GC1109 RX sensitivity

Sets the LSB of undocumented SX1262 register 0x8B5 on Heltec V4 and
Wireless Tracker V2 boards with the GC1109 FEM. This patch was
recommended by Heltec/Semtech and tested in MeshCore PR #1398, where
it significantly reduced packet loss on the Heltec V4.

* Use higher level function

* Add .venv/ to .gitignore
2026-02-21 09:08:14 -06:00
Ben MeadorsandCopilot f615990c00 Limit http connections and add free heap check before allocating for SSL (#9693)
* Reduce maximum concurrent HTTPS connections to save memory

* Add heap check and limit connections to prevent HTTPS connection related crashes

* Use Throttle::isWithinTimespanMs for overflow-safe heap warning throttle

* Update src/mesh/http/WebServer.cpp

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

* Fix double-free heap corruption and parser leak in web server handlers

- Remove double-free in handleFsBrowseStatic, handleNodes, handleScanNetworks:
  JSONValue(const JSONArray&) shallow-copies pointers, so delete value already
  recursively frees all elements. The explicit cleanup loops were deleting the
  same pointers a second time, corrupting the ESP32 heap allocator metadata.
  This is the likely root cause of #8827 (SSL setup failures after uptime).
- Fix memory leak in handleFormUpload: two early-return paths were missing
  delete parser.
- Remove unused global HTTPClient httpClient and its includes.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-21 09:06:17 -06:00
Mike RobbinsandBen Meadors 8feb34e7a8 RadioLib edge-triggered interrupts robustness (#9658)
* Fix potential race condition (read, ISR write, clear) in NotifiedWorkerThread

* Check for missed edge-triggered interrupts race condition between startReceive and enableInterrupt

* Occasionally poll to catch missed RX_DONE interrupts. (RadioLibInterface::pollMissedIrqs)

* Simplify RadioLibInterface::checkRxDoneIrqFlag()

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-21 06:14:04 -06:00
David Remba a5523b04ef Fix/rak3401 button (#9668) 2026-02-21 09:15:22 +01:00
Wessel af518fbd2b Hold GC1109 FEM power during deep sleep for LNA RX wake (#9572)
* Hold GC1109 PA_POWER and PA_EN during deep sleep for LNA RX wake

Use rtc_gpio_hold_en to latch PA_POWER (LDO) and PA_EN (CSD) HIGH
during deep sleep so the GC1109 LNA remains powered for wake-on-packet
RX. Previously these pins used weak pull-ups which could lose state.

On deep sleep wake, skip these pins in the blanket RTC hold release and
instead release them in SX126xInterface::init() after GPIO registers are
set HIGH first, avoiding a power glitch on the GC1109.

Trade-off: ~6.5mA additional deep sleep current for significantly
improved wake-on-packet RX sensitivity (~17dB).

Reference: https://github.com/meshcore-dev/MeshCore/pull/1600

* Add LDO startup delay before GC1109 chip enable

TLV75733P LDO has ~550us startup time (datasheet tSTR). On cold boot,
wait 1ms for VBAT to stabilise before driving CSD/CPS, per GC1109
power-on sequence requirement. On deep sleep wake the LDO is held on
via RTC latch so no delay is needed.
2026-02-20 16:12:54 -06:00
Ben Meadors 8c37d6923b Add #include "RadioLibInterface.h" 2026-02-20 12:00:30 -06:00
Ben Meadors 299ef95f25 Revert "Add agc reset attempt (#8163)" (#9702)
This reverts commit ac611c4b62.
2026-02-20 11:25:28 -06:00
HarukiToredaandBen Meadors 8f81b194d3 BLE Pairing fix (#9701)
* BLE Pairing fix

* gating for consistency

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-20 10:38:07 -06:00
github-actions[bot]andvidplace7 6bc3e31537 Upgrade trunk (#9696)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-20 06:45:46 -06:00
840b29860f skip header items when enabling the InkHUD menu cursor (#9552)
* onNavUp() sets the menu cursor to the last item if not shown

* skip headers when showing the menu cursor in onNavUp() and onNavDown()

* skip headers when showing the menu cursor in onButtonShortPress()

* brace cursor incrementing

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-02-20 06:12:22 -06:00
renovate[bot] 15460c8f97 Update meshtastic-GxEPD2 digest to c7eb4c3 (#9694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-19 17:11:04 -06:00
Ben Meadors a77ac21e0d Trunk 2026-02-19 07:17:50 -06:00
Ben Meadors 2b6d8f6824 Merge branch 'develop' 2026-02-19 07:17:20 -06:00
+19 4c91beeda9 Develop to master (#9618)
* Don't ever define PIN_LED or BLE_LED_INVERTED (#9494)

* Don't ever define PIN_LED

* Deprecate BLE_LED_INVERTED

* Add StatusMessage module and config overrides (#9351)

* Add StatusMessage module and config overrides

* Trunk

* Don't reboot node simply for a StatusMessage config update

* Missed in reviews - fixing send bubble (#9505)

* Prefer EXT_PWR_DETECT pin over chargingVolt to detect power unplugged (#9511)

* Make sure we always return a value in NodeDB::restorePreferences() (#9516)

In case FScom is not defined there is no return statement. This
moves the return outside of the ifdef to make sure a defined
value is returned.

* Inkhud battery icon improvements. (#9513)

* Inkhud battery icon improvements.
Fixes the battery icon draining from the flat side towards the bump, which is backwards from general design language seen on most devices
By request of kr0n05_ on discord, adds the ability to mirror the battery icon which fixes that issue in another way, and is also a common design seen on other devices.

* Remove option for icon mirroring

* Add border + dither to battery to prevent font overlap

* Fix trunk format

* Code cleanup, courtesy of Xaositek.

* Add reply bot module with DM-only responses and rate limiting (#9456)

* Implement Meshtastic reply bot module with ping and status features

Adds a reply bot module that listens for /ping, /hello, and /test commands received via direct messages or broadcasts on the primary channel. The module always replies via direct message to the sender only, reporting hop count, RSSI, and SNR. Per-sender cooldowns are enforced to reduce network spam, and the module can be excluded at build time via a compile flag. Updates include the new module source files and required build configuration changes.

* Update ReplyBotModule.cpp

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

* Update src/modules/ReplyBotModule.h

Match the existing MESHTASTIC_EXCLUDE_* guard pattern so the module is excluded by default.

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

* Update src/modules/ReplyBotModule.cpp

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

* Tidying up

---------

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

* HotFix for ReplyBot - Modules.cpp included and moved configuration.h (#9532)

* Undefine LED_BUILTIN (#9531)

Keep variant in sync with
https://github.com/meshtastic/firmware/commit/df40085

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* Add agc reset attempt (#8163)

* Add agc reset attempt

* Add radioLibInterface include

* Trunk

* AGC reset don't crash, don't naively call

* Update src/main.cpp

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

* Use Throttle function

---------

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

* Remove unused hmx variable (#9529)

The variable is not used at all in the function, remove it to
silence the compiler warning.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* Rename LED_PIN to LED_POWER, move handling out of main to dedicated module (#9512)

* Rename LED_PIN to LED_POWER, move handling out of main to dedicated module

* Misc

* Remove errant endif

* Fix hop_limit upgrade detection (#9550)

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* meshtasticd: Fix install on Fedora 43 (#9556)

* RPM: Include meshtasticd-start.sh (#9561)

* Add Slash Key to VirtualKeyboard (#9563)

Addition of ? and / to the virtual Keyboard via short and long press

* Add support for CW2015 LiPo battery fuel gauge (#9564)

* Add support for CW2015 LiPo battery fuel gauge

* Address Copilot's concerns, minor fixups

* Make LED_POWER blip even in critical battery (#9545)

* Enable FORTIFY and SP for native builds (#9537)

* Enable FORITFY and NX for native builds

meshtasticd does have an executable stack and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and a non-executable stack.

This gives the following improvements on Debian Trixie:

$ checksec --file=./.pio/build/native/meshtasticd
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   13516 Symbols	  No	0		17		./.pio/build/native/meshtasticd

$ checksec --file=./.pio/build/native/meshtasticd
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   13519 Symbols	  Yes	12		20		./.pio/build/native/meshtasticd

Tested with --sim mode I do not get any crashes or similar.

* Enable FORTIFY and NX for native builds

meshtasticd does have an executable stack and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and a non-executable stack.

This gives the following improvements on Debian Trixie:

$ checksec --file=./.pio/build/native/meshtasticd
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   13516 Symbols	  No	0		17		./.pio/build/native/meshtasticd

$ checksec --file=./.pio/build/native/meshtasticd
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   13519 Symbols	  Yes	12		20		./.pio/build/native/meshtasticd

Tested with --sim mode I do not get any crashes or similar.

* Enable FORTIFY and SP for native builds

meshtasticd does have a stack canaries and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and stack canaries.

This gives the following improvements on Debian Trixie:

$ checksec --file=./.pio/build/native/meshtasticd
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   13516 Symbols	  No	0		17		./.pio/build/native/meshtasticd

$ checksec --file=./.pio/build/native/meshtasticd
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   13519 Symbols	  Yes	12		20		./.pio/build/native/meshtasticd

Tested with --sim mode I do not get any crashes or similar.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* Update built-in documentation for current method of implementation (#9592)

* Refactor logging in ProtobufModule to ensure message details are logged after successful decoding (#9536)

* Automated version bumps (#9604)

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

* Add missing openocd_target to custom nrf52 boards (#9603)

This stops platformio complaining about `Missing target configuration for me25ls01-4y10td` etc when trying to flash with nrfutil.

* Add sdl libs for native builds (#9595)

* Add sdl libs for native builds

* Alpine try again

* fix some random compiler warnings (#9596)

* Modify the dependency library of v4-tft (#9507)

* BaseUI: Favorite Screen Signal Quality improvement (#9566)

* Favorite Signal Quality improvement

* Show Voltage if node shares it.

* Trunk Fix

* Change Favorite tittle to encase name with Asterisks

* Add Pluggin In condition for Battery Line

* Adjust getUptimeStr Prefixes

* Create isAPIConnected for SharedCommon usage

* Correct leftSideSpacing to account for isAPIConnected

---------

Co-authored-by: Jason P <applewiz@mac.com>

* ExternalNotification and StatusLED now call AmbientLighting to update… (#9554)

* ExternalNotification and StatusLED now call AmbientLighting to update RGB LEDs. Add optional heartbeat

* Don't overwrite RGB state if heartbeat is disabled.

* Use the right define

* Remove another .h and make rgb static

* move rgb objects into AmbientLighting class

* Straighten out AmbientLighting Thread object

* Use %f for floats

* Fixes on SCD4X admin comands (#9607)

* Fixes on SCD4X admin comands

* Minor fix in logs for SEN5X

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* feat/add sfa30 (#9372)

* Move PMSA003I to separate class and update AQ telemetry

* AirQualityTelemetry module not depend on PM sensor presence

* Remove commented line

* Fixes on PMS class

* Add missing warmup period to wakeUp function

* Fixes on compilation for different variants

* Add functions to check for I2C bus speed and set it

* Initial implementation for SFA30Sensor

* Move PMSA003I to separate class and update AQ telemetry

* AirQualityTelemetry module not depend on PM sensor presence

* Remove commented line

* Fixes on PMS class

* Add missing warmup period to wakeUp function

* Fixes on compilation for different variants

* Add functions to check for I2C bus speed and set it

* Add ScreenFonts.h

Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>

* PMSA003I 1st round test

* Fix I2C scan speed

* Fix minor issues and bring back I2C SPEED def

* Remove PMSA003I library as its no longer needed

* Add functional SCD4X

* Fix screen frame for CO2

* Add admin commands to SCD4X class

* Add further admin commands and fixes.

* Remove unused I2C speed functions and cleanup

* Cleanup of SEN5X specific code added from switching branches
* Remove SCAN_I2C_CLOCK_SPEED block as its not needed
* Remove associated functions for setting I2C speed

* Unify build epoch to add flag in platformio-custom.py (#7917)

* Unify build_epoch replacement logic in platformio-custom

* Missed one

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

* Put guards in place around debug heap operations (#7955)

* Put guards in place around debug heap operations

* Add macros to clean up code

* Add pointer as well

* Cleanup

* Fix memory leak in NextHopRouter: always free packet copy when removing from pending

* Formatting

* Only queue 2 client notification

* Merge pull request #7965 from compumike/compumike/fix-nrf52-bluetooth-memory-leak

Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap

* Merge pull request #7964 from compumike/compumike/fix-nimble-bluetooth-memory-leak

Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap

* Update protobufs (#7973)

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

* T-Lora Pager: Support LR1121 and SX1280 models (#7956)

* T-Lora Pager: Support LR1121 and SX1280 models

* Remove ifdefs

* Trunk

* Trunk

* Static memory pool allocation (#7966)

* Static memory pool

* Initializer

* T-Lora Pager: Support LR1121 and SX1280 models (#7956)

* T-Lora Pager: Support LR1121 and SX1280 models

* Remove ifdefs

---------

Co-authored-by: WillyJL <me@willyjl.dev>

* Portduino dynamic alloc

* Missed

* Drop the limit

* Update meshtastic-esp8266-oled-ssd1306 digest to 0cbc26b (#7977)

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

* Fix json report crashes on esp32 (#7978)

* Tweak maximums

* Fix DRAM overflow on old esp32 targets

* 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

* Scale probe buffer size based on current baud rate (#7975)

* Scale probe buffer size based on current baud rate

* Throttle bad time validation logging and fix time comparison logic

* Remove comment

* Missed the other instances

* Copy pasta

* Fix GPS gm_mktime memory leak (#7981)

* Fix overflow of time value (#7984)

* Fix overflow of time value

* Revert "Fix overflow of time value"

This reverts commit 084796920179e80a7500d36c25fd4d82b3ef4214.

* That got boogered up

* Remove PMSA003 include from modules

* Add flag to exclude air quality module

* Rework PMSA003I to align with new I2C scanner

* Reworks AQ telemetry to match new dynamic allocation method
* Adds VBLE_I2C_CLOCK_SPEED build flag for sensors with different I2C speed requirements
* Reworks PMSA003I

* Move add sensor template to separate file

* Split telemetry on screen options

* Add variable I2C clock compile flag

* Added to Seeed Xiao S3 as demo

* Fix drawFrame in AQ module

* Module settings override to i2cScan module function

* Move to CAN_RECLOCK_I2C per architecture

* Add reclock function in TelemetrySensor.cpp
* Add flag in ESP32 common

* Minor fix

* Move I2C reclock function to src/detect

* Fix uninitMemberVar errors and compile issue

* Make sleep, wakeUp functions generic

* Fix STM32 builds

* Add exclude AQ sensor to builds that have environmental sensor excludes
* Add includes to AddI2CSensorTemplate.h

* SEN5X first pass

* WIP Sen5X functions

* Further (non-working) progress in SEN5X

* WIP Sen5X functions

* Changes on SEN5X library - removing pm_env as well

* Small cleanup of SEN5X sensors

* Minor change for SEN5X detection

* Remove dup code

* Enable PM sensor before sending telemetry.

This enables the PM sensor for a predefined period to allow for warmup.

Once telemetry is sent, the sensor shuts down again.

* Small cleanups in SEN5X sensor

* Add dynamic measurement interval for SEN5X

* Only disable SEN5X if enough time after reading.

* Idle for SEN5X on communication error

* Cleanup of logs and remove unnecessary delays

* Small TODO

* Settle on uint16_t for SEN5X PM data

* Make AQTelemetry sensors non-exclusive

* Implementation of cleaning in FS prefs and cleanup

* Remove unnecessary LOGS
* Add cleaning date storage in FS
* Report non-cumulative PN

* Bring back detection code for SEN5X after branch rebase

* Add placeholder for admin message

* Add VOC measurements and persistence (WIP)

* Adds VOC measurements and state
* Still not working on VOC Index persistence
* Should it stay in continuous mode?

* Add one-shot mode config flag to SEN5X

* Add nan checks on sensor data from SEN5X

* Working implementation on VOCState

* Adds initial timer for SEN55 to not sleep if VOCstate is not stable (1h)
* Adds conditions for stability and sensor state

* Fixes on VOC state and mode swtiching

* Adds a new RHT/Gas only mode, with 3600s stabilization time
* Fixes the VOCState buffer mismatch
* Fixes SEN50/54/55 model mistake

* Adapt SEN5X to new sensor list structure. Improve reclock.

* Improve reClockI2C conditions for different variants
* Add sleep, wakeUp, pendingForReady, hasSleep functions to PM sensors to save battery
* Add SEN5X

* Fix merge errors

* Small reordering in PMS class for consistency

* If one sensor fails, AQ telemetry still reports data

* Small formatting fix

* Add SEN5X to AQI in ScanI2C

* SCD4X now part of AQ module with template list

* Fixes difference between idle and sleep
* In LowPower, sleep is disabled
* Requires testing for I2C clock comms for commands

* Remove unnecessary import

* Add co2 to serialized AQ metrics

* Add SFA30 with new sensor template in AQ module

* Update library dependencies in platformio.ini

* Fix unitialized variables in SEN5X constructor

* Fix missing import

* Fix uninitMemberVars

* Fix import error for SCD4X

* Fix I2CClock logic

* Fix not reclocking back to 700000Hz

* Fix multiple sensors being read simultaneously

*  The logic in AQ module is different to the one in EnvironmentTelemetryModule.  In Env module, if any sensor fails to getMetrics, no valid flag for the module. This prevents other sensors to report data.

* Fix pending clock change in PMSA003

* Cleanup of SEN5X class

* Exclude AQ sensor from wio-e5 due to flash limitations

* Fix I2C clock change logic

* Make sure clock is always set to needed value

* Fix returns

* Fix trunk

* Fix on condition in reclock

* Fix trunk

* Final SFA30 class implementation

* Add HCHO to screen and improve logs

* Add metrics to mesh packet serializer

* Minor fixes in logs

* OCD tidy up of logs

* Fix sleep function

* Remove old I2C_CLOCK_SPEED code

---------

Co-authored-by: nikl <nikl174@mailbox.org>
Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>
Co-authored-by: Nashui-Yan <yannashui10@gmail.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
Co-authored-by: WillyJL <me@willyjl.dev>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Log rxBad PacketHeaders with more info (id, relay_node) like printPacket, so we can try to match RX errors to other packets in the logs. (#9614)

* Exclude status message module

* fix: zero entire public key array instead of only first byte (#9619)

* Update protobufs (#9621)

* Xiao NRF - define suitable i2c pins for the sub-variants (#8866)

Co-authored-by: Christian Walther <cwalther@gmx.ch>

* Update src/detect/ScanI2C.cpp

Co-authored-by: Wessel <wessel@weebl.me>

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

Co-authored-by: Wessel <wessel@weebl.me>

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

Co-authored-by: Wessel <wessel@weebl.me>

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

Co-authored-by: Wessel <wessel@weebl.me>

* Update src/mesh/NodeDB.cpp

Co-authored-by: Wessel <wessel@weebl.me>

* convert GPS global and some new in gps.cpp to unique_ptr (#9628)

Trying this to see if anything bad happen if I were to replace most raw pointers with unique_ptr.

I didn't used std::make_unique since it is only supported in C++14 and onwards but until we update esp32 to arduino 3.x the ESP32 xtensa chips use a C++11 std.

* replace delete in RedirectablePrint.cpp with std::unique_ptr (#9642)

Is part of the unique_ptr modernization effort.

* replace delete in EInkDynamicDisplay.{cpp,h} with std::unique_ptr (#9643)

Is part of the unique_ptr modernization effort.

* Undefine LED_BUILTIN for Heltec v2 variant (#9647)

* Undefine LED_BUILTIN for Heltec v2 variant

* Undefine LED_BUILTIN for Heltec v2.1 variant

---------

Co-authored-by: Jorropo <jorropo.pgm@gmail.com>

* replace delete in RadioInterface.cpp with std::unique_ptr (#9645)

Is part of the unique_ptr modernization effort.

* fix typo in PIN_GPS_SWITCH (#9648)

Wasn't caught by CI.

* replace delete in CryptoEngine.{cpp,h} with std::unique_ptr (#9649)

Is part of the unique_ptr modernization effort.

* workaround NCP5623 and LP5562 I2C builds (#9652)

Theses two appear to be buggy on r1-neo and nomadstar meteor pro, they rely on Wire.h being included previously to their import.

Idk why other platforms using the same smart LEDs are working while theses ones don't.

This should make CI green on the dev branch.

* replace delete in AudioThread.h with std::unique_ptr (#9651)

Is part of the unique_ptr modernization effort.

* Add USB_MODE=1 for Station G2 (#9660)

* InkHUD: Favorite Map Applet (#9654)

* fix a lot of low level cppcheck warnings (#9623)

* simplify the observer pattern, since all the called functions are const getters.
* use arduino macro over std: for numerical values and refactor local variables in drawScrollbar()
* oh, so Cppcheck actually complained about const pointers not being const.
* slowly getting out of ifdef hell
* fix inkHUD warnings as well
* last 2 check warnings
* git checks should fail on low defects from now on

* Feat/add scd30 (#9609)

* Merge develop into SCD30

* Add SCD30 class

* Fix logging and admin commands

* Minor cleanup and logging improvements

* Minor formatting issue

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

* Improvements on setTemperature

* Fix casting float-uint16_t
* Pass 100 for resetting temperature offset

* Fix issues pointed out by copilot

* Add quick reboot to set interval quicker on scd30

* Change saveState to only happen after boot and minor log changes

* Fix missing semicolon in one shot mode log

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* fix: respect DontMqttMeBro flag regardless of channel PSK (#9626)

The previous PSK check was broken from its introduction in #4643 —
memcmp was used in boolean context without comparing to 0, inverting
the condition. Since no one noticed for over a year, the PSK-based
filtering provided no practical value. Simplifying to always respect
the sender's preference is both more correct and easier to reason about.

* our firmware action is too clever

Update pio_target and add pio_opts for checks.

* fix detection of SCD30 by checking if the size of the return from a 2 byte register read is correct (#9664)

* fix detection of SCD30 by checking if thee size of the return from a 2 byte register read is correct
fix signedness warning in PMSA003 sensor code.

* Add alternate path for LPS22HB

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

* check EndTransmission for errors and compare returned length to expected value

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

---------

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

* #9623 resolved a local shadow of next_key by converting it to int. (#9665)

* zip a few gitrefs down (#9672)

* InkHUD: Allow non-system applets to subscribe to input events (#9514)

* Allow inkhud user applets to consume inputs with opt-in system
Adds a way for applets to subscribe to input events while keeping it off
by default to preserve compatibility and expected behaviours. Adds
example for use as well.
* Add check for nullptr on getActiveApplet uses
* Remove redundant includes
* Move subscribedInputs to protected
* More consistent naming scheme

* Fake IAQ values on Non-BSEC2 platforms like Platformio and the original ESP32 (#9663)

* BSEC2 Replacement
- add approximation for IAQ to non-BSEC2 platforms.
- Re-add this sensor to ESP32 targets, and refactor env_extra includes.
- Fix C++ 11 compatibility
* Check for gas resistance 0

* ULED_BUILTIN for 9m2ibr_aprs_lora_tracker (#9685)

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: Eric Sesterhenn <eric.sesterhenn@x41-dsec.de>
Co-authored-by: Vortetty <33466216+Vortetty@users.noreply.github.com>
Co-authored-by: Mattatat25 <108779801+mattatat25@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Max <rekin.m@gmail.com>
Co-authored-by: Colby Dillion <colby.dillion@pacshealth.com>
Co-authored-by: Austin <vidplace7@gmail.com>
Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Chloe Bethel <chloe@9net.org>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Quency-D <55523105+Quency-D@users.noreply.github.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
Co-authored-by: oscgonfer <oscgonfer@users.noreply.github.com>
Co-authored-by: nikl <nikl174@mailbox.org>
Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>
Co-authored-by: Nashui-Yan <yannashui10@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu>
Co-authored-by: WillyJL <me@willyjl.dev>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Wessel <wessel@weebl.me>
Co-authored-by: Christian Walther <cwalther@gmx.ch>
Co-authored-by: Jorropo <jorropo.pgm@gmail.com>
Co-authored-by: Eric Barch <ericb@ericbarch.com>
Co-authored-by: Clive Blackledge <clive@ansible.org>
2026-02-19 07:16:33 -06:00
github-actions[bot]andvidplace7 d0cf79a991 Upgrade trunk (#9692)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-19 06:03:53 -06:00
Max 86986d6337 ULED_BUILTIN for 9m2ibr_aprs_lora_tracker (#9685) 2026-02-19 10:20:07 +08:00
github-actions[bot]andvidplace7 5408e81de7 Upgrade trunk (#9683)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-18 06:44:55 -06:00
Thomas Göttgens 178c3114d9 Fake IAQ values on Non-BSEC2 platforms like Platformio and the original ESP32 (#9663)
* BSEC2 Replacement
- add approximation for IAQ to non-BSEC2 platforms.
- Re-add this sensor to ESP32 targets, and refactor env_extra includes.
- Fix C++ 11 compatibility
* Check for gas resistance 0
2026-02-17 22:16:56 +01:00
renovate[bot] 357a33f454 chore(deps): update actions/stale action to v10.2.0 (#9669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 05:45:54 -06:00
harry-iii-lord 4fccda26c6 Concurrency: Modern Periodic wrapper class. (#9501) 2026-02-17 05:44:26 -06:00
github-actions[bot]andvidplace7 f94c01b452 Upgrade trunk (#9671)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-17 05:38:13 -06:00
Vortetty 3ca68e7788 InkHUD: Allow non-system applets to subscribe to input events (#9514)
* Allow inkhud user applets to consume inputs with opt-in system
Adds a way for applets to subscribe to input events while keeping it off
by default to preserve compatibility and expected behaviours. Adds
example for use as well.
* Add check for nullptr on getActiveApplet uses
* Remove redundant includes
* Move subscribedInputs to protected
* More consistent naming scheme
2026-02-17 11:03:26 +01:00
Thomas Göttgens a4ba312579 zip a few gitrefs down (#9672) 2026-02-17 09:53:20 +01:00
Thomas Göttgens a227fd7028 #9623 resolved a local shadow of next_key by converting it to int. (#9665) 2026-02-16 19:39:43 +01:00
Thomas GöttgensandCopilot 6b44b5786e fix detection of SCD30 by checking if the size of the return from a 2 byte register read is correct (#9664)
* fix detection of SCD30 by checking if thee size of the return from a 2 byte register read is correct
fix signedness warning in PMSA003 sensor code.

* Add alternate path for LPS22HB

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

* check EndTransmission for errors and compare returned length to expected value

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-16 10:07:07 -06:00
Thomas Göttgens 5feba46b53 our firmware action is too clever
Update pio_target and add pio_opts for checks.
2026-02-16 16:39:31 +01:00
Wessel 0cbf53b7a7 fix: respect DontMqttMeBro flag regardless of channel PSK (#9626)
The previous PSK check was broken from its introduction in #4643 —
memcmp was used in boolean context without comparing to 0, inverting
the condition. Since no one noticed for over a year, the PSK-based
filtering provided no practical value. Simplifying to always respect
the sender's preference is both more correct and easier to reason about.
2026-02-16 13:49:58 +01:00
github-actions[bot]andvidplace7 321ccbf5e2 Upgrade trunk (#9661)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-16 05:52:46 -06:00
57268bf4ea Feat/add scd30 (#9609)
* Merge develop into SCD30

* Add SCD30 class

* Fix logging and admin commands

* Minor cleanup and logging improvements

* Minor formatting issue

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

* Improvements on setTemperature

* Fix casting float-uint16_t
* Pass 100 for resetting temperature offset

* Fix issues pointed out by copilot

* Add quick reboot to set interval quicker on scd30

* Change saveState to only happen after boot and minor log changes

* Fix missing semicolon in one shot mode log

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-16 05:38:36 -06:00
Thomas Göttgens 56fd9c7813 fix a lot of low level cppcheck warnings (#9623)
* simplify the observer pattern, since all the called functions are const getters.
* use arduino macro over std: for numerical values and refactor local variables in drawScrollbar()
* oh, so Cppcheck actually complained about const pointers not being const.
* slowly getting out of ifdef hell
* fix inkHUD warnings as well
* last 2 check warnings
* git checks should fail on low defects from now on
2026-02-16 12:28:07 +01:00
HarukiToreda 32db70037d InkHUD: Favorite Map Applet (#9654) 2026-02-16 10:35:27 +01:00
Clive Blackledge 778823e623 Add USB_MODE=1 for Station G2 (#9660) 2026-02-16 10:16:05 +01:00
Jorropo 545826d319 replace delete in AudioThread.h with std::unique_ptr (#9651)
Is part of the unique_ptr modernization effort.
2026-02-15 06:49:16 -06:00
Jorropo dbef1de286 workaround NCP5623 and LP5562 I2C builds (#9652)
Theses two appear to be buggy on r1-neo and nomadstar meteor pro, they rely on Wire.h being included previously to their import.

Idk why other platforms using the same smart LEDs are working while theses ones don't.

This should make CI green on the dev branch.
2026-02-15 00:32:19 +01:00
Jorropo b0bd3df226 replace delete in CryptoEngine.{cpp,h} with std::unique_ptr (#9649)
Is part of the unique_ptr modernization effort.
2026-02-14 22:44:36 +01:00
Jorropo 167bcf2863 fix typo in PIN_GPS_SWITCH (#9648)
Wasn't caught by CI.
2026-02-14 22:12:31 +01:00
Jorropo ff485d5ff8 replace delete in RadioInterface.cpp with std::unique_ptr (#9645)
Is part of the unique_ptr modernization effort.
2026-02-14 21:39:26 +01:00
Eric BarchandJorropo 184e4ddd83 Undefine LED_BUILTIN for Heltec v2 variant (#9647)
* Undefine LED_BUILTIN for Heltec v2 variant

* Undefine LED_BUILTIN for Heltec v2.1 variant

---------

Co-authored-by: Jorropo <jorropo.pgm@gmail.com>
2026-02-14 21:30:16 +01:00
Jorropo aed2c7ca9b replace delete in EInkDynamicDisplay.{cpp,h} with std::unique_ptr (#9643)
Is part of the unique_ptr modernization effort.
2026-02-14 19:56:53 +01:00
Jorropo e1f9ccd337 replace delete in RedirectablePrint.cpp with std::unique_ptr (#9642)
Is part of the unique_ptr modernization effort.
2026-02-14 19:32:16 +01:00
Jorropo df14561560 convert GPS global and some new in gps.cpp to unique_ptr (#9628)
Trying this to see if anything bad happen if I were to replace most raw pointers with unique_ptr.

I didn't used std::make_unique since it is only supported in C++14 and onwards but until we update esp32 to arduino 3.x the ESP32 xtensa chips use a C++11 std.
2026-02-14 19:12:59 +01:00
220 changed files with 5598 additions and 694 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
steps:
- name: Stale PR+Issues
uses: actions/stale@v10.1.1
uses: actions/stale@v10.2.0
with:
days-before-stale: 45
stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days.
+1
View File
@@ -33,6 +33,7 @@ __pycache__
*~
venv/
.venv/
release/
.vscode/extensions.json
/compile_commands.json
+7 -7
View File
@@ -8,20 +8,20 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.501
- renovate@43.10.3
- checkov@3.2.506
- renovate@43.43.2
- prettier@3.8.1
- trufflehog@3.93.3
- trufflehog@3.93.5
- yamllint@1.38.0
- bandit@1.9.3
- bandit@1.9.4
- trivy@0.69.1
- taplo@0.10.0
- ruff@0.15.1
- isort@7.0.0
- ruff@0.15.4
- isort@8.0.0
- markdownlint@0.47.0
- oxipng@10.1.0
- svgo@4.0.0
- actionlint@1.7.10
- actionlint@1.7.11
- flake8@7.3.0
- hadolint@2.14.0
- shfmt@3.6.0
+26
View File
@@ -0,0 +1,26 @@
# Lightweight container for running native PlatformIO tests on non-Linux hosts
FROM python:3.14-slim-trixie
ENV DEBIAN_FRONTEND=noninteractive
ENV PIP_ROOT_USER_ACTION=ignore
# hadolint ignore=DL3008
RUN apt-get update && apt-get install --no-install-recommends -y \
g++ git ca-certificates pkg-config \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir platformio==6.1.19 \
&& useradd --create-home --shell /usr/sbin/nologin meshtastic
WORKDIR /firmware
RUN chown -R meshtastic:meshtastic /firmware
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD platformio --version || exit 1
USER meshtastic
# Run tests by default; override with docker run args for specific filters
CMD ["platformio", "test", "-e", "coverage", "-v"]
+1 -1
View File
@@ -23,4 +23,4 @@ for BOARD in $BOARDS; do
CHECK="${CHECK} -e ${BOARD}"
done
pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high
pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=low --fail-on-defect=medium --fail-on-defect=high
@@ -9,3 +9,5 @@ Lora:
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.0
GPIO_DETECT_PA: 13
TX_GAIN_LORA: [8, 8, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
+16
View File
@@ -0,0 +1,16 @@
# For use with Armbian luckfox-lyra-ultra-w
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
# 1 Watt Lyra Ultra hat
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
CS: 10
IRQ: 5
Busy: 11
Reset: 9
RXen: 14
spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7)
spiSpeed: 2000000
+17
View File
@@ -0,0 +1,17 @@
# For use with Armbian luckfox-lyra-ultra-w
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
# 2 Watt Lyra Ultra hat
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
SX126X_MAX_POWER: 8
CS: 10
IRQ: 5
Busy: 11
Reset: 9
RXen: 14
spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7)
spiSpeed: 2000000
@@ -0,0 +1,25 @@
# For use with Armbian luckfox-lyra // luckfox-lyra-plus
# Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config
# Waveshare LoRa HAT for Raspberry Pi Pico
# https://www.waveshare.com/wiki/Pico-LoRa-SX1262
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
spidev: spidev0.0
CS: # GPIO0_B5
pin: 13
gpiochip: 0
line: 13
IRQ: # GPIO1_C2
pin: 50
gpiochip: 1
line: 18
Busy: # GPIO0_B4
pin: 12
gpiochip: 0
line: 12
Reset: # GPIO0_A2
pin: 2
gpiochip: 0
line: 2
+1 -2
View File
@@ -13,8 +13,7 @@ Lora:
# USB_Serialnum: 12345678
SX126X_MAX_POWER: 22
# Reduce output power to improve EMI
NUM_PA_POINTS: 22
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
# Note: This module integrates an additional PA to achieve higher output power.
# The 'power' parameter here does not represent the actual RF output.
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
+1 -2
View File
@@ -13,8 +13,7 @@ Lora:
# USB_Serialnum: 12345678
SX126X_MAX_POWER: 22
# Reduce output power to improve EMI
NUM_PA_POINTS: 22
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
# Note: This module integrates an additional PA to achieve higher output power.
# The 'power' parameter here does not represent the actual RF output.
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Run native PlatformIO tests inside Docker (for macOS / non-Linux hosts).
#
# Usage:
# ./bin/test-native-docker.sh # run all native tests
# ./bin/test-native-docker.sh -f test_transmit_history # run specific test filter
# ./bin/test-native-docker.sh --rebuild # force rebuild the image
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
IMAGE_NAME="meshtastic-native-test"
REBUILD=false
EXTRA_ARGS=()
for arg in "$@"; do
if [[ "$arg" == "--rebuild" ]]; then
REBUILD=true
else
EXTRA_ARGS+=("$arg")
fi
done
if $REBUILD || ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then
echo "Building test image (first run may take a few minutes)..."
docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/Dockerfile.test" "$ROOT_DIR"
fi
# Disable BUILD_EPOCH to avoid full rebuilds between test runs (matches CI)
sed_cmd='s/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/'
# Default: run all tests. Pass extra args (e.g. -f test_transmit_history) through.
if [[ ${#EXTRA_ARGS[@]} -eq 0 ]]; then
CMD=("platformio" "test" "-e" "coverage" "-v")
else
CMD=("platformio" "test" "-e" "coverage" "-v" "${EXTRA_ARGS[@]}")
fi
exec docker run --rm \
-v "$ROOT_DIR:/src:ro" \
"$IMAGE_NAME" \
bash -c "rm -rf /tmp/fw-test && cp -a /src /tmp/fw-test && cd /tmp/fw-test && sed -i '${sed_cmd}' platformio.ini && ${CMD[*]}"
+40
View File
@@ -0,0 +1,40 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_ESP32S3_DEV",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "LilyGo Mini-Epaper-S3 (4 MB Flash, 2MB PSRAM)",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"require_upload_port": true,
"speed": 460800
},
"url": "https://www.lilygo.cc",
"vendor": "LilyGo"
}
+1 -1
View File
@@ -8,7 +8,7 @@
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=0"
],
+22 -38
View File
@@ -117,7 +117,7 @@ lib_deps =
[radiolib_base]
lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
jgromes/RadioLib@7.5.0
jgromes/RadioLib@7.6.0
[device-ui_base]
lib_deps =
@@ -138,7 +138,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library
adafruit/Adafruit BME280 Library@2.3.0
# renovate: datasource=custom.pio depName=Adafruit DPS310 packageName=adafruit/library/Adafruit DPS310
adafruit/Adafruit DPS310@1.1.5
adafruit/Adafruit DPS310@1.1.6
# renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library
adafruit/Adafruit MCP9808 Library@2.0.2
# renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library
@@ -150,7 +150,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
adafruit/Adafruit LIS3DH@1.3.0
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
adafruit/Adafruit AHTX0@2.0.5
adafruit/Adafruit AHTX0@2.0.6
# renovate: datasource=custom.pio depName=Adafruit LSM6DS packageName=adafruit/library/Adafruit LSM6DS
adafruit/Adafruit LSM6DS@4.7.4
# renovate: datasource=custom.pio depName=Adafruit TSL2591 packageName=adafruit/library/Adafruit TSL2591 Library
@@ -158,7 +158,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=EmotiBit MLX90632 packageName=emotibit/library/EmotiBit MLX90632
emotibit/EmotiBit MLX90632@1.0.8
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
adafruit/Adafruit MLX90614 Library@2.1.5
adafruit/Adafruit MLX90614 Library@2.1.6
# renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow
https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
@@ -180,12 +180,12 @@ lib_deps =
# renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150
dfrobot/DFRobot_BMM150@1.0.0
# renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561
adafruit/Adafruit TSL2561@1.1.2
adafruit/Adafruit TSL2561@1.1.3
# renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE
wollewald/BH1750_WE@1.1.10
; (not included in native / portduino)
[environmental_extra]
; Common environmental sensor libraries (not included in native / portduino)
[environmental_extra_common]
lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
adafruit/Adafruit BMP3XX Library@2.1.6
@@ -205,10 +205,6 @@ lib_deps =
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
closedcube/ClosedCube OPT3001@1.1.2
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
boschsensortec/bsec2@1.10.2610
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
boschsensortec/BME68x Sensor Library@1.3.40408
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
@@ -217,33 +213,21 @@ lib_deps =
sensirion/Sensirion I2C SCD4x@1.1.0
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
sensirion/Sensirion I2C SFA3x@1.0.0
# renovate: datasource=custom.pio depName=Sensirion I2C SCD30 packageName=sensirion/library/Sensirion I2C SCD30
sensirion/Sensirion I2C SCD30@1.0.0
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
; Environmental sensors with BSEC2 (Bosch proprietary IAQ)
[environmental_extra]
lib_deps =
${environmental_extra_common.lib_deps}
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
boschsensortec/bsec2@1.10.2610
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
boschsensortec/BME68x Sensor Library@1.3.40408
; Environmental sensors without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
[environmental_extra_no_bsec]
lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
adafruit/Adafruit BMP3XX Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
adafruit/Adafruit MAX1704X@1.0.3
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
adafruit/Adafruit SHTC3 Library@1.0.2
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
adafruit/Adafruit LPS2X@2.0.6
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
adafruit/Adafruit SHT31 Library@2.2.2
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
adafruit/Adafruit VEML7700 Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library
adafruit/Adafruit SHT4x Library@1.0.5
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
closedcube/ClosedCube OPT3001@1.1.2
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
sensirion/Sensirion Core@0.7.3
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
sensirion/Sensirion I2C SFA3x@1.0.0
${environmental_extra_common.lib_deps}
# renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680
adafruit/Adafruit BME680 Library@^2.0.5
+2
View File
@@ -7,6 +7,8 @@
#include "sleep.h"
#ifdef HAS_NCP5623
#include <Wire.h>
#include <NCP5623.h>
#endif
+11 -16
View File
@@ -4,6 +4,7 @@
#include "configuration.h"
#include "main.h"
#include "sleep.h"
#include <memory>
#ifdef HAS_I2S
#include <AudioFileSourcePROGMEM.h>
@@ -29,9 +30,9 @@ class AudioThread : public concurrency::OSThread
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
#endif
setCPUFast(true);
rtttlFile = new AudioFileSourcePROGMEM(data, len);
i2sRtttl = new AudioGeneratorRTTTL();
i2sRtttl->begin(rtttlFile, audioOut);
rtttlFile = std::unique_ptr<AudioFileSourcePROGMEM>(new AudioFileSourcePROGMEM(data, len));
i2sRtttl = std::unique_ptr<AudioGeneratorRTTTL>(new AudioGeneratorRTTTL());
i2sRtttl->begin(rtttlFile.get(), audioOut.get());
}
// Also handles actually playing the RTTTL, needs to be called in loop
@@ -47,14 +48,10 @@ class AudioThread : public concurrency::OSThread
{
if (i2sRtttl != nullptr) {
i2sRtttl->stop();
delete i2sRtttl;
i2sRtttl = nullptr;
}
if (rtttlFile != nullptr) {
delete rtttlFile;
rtttlFile = nullptr;
}
rtttlFile = nullptr;
setCPUFast(false);
#ifdef T_LORA_PAGER
@@ -66,16 +63,14 @@ class AudioThread : public concurrency::OSThread
{
if (i2sRtttl != nullptr) {
i2sRtttl->stop();
delete i2sRtttl;
i2sRtttl = nullptr;
}
#ifdef T_LORA_PAGER
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
#endif
ESP8266SAM *sam = new ESP8266SAM;
sam->Say(audioOut, text);
delete sam;
auto sam = std::unique_ptr<ESP8266SAM>(new ESP8266SAM);
sam->Say(audioOut.get(), text);
setCPUFast(false);
#ifdef T_LORA_PAGER
io.digitalWrite(EXPANDS_AMP_EN, LOW);
@@ -96,15 +91,15 @@ class AudioThread : public concurrency::OSThread
private:
void initOutput()
{
audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S);
audioOut = std::unique_ptr<AudioOutputI2S>(new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S));
audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK);
audioOut->SetGain(0.2);
};
AudioGeneratorRTTTL *i2sRtttl = nullptr;
AudioOutputI2S *audioOut = nullptr;
std::unique_ptr<AudioGeneratorRTTTL> i2sRtttl = nullptr;
std::unique_ptr<AudioOutputI2S> audioOut = nullptr;
AudioFileSourcePROGMEM *rtttlFile = nullptr;
std::unique_ptr<AudioFileSourcePROGMEM> rtttlFile = nullptr;
};
#endif
+5 -3
View File
@@ -704,11 +704,11 @@ bool Power::setup()
found = true;
} else if (analogInit()) {
found = true;
}
} else {
#ifdef NRF_APM
found = true;
found = true;
#endif
}
#ifdef EXT_PWR_DETECT
attachInterrupt(
EXT_PWR_DETECT,
@@ -846,8 +846,10 @@ void Power::readPowerStatus()
if (batteryLevel) {
hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse;
#ifndef NRF_APM
usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse;
isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse;
#endif
if (hasBattery) {
batteryVoltageMv = batteryLevel->getBattVoltage();
// If the AXP192 returns a valid battery percentage, use it
+4
View File
@@ -182,7 +182,11 @@ static void darkEnter()
static void serialEnter()
{
LOG_POWERFSM("State: serialEnter");
#ifndef ARCH_NRF52
// nRF52 runs BLE on SoftDevice independently of USB serial — no need to disable it.
// (Same rationale as nbEnter() which already guards this with #ifdef ARCH_ESP32)
setBluetoothEnable(false);
#endif
if (screen) {
screen->setOn(true);
}
+18 -29
View File
@@ -227,34 +227,21 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected();
#endif
if (isBleConnected) {
char *message;
size_t initialLen;
size_t len;
initialLen = strlen(format);
message = new char[initialLen + 1];
len = vsnprintf(message, initialLen + 1, format, arg);
if (len > initialLen) {
delete[] message;
message = new char[len + 1];
vsnprintf(message, len + 1, format, arg);
}
auto thread = concurrency::OSThread::currentThread;
meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero;
logRecord.level = getLogLevel(logLevel);
strcpy(logRecord.message, message);
vsprintf(logRecord.message, format, arg);
if (thread)
strcpy(logRecord.source, thread->ThreadName.c_str());
logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true);
uint8_t *buffer = new uint8_t[meshtastic_LogRecord_size];
size_t size = pb_encode_to_bytes(buffer, meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[meshtastic_LogRecord_size]);
size_t size = pb_encode_to_bytes(buffer.get(), meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
#ifdef ARCH_ESP32
nimbleBluetooth->sendLog(buffer, size);
nimbleBluetooth->sendLog(buffer.get(), size);
#elif defined(ARCH_NRF52)
nrf52Bluetooth->sendLog(buffer, size);
nrf52Bluetooth->sendLog(buffer.get(), size);
#endif
delete[] message;
delete[] buffer;
}
}
#else
@@ -292,8 +279,8 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
// append \n to format
size_t len = strlen(format);
char *newFormat = new char[len + 2];
strcpy(newFormat, format);
auto newFormat = std::unique_ptr<char[]>(new char[len + 2]);
strcpy(newFormat.get(), format);
newFormat[len] = '\n';
newFormat[len + 1] = '\0';
@@ -310,23 +297,18 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
va_end(arg);
}
if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
delete[] newFormat;
return;
}
}
if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
delete[] newFormat;
return;
} else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
delete[] newFormat;
return;
} else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
delete[] newFormat;
return;
}
#endif
if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
delete[] newFormat;
return;
}
@@ -338,11 +320,19 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
#endif
va_list arg;
va_list arg_copy;
va_start(arg, format);
log_to_serial(logLevel, newFormat, arg);
log_to_syslog(logLevel, newFormat, arg);
log_to_ble(logLevel, newFormat, arg);
va_copy(arg_copy, arg);
log_to_serial(logLevel, newFormat.get(), arg_copy);
va_end(arg_copy);
va_copy(arg_copy, arg);
log_to_syslog(logLevel, newFormat.get(), arg_copy);
va_end(arg_copy);
log_to_ble(logLevel, newFormat.get(), arg);
va_end(arg);
#ifdef HAS_FREE_RTOS
@@ -352,7 +342,6 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
#endif
}
delete[] newFormat;
return;
}
+2
View File
@@ -35,6 +35,8 @@ void consoleInit()
#if defined(SERIAL_HAS_ON_RECEIVE)
// onReceive does only exist for HardwareSerial not for USB CDC serial
Port.onReceive([sc]() { sc->rxInt(); });
#else
(void)sc;
#endif
DEBUG_PORT.rpInit(); // Simply sets up semaphore
}
+4 -2
View File
@@ -76,8 +76,10 @@ bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrit
void NotifiedWorkerThread::checkNotification()
{
auto n = notification;
notification = 0; // clear notification
// Atomically read and clear. (This avoids a potential race condition where an interrupt handler could set a new notification
// after checkNotification reads but before it clears, which would cause us to miss that notification until the next one comes
// in.)
auto n = notification.exchange(0); // read+clear atomically: like `n = notification; notification = 0;` but interrupt-safe
if (n) {
onNotify(n);
}
+2 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "OSThread.h"
#include <atomic>
namespace concurrency
{
@@ -13,7 +14,7 @@ class NotifiedWorkerThread : public OSThread
/**
* The notification that was most recently used to wake the thread. Read from runOnce()
*/
uint32_t notification = 0;
std::atomic<uint32_t> notification{0};
public:
NotifiedWorkerThread(const char *name) : OSThread(name) {}
+11 -6
View File
@@ -1,24 +1,29 @@
#pragma once
#include <functional>
#include <utility>
#include "concurrency/OSThread.h"
namespace concurrency
{
/**
* @brief Periodically invoke a callback. This just provides C-style callback conventions
* rather than a virtual function - FIXME, remove?
* @brief Periodically invoke a callback.
* Supports both legacy function pointers and modern callables.
*/
class Periodic : public OSThread
{
int32_t (*callback)();
public:
// callback returns the period for the next callback invocation (or 0 if we should no longer be called)
Periodic(const char *name, int32_t (*_callback)()) : OSThread(name), callback(_callback) {}
Periodic(const char *name, int32_t (*cb)()) : OSThread(name), callback(cb) {}
Periodic(const char *name, std::function<int32_t()> cb) : OSThread(name), callback(std::move(cb)) {}
protected:
int32_t runOnce() override { return callback(); }
int32_t runOnce() override { return callback ? callback() : 0; }
private:
std::function<int32_t()> callback;
};
} // namespace concurrency
+65
View File
@@ -244,6 +244,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define BQ25896_ADDR 0x6B
#define LTR553ALS_ADDR 0x23
#define SEN5X_ADDR 0x69
#define SCD30_ADDR 0x61
// -----------------------------------------------------------------------------
// ACCELEROMETER
@@ -527,5 +528,69 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define HAS_SCREEN 0
#endif
// -----------------------------------------------------------------------------
// MESHTASTIC_LOCKDOWN — combined hardened build flag
//
// All targets:
// DEBUG_MUTE — suppress serial/USB log output
// MESHTASTIC_EXCLUDE_* — strip non-essential modules
// MESHTASTIC_PHONEAPI_ACCESS_CONTROL — redact keys from unauthenticated BLE/USB/TCP clients
// HARDENED_DEFAULTS — set is_managed, RANDOM_PIN, TAK role at first boot
//
// nRF52 (CC310 hardware crypto):
// MESHTASTIC_ENCRYPTED_STORAGE — AES-128-CTR + HMAC-SHA256 at-rest encryption
// MESHTASTIC_ENABLE_APPROTECT — write UICR APPROTECT to block debug probe
//
// ESP32 (stub — mbedTLS crypto TODO):
// MESHTASTIC_ENCRYPTED_STORAGE — same API, plaintext pass-through until ported
// APPROTECT equivalent — burn JTAG_DISABLE + flash encryption via espsecure.py
// -----------------------------------------------------------------------------
#ifdef MESHTASTIC_LOCKDOWN
// Suppress all serial/USB-CDC log output — prevents info leakage via physical access
#define DEBUG_MUTE
// Strip non-essential modules to reduce attack surface
#define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1
#define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1
#define MESHTASTIC_EXCLUDE_AIR_QUALITY 1
#define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1
#define MESHTASTIC_EXCLUDE_POWER_TELEMETRY 1
#define MESHTASTIC_EXCLUDE_RANGETEST 1
#define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1
#define MESHTASTIC_EXCLUDE_STOREFORWARD 1
#define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1
#define MESHTASTIC_EXCLUDE_SERIAL 1
#define MESHTASTIC_EXCLUDE_POWERSTRESS 1
#define MESHTASTIC_EXCLUDE_DROPZONE 1
#define MESHTASTIC_EXCLUDE_REPLYBOT 1
#define MESHTASTIC_EXCLUDE_PAXCOUNTER 1
#define MESHTASTIC_EXCLUDE_AUDIO 1
#define MESHTASTIC_EXCLUDE_STATUS 1
#define MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE 1
#define MESHTASTIC_EXCLUDE_POWERMON 1
#define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1
// Access control and hardened defaults are pure C++ — no platform-specific deps.
#define MESHTASTIC_PHONEAPI_ACCESS_CONTROL 1
#define HARDENED_DEFAULTS 1
// Platform-specific security features:
#if defined(ARCH_NRF52)
// Full encrypted storage via CC310 hardware AES-128-CTR + HMAC-SHA256.
#define MESHTASTIC_ENCRYPTED_STORAGE 1
// Hardware JTAG/SWD lockout: firmware writes NRF_UICR->APPROTECT = 0x00 once.
#define MESHTASTIC_ENABLE_APPROTECT 1
#elif defined(ARCH_ESP32)
// Encrypted storage stub: plaintext pass-through, same API, crypto TODO (mbedTLS).
// APPROTECT equivalent: burn JTAG_DISABLE eFuse and enable flash encryption
// via espsecure.py at provisioning time — not firmware-controlled.
#define MESHTASTIC_ENCRYPTED_STORAGE 1
#else
#warning "MESHTASTIC_LOCKDOWN: encrypted storage not available on this platform; access control and hardened defaults still active"
#endif
#endif // MESHTASTIC_LOCKDOWN
#include "DebugConfiguration.h"
#include "RF95Configuration.h"
+2 -1
View File
@@ -91,7 +91,8 @@ class ScanI2C
CST226SE,
SEN5X,
SFA30,
CW2015
CW2015,
SCD30
} DeviceType;
// typedef uint8_t DeviceAddress;
+30 -5
View File
@@ -117,6 +117,25 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
return value;
}
bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint16_t command, uint8_t expectedLength) const
{
TwoWire *i2cBus = fetchI2CBus(addr);
i2cBus->beginTransmission(addr.address);
if (command > 0xFF) {
i2cBus->write((uint8_t)(command >> 8));
}
i2cBus->write((uint8_t)(command & 0xFF));
if (i2cBus->endTransmission() != 0) {
return false;
}
delay(20);
uint8_t received = i2cBus->requestFrom(addr.address, expectedLength);
bool match = (received == expectedLength);
while (i2cBus->available())
i2cBus->read();
return match;
}
/// for SEN5X detection
// Note, this code needs to be called before setting the I2C bus speed
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
@@ -432,8 +451,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
type = OPT3001;
logFoundDevice("OPT3001", (uint8_t)addr.address);
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) !=
0) { // unique SHT4x serial number (6 bytes inc. CRC)
} else if (i2cCommandResponseLength(addr, 0x89, 6)) { // SHT4x serial number (6 bytes inc. CRC)
type = SHT4X;
logFoundDevice("SHT4X", (uint8_t)addr.address);
} else {
@@ -458,13 +476,19 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break;
case LPS22HB_ADDR_ALT:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD060), 48); // get device marking
if (registerValue != 0) {
// SFA30 detection: send 2-byte command 0xD060 (Get Device Marking) and check for 48-byte response
if (i2cCommandResponseLength(addr, 0xD060, 48)) {
type = SFA30;
logFoundDevice("SFA30", (uint8_t)addr.address);
break;
}
// TODO - What happens with these two?
// Fallback: LPS22HB detection at alternate address using WHO_AM_I register (0x0F == 0xB1)
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1);
if (registerValue == 0xB1) {
type = LPS22HB;
logFoundDevice("LPS22HB", (uint8_t)addr.address);
}
break;
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
@@ -557,6 +581,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(SCD30_ADDR, SCD30, "SCD30", (uint8_t)addr.address);
case CST328_ADDR:
// Do we have the CST328 or the CST226SE
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1);
+2
View File
@@ -55,6 +55,8 @@ class ScanI2CTwoWire : public ScanI2C
uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const;
bool i2cCommandResponseLength(DeviceAddress addr, uint16_t command, uint8_t expectedLength) const;
DeviceType probeOLED(ScanI2C::DeviceAddress) const;
static void logFoundDevice(const char *device, uint8_t address);
+10 -12
View File
@@ -52,7 +52,7 @@ SerialUART *GPS::_serial_gps = &GPS_SERIAL_PORT;
HardwareSerial *GPS::_serial_gps = nullptr;
#endif
GPS *gps = nullptr;
std::unique_ptr<GPS> gps = nullptr;
static GPSUpdateScheduling scheduling;
@@ -127,7 +127,7 @@ static int32_t gpsSwitch()
return 1000;
}
static concurrency::Periodic *gpsPeriodic;
static std::unique_ptr<concurrency::Periodic> gpsPeriodic;
#endif
static void UBXChecksum(uint8_t *message, size_t length)
@@ -1485,7 +1485,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
if (bufferSize > 2048)
bufferSize = 2048;
char *response = new char[bufferSize](); // Dynamically allocate based on baud rate
auto response = std::unique_ptr<char[]>(new char[bufferSize]); // Dynamically allocate based on baud rate
uint16_t responseLen = 0;
unsigned long start = millis();
while (millis() - start < timeout) {
@@ -1501,19 +1501,18 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) {
// check if we can see our chips
for (const auto &chipInfo : responseMap) {
if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) {
if (strstr(response.get(), chipInfo.detectionString.c_str()) != nullptr) {
#ifdef GPS_DEBUG
LOG_DEBUG(response);
LOG_DEBUG(response.get());
#endif
LOG_INFO("%s detected", chipInfo.chipName.c_str());
delete[] response; // Cleanup before return
return chipInfo.driver;
}
}
}
if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') {
#ifdef GPS_DEBUG
LOG_DEBUG(response);
LOG_DEBUG(response.get());
#endif
// Reset the response buffer for the next potential message
responseLen = 0;
@@ -1522,13 +1521,12 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
}
}
#ifdef GPS_DEBUG
LOG_DEBUG(response);
LOG_DEBUG(response.get());
#endif
delete[] response; // Cleanup before return
return GNSS_MODEL_UNKNOWN; // Return unknown on timeout
}
GPS *GPS::createGps()
std::unique_ptr<GPS> GPS::createGps()
{
int8_t _rx_gpio = config.position.rx_gpio;
int8_t _tx_gpio = config.position.tx_gpio;
@@ -1553,7 +1551,7 @@ GPS *GPS::createGps()
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
return nullptr;
GPS *new_gps = new GPS;
auto new_gps = std::unique_ptr<GPS>(new GPS());
new_gps->rx_gpio = _rx_gpio;
new_gps->tx_gpio = _tx_gpio;
@@ -1581,7 +1579,7 @@ GPS *GPS::createGps()
#ifdef PIN_GPS_SWITCH
// toggle GPS via external GPIO switch
pinMode(PIN_GPS_SWITCH, INPUT);
gpsPeriodic = new concurrency::Periodic("GPSSwitch", gpsSwitch);
gpsPeriodic = std::unique_ptr<concurrency::Periodic>(new concurrency::Periodic("GPSSwitch", gpsSwitch));
#endif
// Currently disabled per issue #525 (TinyGPS++ crash bug)
+4 -2
View File
@@ -2,6 +2,8 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_GPS
#include <memory>
#include "GPSStatus.h"
#include "GpioLogic.h"
#include "Observer.h"
@@ -118,7 +120,7 @@ class GPS : private concurrency::OSThread
// Creates an instance of the GPS class.
// Returns the new instance or null if the GPS is not present.
static GPS *createGps();
static std::unique_ptr<GPS> createGps();
// Wake the GPS hardware - ready for an update
void up();
@@ -256,5 +258,5 @@ class GPS : private concurrency::OSThread
uint8_t fixeddelayCtr = 0;
};
extern GPS *gps;
extern std::unique_ptr<GPS> gps;
#endif // Exclude GPS
+1 -1
View File
@@ -312,7 +312,7 @@ const char *RtcName(RTCQuality quality)
* @param t The time to potentially set the RTC to.
* @return True if the RTC was set to the provided time, false otherwise.
*/
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t)
{
/* Convert to unix time
The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970
+1 -1
View File
@@ -41,7 +41,7 @@ extern uint32_t lastSetFromPhoneNtpOrGps;
/// If we haven't yet set our RTC this boot, set it from a GPS derived time
RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false);
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t);
RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t);
/// Return a string name for the quality
const char *RtcName(RTCQuality quality);
+11
View File
@@ -259,6 +259,17 @@ bool EInkDisplay::connect()
adafruitDisplay->setRotation(3);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
}
#elif defined(MINI_EPAPER_S3)
spi1 = new SPIClass(HSPI);
spi1->begin(PIN_SPI1_SCK, PIN_SPI1_MISO, PIN_SPI1_MOSI, PIN_EINK_CS);
// Create GxEPD2 objects
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
// Init GxEPD2
adafruitDisplay->init();
adafruitDisplay->setRotation(1);
#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213)
// Detect display model, before starting SPI
+2 -1
View File
@@ -93,7 +93,8 @@ class EInkDisplay : public OLEDDisplay
SPIClass *hspi = NULL;
#endif
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) || \
defined(MINI_EPAPER_S3)
SPIClass *spi1 = NULL;
#endif
+3 -3
View File
@@ -10,7 +10,7 @@ EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDI
{
// If tracking ghost pixels, grab memory
#ifdef EINK_LIMIT_GHOSTING_PX
dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros
dirtyPixels = std::unique_ptr<uint8_t[]>(new uint8_t[EInkDisplay::displayBufferSize]()); // Init with zeros
#endif
}
@@ -19,7 +19,7 @@ EInkDynamicDisplay::~EInkDynamicDisplay()
{
// If we were tracking ghost pixels, free the memory
#ifdef EINK_LIMIT_GHOSTING_PX
delete[] dirtyPixels;
dirtyPixels = nullptr;
#endif
}
@@ -454,7 +454,7 @@ void EInkDynamicDisplay::checkExcessiveGhosting()
void EInkDynamicDisplay::resetGhostPixelTracking()
{
// Copy the current frame into dirtyPixels[] from the display buffer
memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize);
memcpy(dirtyPixels.get(), EInkDisplay::buffer, EInkDisplay::displayBufferSize);
}
#endif // EINK_LIMIT_GHOSTING_PX
+6 -5
View File
@@ -1,6 +1,7 @@
#pragma once
#include "configuration.h"
#include <memory>
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
@@ -116,11 +117,11 @@ class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWo
// Optional - track ghosting, pixel by pixel
// May 2024: no longer used by any display. Kept for possible future use.
#ifdef EINK_LIMIT_GHOSTING_PX
void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh
void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit
void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display.
uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem)
uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use
void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh
void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit
void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display.
std::unique_ptr<uint8_t[]> dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem)
uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use
#endif
// Conditional - async full refresh - only with modified meshtastic/GxEPD2
+2
View File
@@ -1,4 +1,6 @@
#ifdef HAS_LP5562
#include <Wire.h>
#include <LP5562.h>
extern LP5562 rgbw;
+4
View File
@@ -875,6 +875,10 @@ int32_t Screen::runOnce()
break;
case Cmd::STOP_ALERT_FRAME:
NotificationRenderer::pauseBanner = false;
// Return from one-off alert mode back to regular frames.
if (!showingNormalScreen && NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
setFrames();
}
break;
case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
+2 -2
View File
@@ -1209,8 +1209,8 @@ void TFTDisplay::display(bool fromBlank)
bool somethingChanged = false;
// Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step
colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8);
colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8);
colorTftMesh = __builtin_bswap16(TFT_MESH);
colorTftBlack = __builtin_bswap16(TFT_BLACK);
y = 0;
while (y < displayHeight) {
+11 -12
View File
@@ -539,7 +539,7 @@ void menuHandler::messageResponseMenu()
// If viewing ALL chats, hide “Mute Chat”
if (mode != graphics::MessageRenderer::ThreadMode::ALL && mode != graphics::MessageRenderer::ThreadMode::DIRECT) {
const uint8_t chIndex = (threadChannel != 0) ? (uint8_t)threadChannel : channels.getPrimaryIndex();
auto &chan = channels.getByIndex(chIndex);
const auto &chan = channels.getByIndex(chIndex);
optionsArray[options] = chan.settings.module_settings.is_muted ? "Unmute Channel" : "Mute Channel";
optionsEnumArray[options++] = MuteChannel;
@@ -831,7 +831,7 @@ void menuHandler::messageViewModeMenu()
// Gather unique peers
auto dms = messageStore.getDirectMessages();
std::vector<uint32_t> uniquePeers;
for (auto &m : dms) {
for (const auto &m : dms) {
uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
uniquePeers.push_back(peer);
@@ -1397,7 +1397,7 @@ void menuHandler::manageNodeMenu()
}
if (selected == Favorite) {
auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
const auto *n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
if (!n) {
return;
}
@@ -2292,14 +2292,13 @@ void menuHandler::wifiToggleMenu()
void menuHandler::screenOptionsMenu()
{
// Check if brightness is supported
bool hasSupportBrightness = false;
#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
hasSupportBrightness = true;
#endif
#if defined(T_DECK)
// TDeck Doesn't seem to support brightness at all, at least not reliably
hasSupportBrightness = false;
bool hasSupportBrightness = false;
#elif defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
bool hasSupportBrightness = true;
#else
bool hasSupportBrightness = false;
#endif
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
@@ -2444,7 +2443,7 @@ void menuHandler::frameTogglesMenu()
nodelist_hopsignal,
nodelist_distance,
nodelist_bearings,
gps,
gps_position,
lora,
clock,
show_favorites,
@@ -2482,7 +2481,7 @@ void menuHandler::frameTogglesMenu()
#endif
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
optionsEnumArray[options++] = gps;
optionsEnumArray[options++] = gps_position;
#endif
optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa";
@@ -2545,7 +2544,7 @@ void menuHandler::frameTogglesMenu()
screen->toggleFrameVisibility("nodelist_bearings");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == gps) {
} else if (selected == gps_position) {
screen->toggleFrameVisibility("gps");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
+10 -13
View File
@@ -171,7 +171,7 @@ unsigned long getModeCycleIntervalMs()
int calculateMaxScroll(int totalEntries, int visibleRows)
{
return std::max(0, (totalEntries - 1) / (visibleRows * 2));
return max(0, (totalEntries - 1) / (visibleRows * 2));
}
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
@@ -187,13 +187,12 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
if (totalEntries <= visibleNodeRows * columns)
return;
int scrollbarX = display->getWidth() - 2;
int scrollbarHeight = display->getHeight() - scrollStartY - 10;
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
int perPage = visibleNodeRows * columns;
int maxScroll = std::max(0, (totalEntries - 1) / perPage);
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
int thumbHeight = max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) /
max(1, max(0, (totalEntries - 1) / (visibleNodeRows * columns)));
int scrollbarX = display->getWidth() - 2;
for (int i = 0; i < thumbHeight; i++) {
display->setPixel(scrollbarX, thumbY + i);
}
@@ -556,13 +555,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
int maxScroll = 0;
if (perPage > 0) {
maxScroll = std::max(0, (totalEntries - 1) / perPage);
maxScroll = max(0, (totalEntries - 1) / perPage);
}
if (scrollIndex > maxScroll)
scrollIndex = maxScroll;
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
int endIndex = min(startIndex + visibleNodeRows * totalColumns, totalEntries);
int yOffset = 0;
int col = 0;
int lastNodeY = y;
@@ -580,7 +579,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
if (extras)
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
yOffset += rowYOffset;
shownCount++;
rowCount++;
@@ -613,13 +612,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
if (millis() - popupTime < POPUP_DURATION_MS) {
popupTotal = totalEntries;
int perPage = visibleNodeRows * totalColumns;
popupStart = startIndex + 1;
popupEnd = std::min(startIndex + perPage, totalEntries);
popupEnd = min(startIndex + perPage, totalEntries);
popupPage = (scrollIndex + 1);
popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage);
popupMaxPage = max(1, (totalEntries + perPage - 1) / perPage);
char buf[32];
snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage);
+3 -1
View File
@@ -288,7 +288,8 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
// **********************
// * Favorite Node Info *
// **********************
void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
// cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library
void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
if (favoritedNodes.empty())
return;
@@ -1388,6 +1389,7 @@ static int8_t lastFrameIndex = -1;
static uint32_t lastFrameChangeTime = 0;
constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000;
// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library
void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state)
{
int currentFrame = state->currentFrame;
+1 -1
View File
@@ -49,7 +49,7 @@ class UIRenderer
// Navigation bar overlay
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
@@ -42,7 +42,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused)
{
// Contingency only
// - pin wasn't set
if (pin != (uint8_t)-1) {
if (pin != static_cast<uint8_t>(-1)) {
off();
pinMode(pin, INPUT); // High impedance - unnecessary?
} else
@@ -55,7 +55,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused)
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
void LatchingBacklight::peek()
{
assert(pin != (uint8_t)-1);
assert(pin != static_cast<uint8_t>(-1));
digitalWrite(pin, logicActive); // On
on = true;
latched = false;
@@ -67,7 +67,7 @@ void LatchingBacklight::peek()
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
void LatchingBacklight::latch()
{
assert(pin != (uint8_t)-1);
assert(pin != static_cast<uint8_t>(-1));
// Blink if moving from peek to latch
// Indicates to user that the transition has taken place
@@ -89,7 +89,7 @@ void LatchingBacklight::latch()
// Suitable for ending both peek and latch
void LatchingBacklight::off()
{
assert(pin != (uint8_t)-1);
assert(pin != static_cast<uint8_t>(-1));
digitalWrite(pin, !logicActive); // Off
on = false;
latched = false;
@@ -40,7 +40,7 @@ class LatchingBacklight
CallbackObserver<LatchingBacklight, void *> deepSleepObserver =
CallbackObserver<LatchingBacklight, void *>(this, &LatchingBacklight::beforeDeepSleep);
uint8_t pin = (uint8_t)-1;
uint8_t pin = static_cast<uint8_t>(-1);
bool logicActive = HIGH; // Is light active HIGH or active LOW
bool on = false; // Is light on (either peek or latched)
+30 -15
View File
@@ -34,7 +34,7 @@ void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color)
{
// Only render pixels if they fall within user's cropped region
if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight))
assignedTile->handleAppletPixel(x, y, (Color)color);
assignedTile->handleAppletPixel(x, y, static_cast<Color>(color));
}
// Link our applet to a tile
@@ -144,6 +144,21 @@ void InkHUD::Applet::resetDrawingSpace()
setFont(fontSmall);
}
// Sets one or more inputs to enabled/disabled for this applet and if they should be sent to it
void InkHUD::Applet::setInputsSubscribed(uint8_t input, bool captured)
{
if (captured)
subscribedInputs |= input;
else
subscribedInputs &= ~input;
}
// Checks if a specific input is enabled for this applet and should be sent to it
bool InkHUD::Applet::isInputSubscribed(InputMask input)
{
return (subscribedInputs & input) == input;
}
// Tell InkHUD::Renderer that we want to render now
// Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc
// When an applet decides it has heard something important, and wants to redraw, it calls this method
@@ -312,7 +327,7 @@ void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalA
}
// Print text, specifying the position of any edge / corner of the textbox
void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va)
void InkHUD::Applet::printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha, VerticalAlignment va)
{
printAt(x, y, text.c_str(), ha, va);
}
@@ -334,7 +349,7 @@ InkHUD::AppletFont InkHUD::Applet::getFont()
// Parse any text which might have "special characters"
// Re-encodes UTF-8 characters to match our 8-bit encoded fonts
std::string InkHUD::Applet::parse(std::string text)
std::string InkHUD::Applet::parse(const std::string &text)
{
return getFont().decodeUTF8(text);
}
@@ -361,10 +376,10 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node)
}
// Determine if all characters of a string are printable using the current font
bool InkHUD::Applet::isPrintable(std::string text)
bool InkHUD::Applet::isPrintable(const std::string &text)
{
// Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled
for (char &c : text) {
for (const char &c : text) {
if (c == '\x1A')
return false;
}
@@ -387,7 +402,7 @@ uint16_t InkHUD::Applet::getTextWidth(const char *text)
// Gets rendered width of a string
// Wrapper for getTextBounds
uint16_t InkHUD::Applet::getTextWidth(std::string text)
uint16_t InkHUD::Applet::getTextWidth(const std::string &text)
{
return getTextWidth(text.c_str());
}
@@ -435,7 +450,7 @@ std::string InkHUD::Applet::hexifyNodeNum(NodeNum num)
// Print text, with word wrapping
// Avoids splitting words in half, instead moving the entire word to a new line wherever possible
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text)
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text)
{
// Place the AdafruitGFX cursor to suit our "top" coord
setCursor(left, top + getFont().heightAboveCursor());
@@ -492,15 +507,15 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std
// Todo: rewrite making use of AdafruitGFX native text wrapping
char cstr[] = {0, 0};
int16_t l, t;
uint16_t w, h;
int16_t bx, by;
uint16_t bw, bh;
for (uint16_t c = 0; c < word.length(); c++) {
// Shove next char into a c string
cstr[0] = word[c];
getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h);
getTextBounds(cstr, getCursorX(), getCursorY(), &bx, &by, &bw, &bh);
// Manual newline, if next character will spill beyond screen edge
if ((l + w) > left + width)
if ((bx + bw) > left + width)
setCursor(left, getCursorY() + getFont().lineHeight());
// Print next character
@@ -519,7 +534,7 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std
// Simulate running printWrapped, to determine how tall the block of text will be.
// This is a wasteful way of handling things. Maybe some way to optimize in future?
uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text)
uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text)
{
// Cache the current crop region
int16_t cL = cropLeft;
@@ -649,7 +664,7 @@ uint16_t InkHUD::Applet::getActiveNodeCount()
// For each node in db
for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Check if heard recently, and not our own node
if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum())
@@ -702,7 +717,7 @@ std::string InkHUD::Applet::localizeDistance(uint32_t meters)
}
// Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY)
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX, uint8_t thicknessY)
{
// How many times to draw along x axis
int16_t xStart;
@@ -770,7 +785,7 @@ bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n)
│ │
└───────────────────────────────┘
*/
void InkHUD::Applet::drawHeader(std::string text)
void InkHUD::Applet::drawHeader(const std::string &text)
{
// Y position for divider
// - between header text and messages
+31 -8
View File
@@ -89,6 +89,9 @@ class Applet : public GFX
virtual void onForeground() {}
virtual void onBackground() {}
virtual void onShutdown() {}
// Input Events
virtual void onButtonShortPress() {}
virtual void onButtonLongPress() {}
virtual void onExitShort() {}
@@ -100,6 +103,18 @@ class Applet : public GFX
virtual void onFreeText(char c) {}
virtual void onFreeTextDone() {}
virtual void onFreeTextCancel() {}
// List of inputs which can be subscribed to
enum InputMask { // | No Joystick | With Joystick |
BUTTON_SHORT = 1, // | Button Click | Joystick Center Click |
BUTTON_LONG = 2, // | Button Hold | Joystick Center Hold |
EXIT_SHORT = 4, // | no-op | Back Button Click |
EXIT_LONG = 8, // | no-op | Back Button Hold |
NAV_UP = 16, // | no-op | Joystick Up |
NAV_DOWN = 32, // | no-op | Joystick Down |
NAV_LEFT = 64, // | no-op | Joystick Left |
NAV_RIGHT = 128 // | no-op | Joystick Right |
};
bool isInputSubscribed(InputMask input); // Check if input should be handled by applet, this should not be overloaded.
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
@@ -121,20 +136,28 @@ class Applet : public GFX
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
void resetCrop(); // Removes setCrop()
// User Input Handling
uint8_t subscribedInputs = 0b00000000; // Maybe uint16_t for futureproofing? other devices may need more inputs
void setInputsSubscribed(uint8_t input,
bool captured); // Set if an input should be handled by applet or not, this should not be
// overloaded. Can take multiple inputs at once if you OR/`|` them together
// Text
void setFont(AppletFont f);
AppletFont getFont();
uint16_t getTextWidth(std::string text);
uint16_t getTextWidth(const std::string &text);
uint16_t getTextWidth(const char *text);
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text); // Result of printWrapped
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping
void printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX,
uint8_t thicknessY); // Faux bold
void printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text); // Per-word line wrapping
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines
void drawHeader(std::string text); // Draw the standard applet header
void drawHeader(const std::string &text); // Draw the standard applet header
// Meshtastic Logo
@@ -150,9 +173,9 @@ class Applet : public GFX
std::string getTimeString(); // Current time, human readable
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
std::string parse(std::string text); // Handle text which might contain special chars
std::string parse(const std::string &text); // Handle text which might contain special chars
std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars
bool isPrintable(std::string); // Check for characters which the font can't print
bool isPrintable(const std::string &text); // Check for characters which the font can't print
// Convenient references
+7 -7
View File
@@ -39,11 +39,11 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding
// Caution: signed and unsigned types
int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset;
if (glyphAscender > 0)
this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender);
this->ascenderHeight = max(this->ascenderHeight, static_cast<uint8_t>(glyphAscender));
int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset;
if (glyphDescender > 0)
this->descenderHeight = max(this->descenderHeight, (uint8_t)glyphDescender);
this->descenderHeight = max(this->descenderHeight, static_cast<uint8_t>(glyphDescender));
}
// Apply any manual padding to grow or shrink the line size
@@ -52,7 +52,7 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding
descenderHeight += paddingBottom;
// Find how far the cursor advances when we "print" a space character
spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance;
spaceCharWidth = gfxFont->glyph[static_cast<uint8_t>(' ') - gfxFont->first].xAdvance;
}
/*
@@ -98,7 +98,7 @@ uint8_t InkHUD::AppletFont::widthBetweenWords()
// Convert a unicode char from set of UTF-8 bytes to UTF-32
// Used by AppletFont::applyEncoding, which remaps unicode chars for extended ASCII fonts, based on their UTF-32 value
uint32_t InkHUD::AppletFont::toUtf32(std::string utf8)
uint32_t InkHUD::AppletFont::toUtf32(const std::string &utf8)
{
uint32_t utf32 = 0;
@@ -132,7 +132,7 @@ uint32_t InkHUD::AppletFont::toUtf32(std::string utf8)
// Process a string, collating UTF-8 bytes, and sending them off for re-encoding to extended ASCII
// Not all InkHUD text is passed through here, only text which could potentially contain non-ASCII chars
std::string InkHUD::AppletFont::decodeUTF8(std::string encoded)
std::string InkHUD::AppletFont::decodeUTF8(const std::string &encoded)
{
// Final processed output
std::string decoded;
@@ -141,7 +141,7 @@ std::string InkHUD::AppletFont::decodeUTF8(std::string encoded)
std::string utf8Char;
uint8_t utf8CharSize = 0;
for (char &c : encoded) {
for (const char &c : encoded) {
// If first byte
if (utf8Char.empty()) {
@@ -178,7 +178,7 @@ std::string InkHUD::AppletFont::decodeUTF8(std::string encoded)
// Re-encode a single UTF-8 character to extended ASCII
// Target encoding depends on the font
char InkHUD::AppletFont::applyEncoding(std::string utf8)
char InkHUD::AppletFont::applyEncoding(const std::string &utf8)
{
// ##################################################### Syntactic Sugar #####################################################
#define REMAP(in, out) \
+6 -5
View File
@@ -30,20 +30,21 @@ class AppletFont
};
AppletFont();
AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0);
explicit AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0,
int8_t paddingBottom = 0);
uint8_t lineHeight();
uint8_t heightAboveCursor();
uint8_t heightBelowCursor();
uint8_t widthBetweenWords(); // Width of the space character
std::string decodeUTF8(std::string encoded);
std::string decodeUTF8(const std::string &encoded);
const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font
const GFXfont *gfxFont = nullptr; // Default value: in-built AdafruitGFX font
private:
uint32_t toUtf32(std::string utf8);
char applyEncoding(std::string utf8);
uint32_t toUtf32(const std::string &utf8);
char applyEncoding(const std::string &utf8);
uint8_t height = 8; // Default value: in-built AdafruitGFX font
uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font
@@ -81,25 +81,25 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke
uint8_t InkHUD::NodeListApplet::maxCards()
{
// Cache result. Shouldn't change during execution
static uint8_t cards = 0;
static uint8_t maxCardCount = 0;
if (!cards) {
if (!maxCardCount) {
const uint16_t height = Tile::maxDisplayDimension();
// Use a loop instead of arithmetic, because it's easier for my brain to follow
// Add cards one by one, until the latest card extends below screen
uint16_t y = cardH; // First card: no margin above
cards = 1;
maxCardCount = 1;
while (y < height) {
y += cardMarginH;
y += cardH;
cards++;
maxCardCount++;
}
}
return cards;
return maxCardCount;
}
// Draw, using info which derived applet placed into NodeListApplet::cards for us
@@ -137,12 +137,12 @@ void InkHUD::NodeListApplet::onRender(bool full)
// Gather info
// ========================================
NodeNum &nodeNum = card->nodeNum;
const NodeNum &nodeNum = card->nodeNum;
SignalStrength &signal = card->signal;
std::string longName; // handled below
std::string shortName; // handled below
std::string distance; // handled below;
uint8_t &hopsAway = card->hopsAway;
const uint8_t &hopsAway = card->hopsAway;
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
@@ -0,0 +1,79 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./UserAppletInputExample.h"
using namespace NicheGraphics;
void InkHUD::UserAppletInputExampleApplet::onActivate()
{
setGrabbed(false);
}
void InkHUD::UserAppletInputExampleApplet::onRender(bool full)
{
drawHeader("Input Example");
uint16_t headerHeight = getHeaderHeight();
std::string buttonName;
if (settings->joystick.enabled)
buttonName = "joystick center button";
else
buttonName = "user button";
std::string additional = " | Control is grabbed, long press " + buttonName + " to release controls";
if (!isGrabbed)
additional = " | Control is released, long press " + buttonName + " to grab controls";
printWrapped(0, headerHeight, width(), "Last button: " + lastInput + additional);
}
void InkHUD::UserAppletInputExampleApplet::setGrabbed(bool grabbed)
{
isGrabbed = grabbed;
setInputsSubscribed(BUTTON_SHORT | EXIT_SHORT | EXIT_LONG | NAV_UP | NAV_DOWN | NAV_LEFT | NAV_RIGHT,
grabbed); // Enables/disables grabbing all inputs
setInputsSubscribed(BUTTON_LONG, true); // Always grab this input
}
void InkHUD::UserAppletInputExampleApplet::onButtonShortPress()
{
lastInput = "BUTTON_SHORT";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onButtonLongPress()
{
lastInput = "BUTTON_LONG";
setGrabbed(!isGrabbed);
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onExitShort()
{
lastInput = "EXIT_SHORT";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onExitLong()
{
lastInput = "EXIT_LONG";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavUp()
{
lastInput = "NAV_UP";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavDown()
{
lastInput = "NAV_DOWN";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavLeft()
{
lastInput = "NAV_LEFT";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavRight()
{
lastInput = "NAV_RIGHT";
requestUpdate();
}
#endif
@@ -0,0 +1,36 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
namespace NicheGraphics::InkHUD
{
class UserAppletInputExampleApplet : public Applet
{
public:
void onActivate() override;
void onRender(bool full) override;
void onButtonShortPress() override;
void onButtonLongPress() override;
void onExitShort() override;
void onExitLong() override;
void onNavUp() override;
void onNavDown() override;
void onNavLeft() override;
void onNavRight() override;
private:
std::string lastInput = "None";
bool isGrabbed = false;
void setGrabbed(bool grabbed);
};
} // namespace NicheGraphics::InkHUD
#endif
@@ -29,10 +29,10 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
// If we get a different type of status, something has gone weird elsewhere
assert(status->getStatusType() == STATUS_TYPE_POWER);
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status;
const meshtastic::PowerStatus *pwrStatus = (const meshtastic::PowerStatus *)status;
// Get the new state of charge %, and round to the nearest 10%
uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10;
uint8_t newSocRounded = ((pwrStatus->getBatteryChargePercent() + 5) / 10) * 10;
// If rounded value has changed, trigger a display update
// It's okay to requestUpdate before we store the new value, as the update won't run until next loop()
@@ -1176,7 +1176,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
items.push_back(MenuItem("Back", previousPage));
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
meshtastic_Channel &ch = channels.getByIndex(i);
const meshtastic_Channel &ch = channels.getByIndex(i);
if (!ch.has_settings)
continue;
@@ -1252,7 +1252,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
case NODE_CONFIG_CHANNEL_PRECISION: {
previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL;
items.push_back(MenuItem("Back", previousPage));
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
const meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) {
items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
break;
@@ -1524,7 +1524,15 @@ void InkHUD::MenuApplet::onButtonShortPress()
if (!settings->joystick.enabled) {
if (!cursorShown) {
cursorShown = true;
// Select the first item that isn't a header
cursor = 0;
while (cursor < items.size() && items.at(cursor).isHeader) {
cursor++;
}
if (cursor >= items.size()) {
cursorShown = false;
cursor = 0;
}
} else {
do {
cursor = (cursor + 1) % items.size();
@@ -1576,7 +1584,15 @@ void InkHUD::MenuApplet::onNavUp()
if (!cursorShown) {
cursorShown = true;
cursor = 0;
// Select the last item that isn't a header
cursor = items.size() - 1;
while (items.at(cursor).isHeader) {
if (cursor == 0) {
cursorShown = false;
break;
}
cursor--;
}
} else {
do {
if (cursor == 0)
@@ -1597,7 +1613,15 @@ void InkHUD::MenuApplet::onNavDown()
if (!cursorShown) {
cursorShown = true;
// Select the first item that isn't a header
cursor = 0;
while (cursor < items.size() && items.at(cursor).isHeader) {
cursor++;
}
if (cursor >= items.size()) {
cursorShown = false;
cursor = 0;
}
} else {
do {
cursor = (cursor + 1) % items.size();
@@ -1759,7 +1783,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
// Get the channel, and check if it's enabled
meshtastic_Channel &channel = channels.getByIndex(i);
const meshtastic_Channel &channel = channels.getByIndex(i);
if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED)
continue;
@@ -1829,7 +1853,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
items.push_back(MenuItem("Exit", MenuPage::EXIT));
}
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, std::string text)
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, const std::string &text)
{
setFont(fontSmall);
uint16_t wrapMaxH = 0;
@@ -55,7 +55,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height,
std::string text); // Draw input field for free text
const std::string &text); // Draw input field for free text
uint16_t getSystemInfoPanelHeight();
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
uint16_t *height = nullptr); // Info panel at top of root menu
@@ -228,17 +228,17 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) {
// Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently
bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
bool msgIsBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
// Pick source of message
MessageStore::Message *message =
isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
const MessageStore::Message *message =
msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
// Find info about the sender
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
// Leading tag (channel vs. DM)
text += isBroadcast ? "From:" : "DM: ";
text += msgIsBroadcast ? "From:" : "DM: ";
// Sender id
if (node && node->has_user)
@@ -252,7 +252,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
text.clear();
// Leading tag (channel vs. DM)
text += isBroadcast ? "Msg from " : "DM from ";
text += msgIsBroadcast ? "Msg from " : "DM from ";
// Sender id
if (node && node->has_user)
@@ -55,12 +55,12 @@ int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *sta
// We'll mimic that behavior, just to keep in line with the other Statuses,
// even though I'm not sure what the original reason for jumping through these extra hoops was.
assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH);
meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status;
const auto *btStatus = static_cast<const meshtastic::BluetoothStatus *>(status);
// When pairing begins
if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
if (btStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
// Store the passkey for rendering
passkey = bluetoothStatus->getPasskey();
passkey = btStatus->getPasskey();
// Show pairing screen
bringToForeground();
@@ -0,0 +1,111 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./FavoritesMapApplet.h"
#include "NodeDB.h"
using namespace NicheGraphics;
bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node)
{
// Keep our own node available as map anchor/center; all others must be favorited.
return node && (node->num == nodeDB->getNodeNum() || node->is_favorite);
}
void InkHUD::FavoritesMapApplet::onRender(bool full)
{
// Custom empty state text for favorites-only map.
if (!enoughMarkers()) {
printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Favorite node position", CENTER, MIDDLE);
printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE);
return;
}
// Draw the usual map applet first.
MapApplet::onRender(full);
// Draw our latest "node of interest" as a special marker.
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
if (node && node->is_favorite && nodeDB->hasValidPosition(node) && enoughMarkers())
drawLabeledMarker(node);
}
// Determine if we need to redraw the map, when we receive a new position packet.
ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshPacket &mp)
{
// If applet is not active, we shouldn't be handling any data.
if (!isActive())
return ProcessMessage::CONTINUE;
// Try decode a position from the packet.
bool hasPosition = false;
float lat;
float lng;
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) {
meshtastic_Position position = meshtastic_Position_init_default;
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) {
if (position.has_latitude_i && position.has_longitude_i // Actually has position
&& (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island"
{
hasPosition = true;
lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format
lng = position.longitude_i * 1e-7;
}
}
}
// Skip if we didn't get a valid position.
if (!hasPosition)
return ProcessMessage::CONTINUE;
const int8_t hopsAway = getHopsAway(mp);
const bool hasHopsAway = hopsAway >= 0;
// Determine if the position packet would change anything on-screen.
bool somethingChanged = false;
// If our own position.
if (isFromUs(&mp)) {
// Ignore tiny local movement to reduce update spam.
if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) {
somethingChanged = true;
ourLastLat = lat;
ourLastLng = lng;
}
} else {
// For non-local packets, this applet only reacts to favorited nodes.
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
if (!sender || !sender->is_favorite)
return ProcessMessage::CONTINUE;
// Check if this position is from someone different than our previous position packet.
if (mp.from != lastFrom) {
somethingChanged = true;
lastFrom = mp.from;
lastLat = lat;
lastLng = lng;
lastHopsAway = hopsAway;
}
// Same sender: check if position changed.
else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) {
somethingChanged = true;
lastLat = lat;
lastLng = lng;
}
// Same sender, same position: check if hops changed.
else if (hasHopsAway && (hopsAway != lastHopsAway)) {
somethingChanged = true;
lastHopsAway = hopsAway;
}
}
if (somethingChanged) {
requestAutoshow();
requestUpdate();
}
return ProcessMessage::CONTINUE;
}
#endif
@@ -0,0 +1,44 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Plots position of favorited nodes from DB, with North facing up.
Scaled to fit the most distant node.
Size of marker represents hops away.
The favorite node which most recently sent a position will be labeled.
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
#include "SinglePortModule.h"
namespace NicheGraphics::InkHUD
{
class FavoritesMapApplet : public MapApplet, public SinglePortModule
{
public:
FavoritesMapApplet() : SinglePortModule("FavoritesMapApplet", meshtastic_PortNum_POSITION_APP) {}
void onRender(bool full) override;
protected:
bool shouldDrawNode(meshtastic_NodeInfoLite *node) override;
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
NodeNum lastFrom = 0; // Sender of most recent favorited (non-local) position packet
float lastLat = 0.0;
float lastLng = 0.0;
float lastHopsAway = 0;
float ourLastLat = 0.0; // Info about most recent local position
float ourLastLng = 0.0;
};
} // namespace NicheGraphics::InkHUD
#endif
@@ -69,9 +69,10 @@ void InkHUD::HeardApplet::populateFromNodeDB()
}
// Sort the collection by age
std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool {
return (top->last_heard > bottom->last_heard);
});
std::sort(ordered.begin(), ordered.end(),
[](const meshtastic_NodeInfoLite *top, const meshtastic_NodeInfoLite *bottom) -> bool {
return (top->last_heard > bottom->last_heard);
});
// Keep the most recent entries only
// Just enough to fill the screen
@@ -69,7 +69,7 @@ void InkHUD::ThreadedMessageApplet::onRender(bool full)
while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) {
// Grab data for message
MessageStore::Message &m = store->messages.at(i);
const MessageStore::Message &m = store->messages.at(i);
bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
+61 -12
View File
@@ -59,10 +59,16 @@ void InkHUD::Events::onButtonShort()
if (consumer) {
consumer->onButtonShortPress();
} else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
if (!settings->joystick.enabled)
inkhud->nextApplet();
else
inkhud->openMenu();
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_SHORT))
userConsumer->onButtonShortPress();
else {
if (!settings->joystick.enabled)
inkhud->nextApplet();
else
inkhud->openMenu();
}
}
}
@@ -84,8 +90,14 @@ void InkHUD::Events::onButtonLong()
// If no system applet is handling input, default behavior instead is to open the menu
if (consumer)
consumer->onButtonLongPress();
else
inkhud->openMenu();
else {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_LONG))
userConsumer->onButtonLongPress();
else
inkhud->openMenu();
}
}
void InkHUD::Events::onExitShort()
@@ -110,8 +122,14 @@ void InkHUD::Events::onExitShort()
// If no system applet is handling input, default behavior instead is change tiles
if (consumer)
consumer->onExitShort();
else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module
inkhud->nextTile();
else if (!dismissedExt) { // Don't change tile if this button press silenced the external notification module
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT))
userConsumer->onExitShort();
else
inkhud->nextTile();
}
}
}
@@ -133,6 +151,13 @@ void InkHUD::Events::onExitLong()
if (consumer)
consumer->onExitLong();
else {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG))
userConsumer->onExitLong();
// Nothing uses exit long yet
}
}
}
@@ -157,6 +182,12 @@ void InkHUD::Events::onNavUp()
if (consumer)
consumer->onNavUp();
else if (!dismissedExt) {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP))
userConsumer->onNavUp();
}
}
}
@@ -181,6 +212,12 @@ void InkHUD::Events::onNavDown()
if (consumer)
consumer->onNavDown();
else if (!dismissedExt) {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN))
userConsumer->onNavDown();
}
}
}
@@ -206,8 +243,14 @@ void InkHUD::Events::onNavLeft()
// If no system applet is handling input, default behavior instead is to cycle applets
if (consumer)
consumer->onNavLeft();
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
inkhud->prevApplet();
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT))
userConsumer->onNavLeft();
else
inkhud->prevApplet();
}
}
}
@@ -233,8 +276,14 @@ void InkHUD::Events::onNavRight()
// If no system applet is handling input, default behavior instead is to cycle applets
if (consumer)
consumer->onNavRight();
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
inkhud->nextApplet();
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT))
userConsumer->onNavRight();
else
inkhud->nextApplet();
}
}
}
+6
View File
@@ -210,6 +210,12 @@ void InkHUD::InkHUD::prevApplet()
windowManager->prevApplet();
}
// Returns the currently active applet
InkHUD::Applet *InkHUD::InkHUD::getActiveApplet()
{
return windowManager->getActiveApplet();
}
// Show the menu (on the the focused tile)
// The applet previously displayed there will be restored once the menu closes
void InkHUD::InkHUD::openMenu()
+1
View File
@@ -74,6 +74,7 @@ class InkHUD
void nextApplet();
void prevApplet();
NicheGraphics::InkHUD::Applet *getActiveApplet();
void openMenu();
void openAlignStick();
void openKeyboard();
+15 -13
View File
@@ -12,7 +12,7 @@ using namespace NicheGraphics;
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
InkHUD::MessageStore::MessageStore(std::string label)
InkHUD::MessageStore::MessageStore(const std::string &label)
{
filename = "";
filename += "/NicheGraphics";
@@ -50,12 +50,13 @@ void InkHUD::MessageStore::saveToFlash()
// For each message
for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) {
Message &m = messages.at(i);
f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes
f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes
f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte
f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length
f.write('\0'); // Append null term
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
f.write(reinterpret_cast<const uint8_t *>(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes
f.write(reinterpret_cast<const uint8_t *>(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes
f.write(reinterpret_cast<const uint8_t *>(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte
f.write(reinterpret_cast<const uint8_t *>(m.text.c_str()), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text
f.write('\0'); // Append null term
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast<uint32_t>(i), min(MAX_MESSAGE_SIZE, m.text.size()),
m.text.c_str());
}
// Release firmware's SPI lock, because SafeFile::close needs it
@@ -111,17 +112,17 @@ void InkHUD::MessageStore::loadFromFlash()
// First byte: how many messages are in the flash store
uint8_t flashMessageCount = 0;
f.readBytes((char *)&flashMessageCount, 1);
LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount);
f.readBytes(reinterpret_cast<char *>(&flashMessageCount), 1);
LOG_DEBUG("Messages available: %u", static_cast<uint32_t>(flashMessageCount));
// For each message
for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) {
Message m;
// Read meta data (fixed width)
f.readBytes((char *)&m.timestamp, sizeof(m.timestamp));
f.readBytes((char *)&m.sender, sizeof(m.sender));
f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex));
f.readBytes(reinterpret_cast<char *>(&m.timestamp), sizeof(m.timestamp));
f.readBytes(reinterpret_cast<char *>(&m.sender), sizeof(m.sender));
f.readBytes(reinterpret_cast<char *>(&m.channelIndex), sizeof(m.channelIndex));
// Read characters until we find a null term
char c;
@@ -136,7 +137,8 @@ void InkHUD::MessageStore::loadFromFlash()
// Store in RAM
messages.push_back(m);
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str());
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", static_cast<uint32_t>(i), m.timestamp, m.sender,
m.text.c_str());
}
f.close();
+1 -1
View File
@@ -31,7 +31,7 @@ class MessageStore
};
MessageStore() = delete;
explicit MessageStore(std::string label); // Label determines filename in flash
explicit MessageStore(const std::string &label); // Label determines filename in flash
void saveToFlash();
void loadFromFlash();
@@ -8,5 +8,5 @@ build_flags =
-D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling
-D HAS_BUTTON=0 ; Suppress default ButtonThread
lib_deps =
# TODO renovate
https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX
# renovate: datasource=github-tags depName=GFX_Root packageName=ZinggJM/GFX_Root
https://github.com/ZinggJM/GFX_Root/archive/3195764e352a0d2567c8d277ac408ca7293a99b0.zip ; Used by InkHUD as a "slimmer" version of AdafruitGFX
+13 -13
View File
@@ -269,42 +269,42 @@ void InkHUD::Renderer::clearTile(Tile *t)
// Rotate the tile dimensions
int16_t left = 0;
int16_t top = 0;
uint16_t width = 0;
uint16_t height = 0;
uint16_t tileW = 0;
uint16_t tileH = 0;
switch (settings->rotation) {
case 0:
left = t->getLeft();
top = t->getTop();
width = t->getWidth();
height = t->getHeight();
tileW = t->getWidth();
tileH = t->getHeight();
break;
case 1:
left = driver->width - (t->getTop() + t->getHeight());
top = t->getLeft();
width = t->getHeight();
height = t->getWidth();
tileW = t->getHeight();
tileH = t->getWidth();
break;
case 2:
left = driver->width - (t->getLeft() + t->getWidth());
top = driver->height - (t->getTop() + t->getHeight());
width = t->getWidth();
height = t->getHeight();
tileW = t->getWidth();
tileH = t->getHeight();
break;
case 3:
left = t->getTop();
top = driver->height - (t->getLeft() + t->getWidth());
width = t->getHeight();
height = t->getWidth();
tileW = t->getHeight();
tileH = t->getWidth();
break;
}
// Calculate the bounds to clear
uint16_t xStart = (left < 0) ? 0 : left;
uint16_t yStart = (top < 0) ? 0 : top;
if (xStart >= driver->width || yStart >= driver->height || left + width < 0 || top + height < 0)
if (xStart >= driver->width || yStart >= driver->height || left + tileW < 0 || top + tileH < 0)
return; // the box is completely off the screen
uint16_t xEnd = left + width;
uint16_t yEnd = top + height;
uint16_t xEnd = left + tileW;
uint16_t yEnd = top + tileH;
if (xEnd > driver->width)
xEnd = driver->width;
if (yEnd > driver->height)
+7 -1
View File
@@ -273,6 +273,12 @@ void InkHUD::WindowManager::prevApplet()
inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
}
// Returns active applet
NicheGraphics::InkHUD::Applet *InkHUD::WindowManager::getActiveApplet()
{
return userTiles.at(settings->userTiles.focused)->getAssignedApplet();
}
// Rotate the display image by 90 degrees
void InkHUD::WindowManager::rotate()
{
@@ -396,7 +402,7 @@ void InkHUD::WindowManager::autoshow()
{
// Don't perform autoshow if a system applet has exclusive use of the display right now
// Note: lockRequests prevents autoshow attempting to hide menuApplet
for (SystemApplet *sa : inkhud->systemApplets) {
for (const SystemApplet *sa : inkhud->systemApplets) {
if (sa->lockRendering || sa->lockRequests)
return;
}
@@ -29,6 +29,7 @@ class WindowManager
void nextTile();
void prevTile();
Applet *getActiveApplet();
void openMenu();
void openAlignStick();
void openKeyboard();
@@ -146,7 +146,7 @@ void CannedMessageStore::handleGet(meshtastic_AdminMessage *response)
std::string merged;
if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0
merged.reserve(201);
for (std::string &s : messages) {
for (const std::string &s : messages) {
merged += s;
merged += '|';
}
+2 -1
View File
@@ -276,7 +276,8 @@ int32_t ButtonThread::runOnce()
case BUTTON_EVENT_LONG_RELEASED: {
LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime);
if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE &&
// Require press started after boot holdoff to avoid phantom shutdown from floating pins
if (millis() > 30000 && buttonPressStartTime > 30000 && _longLongPress != INPUT_BROKER_NONE &&
(millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) {
evt.inputEvent = _longLongPress;
this->notifyObservers(&evt);
+4 -5
View File
@@ -106,8 +106,8 @@ static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{},
{}};
HackadayCommunicatorKeyboard::HackadayCommunicatorKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
{
reset();
}
@@ -147,7 +147,6 @@ void HackadayCommunicatorKeyboard::pressed(uint8_t key)
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) {
@@ -187,8 +186,8 @@ void HackadayCommunicatorKeyboard::released()
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) {
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -18,8 +18,8 @@ class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase
private:
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
uint32_t last_modifier_time; // Timestamp of the last modifier key press
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+16 -1
View File
@@ -100,13 +100,28 @@ void InputBroker::processInputEventQueue()
int InputBroker::handleInputEvent(const InputEvent *event)
{
powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release
#if HAS_SCREEN
bool screenWasOff = false;
if (screen) {
screenWasOff = !screen->isScreenOn();
}
#endif
powerFSM.trigger(EVENT_INPUT);
if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule &&
moduleConfig.external_notification.enabled && externalNotificationModule->nagging()) {
externalNotificationModule->stopNow();
// If this turns off a notification, don't further process the event
return 0;
}
#if HAS_SCREEN
if (screen && screenWasOff) {
// If the screen was off, it is in the process of turning on, and we just drop the event
return 0;
}
#endif
this->notifyObservers(event);
return 0;
}
+3 -3
View File
@@ -91,7 +91,7 @@ MPR121Keyboard::MPR121Keyboard() : m_wire(nullptr), m_addr(0), readCallback(null
{
// LOG_DEBUG("MPR121 @ %02x", m_addr);
state = Init;
last_key = -1;
last_key = UINT8_MAX;
last_tap = 0L;
char_idx = 0;
queue = "";
@@ -359,8 +359,8 @@ void MPR121Keyboard::released()
return;
}
// would clear longpress callback... later.
if (last_key < 0 || last_key > _NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = -1;
if (last_key >= _NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = UINT8_MAX;
state = Idle;
return;
}
+1 -1
View File
@@ -14,7 +14,7 @@ class MPR121Keyboard
MPR121States state;
int8_t last_key;
uint8_t last_key;
uint32_t last_tap;
uint8_t char_idx;
+5 -6
View File
@@ -43,8 +43,8 @@ static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = {
};
TCA8418Keyboard::TCA8418Keyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(-1), next_key(-1), last_tap(0L), char_idx(0), tap_interval(0),
should_backspace(false)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(UINT8_MAX), next_key(UINT8_MAX), last_tap(0L), char_idx(0),
tap_interval(0), should_backspace(false)
{
}
@@ -63,7 +63,6 @@ void TCA8418Keyboard::pressed(uint8_t key)
if (state == Init || state == Busy) {
return;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
@@ -72,7 +71,7 @@ void TCA8418Keyboard::pressed(uint8_t key)
}
// Compute key index based on dynamic row/column
next_key = row * _TCA8418_COLS + col;
next_key = (uint8_t)(row * _TCA8418_COLS + col);
// LOG_DEBUG("TCA8418: Key %u -> Next Key %u", key, next_key);
@@ -106,8 +105,8 @@ void TCA8418Keyboard::released()
return;
}
if (last_key < 0 || last_key > _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -14,8 +14,8 @@ class TCA8418Keyboard : public TCA8418KeyboardBase
void pressed(uint8_t key) override;
void released(void) override;
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+4 -5
View File
@@ -62,8 +62,8 @@ static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = {
};
TDeckProKeyboard::TDeckProKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
{
}
@@ -101,7 +101,6 @@ void TDeckProKeyboard::pressed(uint8_t key)
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
@@ -142,8 +141,8 @@ void TDeckProKeyboard::released()
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) {
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -19,8 +19,8 @@ class TDeckProKeyboard : public TCA8418KeyboardBase
private:
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
uint32_t last_modifier_time; // Timestamp of the last modifier key press
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+4 -5
View File
@@ -65,8 +65,8 @@ static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'},
{' ', 0x00, Key::BL_TOGGLE}};
TLoraPagerKeyboard::TLoraPagerKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
{
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
@@ -129,7 +129,6 @@ void TLoraPagerKeyboard::pressed(uint8_t key)
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
@@ -170,8 +169,8 @@ void TLoraPagerKeyboard::released()
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) {
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -21,8 +21,8 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase
private:
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
uint32_t last_modifier_time; // Timestamp of the last modifier key press
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+53 -27
View File
@@ -9,6 +9,7 @@
#include "PowerMon.h"
#include "RadioLibInterface.h"
#include "ReliableRouter.h"
#include "TransmitHistory.h"
#include "airtime.h"
#include "buzz.h"
#include "power/PowerHAL.h"
@@ -58,6 +59,13 @@ NimbleBluetooth *nimbleBluetooth = nullptr;
NRF52Bluetooth *nrf52Bluetooth = nullptr;
#endif
#ifdef MESHTASTIC_ENABLE_APPROTECT
#include "security/APProtect.h"
#endif
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
#include "security/EncryptedStorage.h"
#endif
#if HAS_WIFI || defined(USE_WS5500)
#include "mesh/api/WiFiServerAPI.h"
#include "mesh/wifi/WiFiAPClient.h"
@@ -192,8 +200,6 @@ bool kb_found = false;
// global bool to record that on-screen keyboard (OSK) is present
bool osk_found = false;
unsigned long last_listen = 0;
// The I2C address of the RTC Module (if found)
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
// The I2C address of the Accelerometer (if found)
@@ -248,10 +254,7 @@ uint32_t timeLastPowered = 0;
static OSThread *powerFSMthread;
OSThread *ambientLightingThread;
RadioInterface *rIf = NULL;
#ifdef ARCH_PORTDUINO
RadioLibHal *RadioLibHAL = NULL;
#endif
/**
* Some platforms (nrf52) might provide an alterate version that suppresses calling delay from sleep.
@@ -368,6 +371,10 @@ void setup()
consoleInit(); // Set serial baud rate and init our mesh console
#endif
#ifdef MESHTASTIC_ENABLE_APPROTECT
enableAPProtect();
#endif
#ifdef UNPHONE
unphone.printStore();
#endif
@@ -393,8 +400,8 @@ void setup()
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
#ifndef SENSECAP_INDICATOR
// use PSRAM for malloc calls > 256 bytes
heap_caps_malloc_extmem_enable(256);
// use PSRAM for malloc calls > 2048 bytes
heap_caps_malloc_extmem_enable(2048);
#endif
#endif
@@ -465,6 +472,17 @@ void setup()
fsInit();
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
EncryptedStorage::initLocked();
if (!EncryptedStorage::isUnlocked()) {
if (!EncryptedStorage::isProvisioned()) {
LOG_WARN("TAK: Device not provisioned — connect and set a passphrase to unlock storage");
} else {
LOG_WARN("TAK: Device locked — connect and provide passphrase to unlock storage");
}
}
#endif
#if !MESHTASTIC_EXCLUDE_I2C
#if defined(I2C_SDA1) && defined(ARCH_RP2040)
Wire1.setSDA(I2C_SDA1);
@@ -708,6 +726,9 @@ void setup()
// We do this as early as possible because this loads preferences from flash
// but we need to do this after main cpu init (esp32setup), because we need the random seed set
nodeDB = new NodeDB;
// Initialize transmit history to persist broadcast throttle timers across reboots
TransmitHistory::getInstance()->loadFromDisk();
#if HAS_TFT
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
tftSetup();
@@ -725,17 +746,15 @@ void setup()
playStartMelody();
#if HAS_SCREEN
// fixed screen override?
if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO)
screen_model = config.display.oled;
// fixed screen override?
#if defined(USE_SH1107)
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128
screen_geometry = GEOMETRY_128_128;
#endif
#if defined(USE_SH1107_128_64)
#elif defined(USE_SH1107_128_64)
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64
#else
if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO)
screen_model = config.display.oled;
#endif
#endif
@@ -954,7 +973,7 @@ void setup()
#endif
#endif
initLoRa();
auto rIf = initLoRa();
lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation)
@@ -1003,12 +1022,12 @@ void setup()
if (!rIf)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO);
else {
router->addInterface(rIf);
// Log bit rate to debug output
LOG_DEBUG("LoRA bitrate = %f bytes / sec", (float(meshtastic_Constants_DATA_PAYLOAD_LEN) /
(float(rIf->getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN)))) *
1000);
router->addInterface(std::move(rIf));
}
// This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values
@@ -1125,10 +1144,19 @@ void loop()
#endif
power->powerCommandsCheck();
if (RadioLibInterface::instance != nullptr && !Throttle::isWithinTimespanMs(last_listen, 1000 * 60) &&
!(RadioLibInterface::instance->isSending() || RadioLibInterface::instance->isActivelyReceiving())) {
RadioLibInterface::instance->startReceive();
LOG_DEBUG("attempting AGC reset");
if (RadioLibInterface::instance != nullptr) {
static uint32_t lastRadioMissedIrqPoll;
if (!Throttle::isWithinTimespanMs(lastRadioMissedIrqPoll, 1000)) {
lastRadioMissedIrqPoll = millis();
RadioLibInterface::instance->pollMissedIrqs();
}
// Periodic AGC reset — warm sleep + recalibrate to prevent stuck AGC gain
static uint32_t lastAgcReset;
if (!Throttle::isWithinTimespanMs(lastAgcReset, AGC_RESET_INTERVAL_MS)) {
lastAgcReset = millis();
RadioLibInterface::instance->resetAGC();
}
}
#ifdef DEBUG_STACK
@@ -1150,10 +1178,7 @@ void loop()
}
if (portduino_status.LoRa_in_error && rebootAtMsec == 0) {
LOG_ERROR("LoRa in error detected, attempting to recover");
if (rIf != nullptr) {
delete rIf;
rIf = nullptr;
}
router->addInterface(nullptr);
if (portduino_config.lora_spi_dev == "ch341") {
if (ch341Hal != nullptr) {
delete ch341Hal;
@@ -1169,8 +1194,9 @@ void loop()
exit(EXIT_FAILURE);
}
}
if (initLoRa()) {
router->addInterface(rIf);
auto rIf = initLoRa();
if (rIf) {
router->addInterface(std::move(rIf));
portduino_status.LoRa_in_error = false;
} else {
LOG_WARN("Reconfigure failed, rebooting");
-1
View File
@@ -33,7 +33,6 @@ extern ScanI2C::DeviceAddress cardkb_found;
extern uint8_t kb_model;
extern bool kb_found;
extern bool osk_found;
extern unsigned long last_listen;
extern ScanI2C::DeviceAddress rtc_found;
extern ScanI2C::DeviceAddress accelerometer_found;
extern ScanI2C::FoundDevice rgb_found;
+3 -7
View File
@@ -22,6 +22,8 @@ const char *Channels::serialChannel = "serial";
const char *Channels::mqttChannel = "mqtt";
#endif
meshtastic_Channel dummyChannel = {.index = -1};
uint8_t xorHash(const uint8_t *p, size_t len)
{
uint8_t code = 0;
@@ -309,13 +311,7 @@ meshtastic_Channel &Channels::getByIndex(ChannelIndex chIndex)
return *ch;
} else {
LOG_ERROR("Invalid channel index %d > %d, malformed packet received?", chIndex, channelFile.channels_count);
static meshtastic_Channel *ch = (meshtastic_Channel *)malloc(sizeof(meshtastic_Channel));
memset(ch, 0, sizeof(meshtastic_Channel));
// ch.index -1 means we don't know the channel locally and need to look it up by settings.name
// not sure this is handled right everywhere
ch->index = -1;
return *ch;
return dummyChannel;
}
}
+5 -6
View File
@@ -1,6 +1,7 @@
#include "CryptoEngine.h"
// #include "NodeDB.h"
#include "architecture.h"
#include <memory>
#if !(MESHTASTIC_EXCLUDE_PKI)
#include "NodeDB.h"
@@ -169,10 +170,9 @@ void CryptoEngine::hash(uint8_t *bytes, size_t numBytes)
void CryptoEngine::aesSetKey(const uint8_t *key_bytes, size_t key_len)
{
delete aes;
aes = nullptr;
if (key_len != 0) {
aes = new AESSmall256();
aes = std::unique_ptr<AESSmall256>(new AESSmall256());
aes->setKey(key_bytes, key_len);
}
}
@@ -231,12 +231,11 @@ void CryptoEngine::decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes
// Generic implementation of AES-CTR encryption.
void CryptoEngine::encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes)
{
delete ctr;
ctr = nullptr;
std::unique_ptr<CTRCommon> ctr;
if (_key.length == 16)
ctr = new CTR<AES128>();
ctr = std::unique_ptr<CTRCommon>(new CTR<AES128>());
else
ctr = new CTR<AES256>();
ctr = std::unique_ptr<CTRCommon>(new CTR<AES256>());
ctr->setKey(_key.bytes, _key.length);
static uint8_t scratch[MAX_BLOCKSIZE];
memcpy(scratch, bytes, numBytes);
+2 -2
View File
@@ -5,6 +5,7 @@
#include "configuration.h"
#include "mesh-pb-constants.h"
#include <Arduino.h>
#include <memory>
extern concurrency::Lock *cryptLock;
@@ -48,7 +49,7 @@ class CryptoEngine
virtual void aesSetKey(const uint8_t *key, size_t key_len);
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
AESSmall256 *aes = NULL;
std::unique_ptr<AESSmall256> aes = nullptr;
#endif
@@ -77,7 +78,6 @@ class CryptoEngine
/** Our per packet nonce */
uint8_t nonce[16] = {0};
CryptoKey key = {};
CTRCommon *ctr = NULL;
#if !(MESHTASTIC_EXCLUDE_PKI)
uint8_t shared_key[32] = {0};
uint8_t private_key[32] = {0};
+33
View File
@@ -263,6 +263,7 @@ template <typename T> void LR11x0Interface<T>::startReceive()
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
enableInterrupt(isrRxLevel0);
checkRxDoneIrqFlag();
#endif
}
@@ -298,6 +299,38 @@ template <typename T> bool LR11x0Interface<T>::isActivelyReceiving()
RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED);
}
#ifdef LR11X0_AGC_RESET
template <typename T> void LR11x0Interface<T>::resetAGC()
{
// Safety: don't reset mid-packet
if (sendingPacket != NULL || (isReceiving && isActivelyReceiving()))
return;
LOG_DEBUG("LR11x0 AGC reset: warm sleep + Calibrate(0x3F)");
// 1. Warm sleep — powers down the analog frontend, resetting AGC state
lora.sleep(true, 0);
// 2. Wake to RC standby for stable calibration
lora.standby(RADIOLIB_LR11X0_STANDBY_RC, true);
// 3. Calibrate all blocks (PLL, ADC, image, RC oscillators)
// calibrate() is protected on LR11x0, so use raw SPI (same as internal implementation)
uint8_t calData = RADIOLIB_LR11X0_CALIBRATE_ALL;
module.SPIwriteStream(RADIOLIB_LR11X0_CMD_CALIBRATE, &calData, 1, true, true);
// 4. Re-calibrate image rejection for actual operating frequency
// Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions.
lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f);
// 5. Re-apply RX boosted gain mode
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
// 6. Resume receiving
startReceive();
}
#endif
template <typename T> bool LR11x0Interface<T>::sleep()
{
// \todo Display actual typename of the adapter, not just `LR11x0`
+4
View File
@@ -27,6 +27,10 @@ template <class T> class LR11x0Interface : public RadioLibInterface
bool isIRQPending() override { return lora.getIrqFlags() != 0; }
#ifdef LR11X0_AGC_RESET
void resetAGC() override;
#endif
protected:
/**
* Specific module instance
+12
View File
@@ -106,6 +106,18 @@ class MeshModule
/* We allow modules to ignore a request without sending an error if they have a specific reason for it. */
bool ignoreRequest = false;
/**
* Check if the current request is a multi-hop broadcast. Modules should call this in allocReply()
* and return NULL to prevent reply storms from broadcast requests that have already been relayed.
*/
bool isMultiHopBroadcastRequest()
{
if (currentRequest && isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) {
return true;
}
return false;
}
/** If a bound channel name is set, we will only accept received packets that come in on that channel.
* A special exception (FIXME, not sure if this is a good idea) - packets that arrive on the local interface
* are allowed on any channel (this lets the local user do anything).
+23
View File
@@ -184,6 +184,29 @@ bool MeshPacketQueue::replaceLowerPriorityPacket(meshtastic_MeshPacket *p)
}
}
if (backPacket->tx_after) {
// Check if there's a late packet at the queue end
auto now = millis();
if (backPacket->tx_after < now && (!p->tx_after || backPacket->tx_after > p->tx_after)) {
int32_t dt = (int32_t)(backPacket->tx_after - now);
if (p->tx_after) {
LOG_WARN("Dropping late packet 0x%08x with TX delay %dms to make room in the TX queue for packet 0x%08x with "
"TX delay %ums",
backPacket->id, dt, p->id, p->tx_after - now);
} else {
LOG_WARN("Dropping late packet 0x%08x with TX delay %dms to make room in the TX queue for packet 0x%08x "
"with no TX delay",
backPacket->id, dt, p->id);
}
queue.pop_back();
packetPool.release(backPacket);
// Insert the new packet in the correct order
enqueue(p);
return true;
}
}
// If the back packet's priority is not lower, no replacement occurs
return false;
}
+159 -12
View File
@@ -30,6 +30,10 @@
#include <power/PowerHAL.h>
#include <vector>
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
#include "security/EncryptedStorage.h"
#endif
#ifdef ARCH_ESP32
#if HAS_WIFI
#include "mesh/wifi/WiFiAPClient.h"
@@ -143,9 +147,8 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_
digitalWrite(rst, HIGH);
delay(10);
uint32_t ID = 0;
ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst);
ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice
readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst);
uint32_t ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice
return ID;
}
@@ -322,9 +325,9 @@ NodeDB::NodeDB()
// Uncomment below to always enable UDP broadcasts
// config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST;
// If we are setup to broadcast on the default channel, ensure that the telemetry intervals are coerced to the minimum value
// of 30 minutes or more
if (channels.isDefaultChannel(channels.getPrimaryIndex())) {
// If we are setup to broadcast on any default channel slot (with default frequency slot semantics),
// ensure that the telemetry intervals are coerced to the minimum value of 30 minutes or more.
if (channels.hasDefaultChannel()) {
LOG_DEBUG("Coerce telemetry to min of 30 minutes on defaults");
moduleConfig.telemetry.device_update_interval = Default::getConfiguredOrMinimumValue(
moduleConfig.telemetry.device_update_interval, min_default_telemetry_interval_secs);
@@ -646,6 +649,20 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
config.security.private_key.size = 0;
}
config.security.public_key.size = 0;
#ifdef HARDENED_DEFAULTS
config.security.serial_enabled = false; // Disable USB serial PhoneAPI — no auth, physical access only
config.security.debug_log_api_enabled = false; // No debug log leaks
config.security.admin_channel_enabled = false; // No legacy admin channel
config.security.is_managed = true; // Block all from=0 admin when unlocked;
// operator must use PKC admin key to configure.
// Passphrase delivery still works when locked
// (AdminModule special-cases !isUnlocked()).
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY;
// FIXED_PIN allows the operator to connect via BLE and provision the passphrase.
config.bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN;
#endif
#ifdef PIN_GPS_EN
config.position.gps_en_gpio = PIN_GPS_EN;
#endif
@@ -711,7 +728,9 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
#ifdef USERPREFS_FIXED_BLUETOOTH
config.bluetooth.fixed_pin = USERPREFS_FIXED_BLUETOOTH;
config.bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN;
#else
#elif !defined(HARDENED_DEFAULTS)
// Default: use RANDOM_PIN only if device has a screen (to display the PIN).
// set FIXED_PIN in the HARDENED_DEFAULTS block above.
config.bluetooth.mode = hasScreen ? meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN
: meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN;
#endif
@@ -814,26 +833,28 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.has_store_forward = true;
moduleConfig.has_telemetry = true;
moduleConfig.has_external_notification = true;
#if defined(PIN_BUZZER)
#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION)
moduleConfig.external_notification.enabled = true;
#endif
#if defined(PIN_BUZZER)
moduleConfig.external_notification.output_buzzer = PIN_BUZZER;
moduleConfig.external_notification.use_pwm = true;
moduleConfig.external_notification.alert_message_buzzer = true;
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
#endif
#if defined(PIN_VIBRATION)
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output_vibra = PIN_VIBRATION;
moduleConfig.external_notification.alert_message_vibra = true;
moduleConfig.external_notification.output_ms = 500;
moduleConfig.external_notification.nag_timeout = 2;
#endif
#if defined(LED_NOTIFICATION)
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output = LED_NOTIFICATION;
moduleConfig.external_notification.active = LED_STATE_ON;
moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000;
#endif
#if defined(PIN_VIBRATION)
moduleConfig.external_notification.nag_timeout = 2;
#elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION)
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
#endif
@@ -1147,6 +1168,37 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t
void *dest_struct)
{
LoadFileResult state = LoadFileResult::OTHER_FAILURE;
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
// check if the file is encrypted and decrypt before protobuf decode
if (EncryptedStorage::isEncrypted(filename)) {
uint8_t *decBuf = new uint8_t[protoSize];
if (!decBuf) {
LOG_ERROR("OOM decrypting %s", filename);
return LoadFileResult::OTHER_FAILURE;
}
size_t decLen = 0;
if (EncryptedStorage::readAndDecrypt(filename, decBuf, protoSize, decLen)) {
LOG_INFO("Load encrypted %s", filename);
pb_istream_t stream = pb_istream_from_buffer(decBuf, decLen);
if (fields != &meshtastic_NodeDatabase_msg)
memset(dest_struct, 0, objSize);
if (!pb_decode(&stream, fields, dest_struct)) {
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
state = LoadFileResult::DECODE_FAILED;
} else {
LOG_INFO("Loaded encrypted %s successfully", filename);
state = LoadFileResult::LOAD_SUCCESS;
}
} else {
LOG_ERROR("Decrypt failed for %s, treating as corrupt", filename);
state = LoadFileResult::DECODE_FAILED;
}
delete[] decBuf;
return state;
}
#endif
#ifdef FSCom
concurrency::LockGuard g(spiLock);
@@ -1223,6 +1275,21 @@ void NodeDB::loadFromDisk()
}
#endif
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
if (!EncryptedStorage::isUnlocked()) {
// Encrypted storage is locked — install defaults and wait for passphrase via BLE/serial.
// reloadFromDisk() will be called by AdminModule once the device is unlocked.
LOG_WARN("NodeDB: Encrypted storage locked, using default config until unlocked");
installDefaultNodeDatabase();
installDefaultDeviceState();
installDefaultConfig();
installDefaultModuleConfig();
installDefaultChannels();
return;
}
#endif
auto state = loadProto(nodeDatabaseFileName, getMaxNodesAllocatedSize(), sizeof(meshtastic_NodeDatabase),
&meshtastic_NodeDatabase_msg, &nodeDatabase);
if (nodeDatabase.version < DEVICESTATE_MIN_VER) {
@@ -1384,6 +1451,35 @@ void NodeDB::loadFromDisk()
LOG_INFO("Loaded UIConfig");
}
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
// Ensure all config segments are persisted to encrypted storage.
// installDefaultConfig/installDefaultModuleConfig only set in-memory structs
// without saving to disk, so we force a save here to ensure encrypted files exist.
{
const char *filesToCheck[] = {configFileName, moduleConfigFileName, channelFileName, deviceStateFileName,
nodeDatabaseFileName};
int segments[] = {SEGMENT_CONFIG, SEGMENT_MODULECONFIG, SEGMENT_CHANNELS, SEGMENT_DEVICESTATE, SEGMENT_NODEDATABASE};
int toSave = 0;
for (int i = 0; i < 5; i++) {
if (!EncryptedStorage::isEncrypted(filesToCheck[i])) {
toSave |= segments[i];
}
}
if (toSave) {
LOG_INFO("TAK: Saving unencrypted segments to encrypted storage (mask=0x%x)", toSave);
saveToDisk(toSave);
}
// Migrate any remaining plaintext proto files (from standard firmware upgrade)
for (const char *fn : filesToCheck) {
if (!EncryptedStorage::isEncrypted(fn)) {
LOG_INFO("Migrating %s to encrypted storage", fn);
EncryptedStorage::migrateFile(fn);
}
}
}
#endif
// 2.4.X - configuration migration to update new default intervals
if (moduleConfig.version < 23) {
LOG_DEBUG("ModuleConfig version %d is stale, upgrading to new default intervals", moduleConfig.version);
@@ -1419,8 +1515,21 @@ void NodeDB::loadFromDisk()
}
#endif
}
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
/**
* Re-run loadFromDisk() after encrypted storage is unlocked at runtime.
* Called by AdminModule after a successful provisionPassphrase / unlockWithPassphrase.
*/
void NodeDB::reloadFromDisk()
{
LOG_INFO("NodeDB: Reloading config from encrypted storage after unlock");
loadFromDisk();
}
#endif
/** Save a protobuf from a file, return true for success */
bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct,
bool fullAtomic)
@@ -1433,6 +1542,34 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
return false;
}
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
// Encrypt all files except uiconfig (no secrets) and the DEK file (self-encrypted)
if (strcmp(filename, uiconfigFileName) != 0) {
// First encode protobuf to a temporary buffer
uint8_t *pbBuf = new uint8_t[protoSize];
if (!pbBuf) {
LOG_ERROR("OOM encoding %s for encryption", filename);
return false;
}
pb_ostream_t stream = pb_ostream_from_buffer(pbBuf, protoSize);
if (!pb_encode(&stream, fields, dest_struct)) {
LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream));
delete[] pbBuf;
return false;
}
size_t encodedSize = stream.bytes_written;
bool ok = EncryptedStorage::encryptAndWrite(filename, pbBuf, encodedSize, fullAtomic);
delete[] pbBuf;
if (!ok) {
LOG_ERROR("EncryptedStorage: Failed to encrypt and write %s", filename);
}
return ok;
}
#endif
bool okay = false;
#ifdef FSCom
auto f = SafeFile(filename, fullAtomic);
@@ -1525,6 +1662,16 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
return false;
}
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
// When encrypted storage is locked, encryptAndWrite() returns false for every file.
// That would cause saveToDisk()'s nRF52 retry path to call FSCom.format(), wiping all
// encrypted proto files from flash. Return true here — "nothing to save, not an error."
if (!EncryptedStorage::isUnlocked()) {
LOG_WARN("NodeDB: saveToDisk skipped — encrypted storage locked");
return true;
}
#endif
bool success = true;
#ifdef FSCom
spiLock->lock();
+6
View File
@@ -307,6 +307,12 @@ class NodeDB
newStatus.notifyObservers(&status);
}
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
/// Re-run loadFromDisk() after the encrypted storage is unlocked at runtime.
/// Called by AdminModule after a successful provisionPassphrase / unlockWithPassphrase.
void reloadFromDisk();
#endif
private:
bool duplicateWarned = false;
bool localPositionUpdatedSinceBoot = false;
+2 -2
View File
@@ -439,7 +439,7 @@ void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, cons
}
// Getters and setters for hop limit fields packed in hop_limit
inline uint8_t PacketHistory::getHighestHopLimit(PacketRecord &r)
inline uint8_t PacketHistory::getHighestHopLimit(const PacketRecord &r)
{
return r.hop_limit & HOP_LIMIT_HIGHEST_MASK;
}
@@ -449,7 +449,7 @@ inline void PacketHistory::setHighestHopLimit(PacketRecord &r, uint8_t hopLimit)
r.hop_limit = (r.hop_limit & ~HOP_LIMIT_HIGHEST_MASK) | (hopLimit & HOP_LIMIT_HIGHEST_MASK);
}
inline uint8_t PacketHistory::getOurTxHopLimit(PacketRecord &r)
inline uint8_t PacketHistory::getOurTxHopLimit(const PacketRecord &r)
{
return (r.hop_limit & HOP_LIMIT_OUR_TX_MASK) >> HOP_LIMIT_OUR_TX_SHIFT;
}
+2 -2
View File
@@ -43,9 +43,9 @@ class PacketHistory
* @return true if node was indeed a relayer, false if not */
bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr);
uint8_t getHighestHopLimit(PacketRecord &r);
uint8_t getHighestHopLimit(const PacketRecord &r);
void setHighestHopLimit(PacketRecord &r, uint8_t hopLimit);
uint8_t getOurTxHopLimit(PacketRecord &r);
uint8_t getOurTxHopLimit(const PacketRecord &r);
void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit);
PacketHistory(const PacketHistory &); // non construction-copyable
+175 -14
View File
@@ -3,6 +3,9 @@
#include "GPS.h"
#endif
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
#include "security/EncryptedStorage.h"
#endif
#include "Channels.h"
#include "Default.h"
#include "FSCommon.h"
@@ -54,6 +57,16 @@ void PhoneAPI::handleStartConfig()
observe(&service->fromNumChanged);
#ifdef FSCom
observe(&xModem.packetReady);
#endif
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
// New physical connection: reset per-connection PKC auth.
// Do NOT reset here on re-requests (want_config_id sent again within the same
// connection after auth) — that would strip auth from a client who just unlocked
// and is re-fetching the full unredacted config.
isAdminAuthorized = false;
// MED-2: s_localAdminAuthorized (device-level passphrase auth) is intentionally
// NOT reset here. It persists until an explicit Lock Now (revokeAllAuth()).
// Resetting it on every new connection would cause an auth DoS.
#endif
}
@@ -127,6 +140,13 @@ void PhoneAPI::close()
config_state = 0;
pauseBluetoothLogging = false;
heartbeatReceived = false;
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
isAdminAuthorized = false;
// MED-2: do NOT reset s_localAdminAuthorized on disconnect.
// It represents device-level passphrase auth and must persist across connections
// until an explicit Lock Now (revokeAllAuth()). Resetting it here would revoke
// any passphrase-authenticated session the moment a second client connects.
#endif
}
}
@@ -155,6 +175,21 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
if (pb_decode_from_bytes(buf, bufLength, &meshtastic_ToRadio_msg, &toRadioScratch)) {
switch (toRadioScratch.which_payload_variant) {
case meshtastic_ToRadio_packet_tag:
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!isAdminAuthorized && !s_localAdminAuthorized) {
// Allow admin messages addressed to this device — passphrase delivery must get through.
// AdminModule handles its own is_managed gate for those.
// Block everything else — unauthorized clients cannot inject mesh traffic.
// Require the packet to carry a decoded (not encrypted) payload so portnum is valid.
bool isLocalAdmin = toRadioScratch.packet.which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
toRadioScratch.packet.decoded.portnum == meshtastic_PortNum_ADMIN_APP &&
toRadioScratch.packet.to == nodeDB->getNodeNum();
if (!isLocalAdmin) {
LOG_INFO("TAK: Dropping non-admin ToRadio packet from unauthorized client");
return false;
}
}
#endif
return handleToRadioPacket(toRadioScratch.packet);
case meshtastic_ToRadio_want_config_id_tag:
config_nonce = toRadioScratch.want_config_id;
@@ -166,6 +201,12 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
close();
break;
case meshtastic_ToRadio_xmodemPacket_tag:
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!isAdminAuthorized && !s_localAdminAuthorized) {
LOG_INFO("TAK: Dropping xmodem packet from unauthorized client");
break;
}
#endif
LOG_INFO("Got xmodem packet");
#ifdef FSCom
xModem.handlePacket(toRadioScratch.xmodemPacket);
@@ -286,8 +327,15 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
}
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
// If client only wants node info, jump directly to sending nodes
state = STATE_SEND_OTHER_NODEINFOS;
onNowHasData(0);
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!isAdminAuthorized && !s_localAdminAuthorized) {
state = STATE_SEND_COMPLETE_ID; // Unauthorized: skip node DB
} else
#endif
{
state = STATE_SEND_OTHER_NODEINFOS;
onNowHasData(0);
}
} else {
state = STATE_SEND_METADATA;
}
@@ -303,7 +351,17 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
case STATE_SEND_CHANNELS:
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_channel_tag;
fromRadioScratch.channel = channels.getByIndex(config_state);
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!isAdminAuthorized && !s_localAdminAuthorized) {
// Send channel structure but zero out PSK to prevent key extraction
fromRadioScratch.channel = channels.getByIndex(config_state);
memset(fromRadioScratch.channel.settings.psk.bytes, 0, sizeof(fromRadioScratch.channel.settings.psk.bytes));
fromRadioScratch.channel.settings.psk.size = 0;
} else
#endif
{
fromRadioScratch.channel = channels.getByIndex(config_state);
}
config_state++;
// Advance when we have sent all of our Channels
if (config_state >= MAX_NUM_CHANNELS) {
@@ -336,6 +394,13 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
LOG_DEBUG("Send config: network");
fromRadioScratch.config.which_payload_variant = meshtastic_Config_network_tag;
fromRadioScratch.config.payload_variant.network = config.network;
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!isAdminAuthorized && !s_localAdminAuthorized) {
// Redact wifi PSK
memset(fromRadioScratch.config.payload_variant.network.wifi_psk, 0,
sizeof(fromRadioScratch.config.payload_variant.network.wifi_psk));
}
#endif
break;
case meshtastic_Config_display_tag:
LOG_DEBUG("Send config: display");
@@ -356,6 +421,30 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
LOG_DEBUG("Send config: security");
fromRadioScratch.config.which_payload_variant = meshtastic_Config_security_tag;
fromRadioScratch.config.payload_variant.security = config.security;
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!isAdminAuthorized && !s_localAdminAuthorized) {
// Redact private key
memset(fromRadioScratch.config.payload_variant.security.private_key.bytes, 0,
sizeof(fromRadioScratch.config.payload_variant.security.private_key.bytes));
fromRadioScratch.config.payload_variant.security.private_key.size = 0;
// Redact admin keys
for (int i = 0; i < 3; i++) {
memset(fromRadioScratch.config.payload_variant.security.admin_key[i].bytes, 0,
sizeof(fromRadioScratch.config.payload_variant.security.admin_key[i].bytes));
fromRadioScratch.config.payload_variant.security.admin_key[i].size = 0;
}
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
// Signal provisioning state via admin_key[0] so the client knows
// whether to show "Set passphrase" vs "Enter passphrase":
// admin_key[0].size == 0 → not provisioned (first time setup)
// admin_key[0].size == 1, bytes[0] == 0x01 → provisioned, locked
if (EncryptedStorage::isProvisioned()) {
fromRadioScratch.config.payload_variant.security.admin_key[0].bytes[0] = 0x01;
fromRadioScratch.config.payload_variant.security.admin_key[0].size = 1;
}
#endif
}
#endif
break;
case meshtastic_Config_sessionkey_tag:
LOG_DEBUG("Send config: sessionkey");
@@ -454,6 +543,12 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
config_state++;
// Advance when we have sent all of our ModuleConfig objects
if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) {
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!isAdminAuthorized && !s_localAdminAuthorized) {
// Unauthorized client: skip node DB and file manifest — only send config complete
state = STATE_SEND_COMPLETE_ID;
} else
#endif
// Handle special nonce behaviors:
// - SPECIAL_NONCE_ONLY_CONFIG: Skip node info, go directly to file manifest
// - SPECIAL_NONCE_ONLY_NODES: After sending nodes, skip to complete
@@ -544,24 +639,46 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
fromRadioScratch.queueStatus = *queueStatusPacketForPhone;
releaseQueueStatusPhonePacket();
} else if (mqttClientProxyMessageForPhone) {
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_mqttClientProxyMessage_tag;
fromRadioScratch.mqttClientProxyMessage = *mqttClientProxyMessageForPhone;
releaseMqttClientProxyPhonePacket();
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!isAdminAuthorized && !s_localAdminAuthorized) {
releaseMqttClientProxyPhonePacket(); // Discard — unauthorized client
} else
#endif
{
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_mqttClientProxyMessage_tag;
fromRadioScratch.mqttClientProxyMessage = *mqttClientProxyMessageForPhone;
releaseMqttClientProxyPhonePacket();
}
} else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) {
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_xmodemPacket_tag;
fromRadioScratch.xmodemPacket = xmodemPacketForPhone;
xmodemPacketForPhone = meshtastic_XModem_init_zero;
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!isAdminAuthorized && !s_localAdminAuthorized) {
xmodemPacketForPhone = meshtastic_XModem_init_zero; // Discard — unauthorized client
} else
#endif
{
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_xmodemPacket_tag;
fromRadioScratch.xmodemPacket = xmodemPacketForPhone;
xmodemPacketForPhone = meshtastic_XModem_init_zero;
}
} else if (clientNotification) {
// Always deliver clientNotification — required for TAK_UNLOCKED/TAK_LOCKED to reach client
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_clientNotification_tag;
fromRadioScratch.clientNotification = *clientNotification;
releaseClientNotification();
} else if (packetForPhone) {
printPacket("phone downloaded packet", packetForPhone);
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!isAdminAuthorized && !s_localAdminAuthorized) {
releasePhonePacket(); // Discard mesh traffic — unauthorized client
} else
#endif
{
printPacket("phone downloaded packet", packetForPhone);
// Encapsulate as a FromRadio packet
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
fromRadioScratch.packet = *packetForPhone;
releasePhonePacket();
// Encapsulate as a FromRadio packet
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
fromRadioScratch.packet = *packetForPhone;
releasePhonePacket();
}
}
break;
@@ -604,6 +721,28 @@ void PhoneAPI::sendConfigComplete()
service->api_state = service->STATE_ETH;
}
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
// Immediately after config_complete_id, notify the client if it hasn't authenticated.
// Gate on client auth state (not storage lock state) — storage may be auto-unlocked via
// boot token, but each new BLE/USB connection must still present the passphrase.
if (!isAdminAuthorized && !s_localAdminAuthorized) {
char msg[48];
if (!EncryptedStorage::isProvisioned()) {
strncpy(msg, "TAK_NEEDS_PROVISION", sizeof(msg));
} else {
// Provisioned: client must authenticate. Use lock reason if storage is hard-locked,
// otherwise signal that passphrase auth is required for this connection.
if (!EncryptedStorage::isUnlocked()) {
snprintf(msg, sizeof(msg), "TAK_LOCKED:%s", EncryptedStorage::getLockReason());
} else {
strncpy(msg, "TAK_LOCKED:needs_auth", sizeof(msg));
}
}
sendNotification(meshtastic_LogRecord_Level_WARNING, 0, msg);
LOG_INFO("PhoneAPI: sent %s to client", msg);
}
#endif
// Allow subclasses to know we've entered steady-state so they can lower power consumption
onConfigComplete();
@@ -842,3 +981,25 @@ int PhoneAPI::onNotify(uint32_t newValue)
return timeout ? -1 : 0; // If we timed out, MeshService should stop iterating through observers as we just removed one
}
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
// HIGH-3: static atomic so BLE-task writes and main-loop reads are race-free.
std::atomic<bool> PhoneAPI::s_localAdminAuthorized{false};
void PhoneAPI::authorizeLocalAdmin()
{
s_localAdminAuthorized.store(true);
LOG_INFO("TAK: Local admin authorized via PKC");
}
bool PhoneAPI::isLocalAdminAuthorized()
{
return s_localAdminAuthorized.load();
}
void PhoneAPI::revokeAllAuth()
{
s_localAdminAuthorized.store(false);
LOG_INFO("TAK: All connection auth revoked (Lock Now)");
}
#endif
+21
View File
@@ -4,6 +4,7 @@
#include "concurrency/Lock.h"
#include "mesh-pb-constants.h"
#include "meshtastic/portnums.pb.h"
#include <atomic>
#include <deque>
#include <iterator>
#include <string>
@@ -138,6 +139,19 @@ class PhoneAPI
bool isConnected() { return state != STATE_SEND_NOTHING; }
bool isSendingPackets() { return state == STATE_SEND_PACKETS; }
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
/// Set by AdminModule after successful PKC admin key validation.
/// When true, config dumps include full unredacted secrets.
void setAdminAuthorized(bool authorized) { isAdminAuthorized = authorized; }
bool getAdminAuthorized() const { return isAdminAuthorized; }
/// Static: authorize all local PhoneAPI instances (called by AdminModule)
static void authorizeLocalAdmin();
static bool isLocalAdminAuthorized();
/// Static: immediately revoke all connection-level auth (called on Lock Now)
static void revokeAllAuth();
#endif
protected:
/// Our fromradio packet while it is being assembled
meshtastic_FromRadio fromRadioScratch = {};
@@ -179,6 +193,13 @@ class PhoneAPI
APIType api_type = TYPE_NONE;
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
// HIGH-3: both flags are accessed from multiple tasks (main loop + NimBLE BLE callback
// task), so they must be atomic to prevent data races on read-modify-write sequences.
std::atomic<bool> isAdminAuthorized{false};
static std::atomic<bool> s_localAdminAuthorized;
#endif
private:
void releasePhonePacket();
+1
View File
@@ -301,6 +301,7 @@ void RF95Interface::startReceive()
// Must be done AFTER, starting receive, because startReceive clears (possibly stale) interrupt pending register bits
enableInterrupt(isrRxLevel0);
checkRxDoneIrqFlag();
}
bool RF95Interface::isChannelActive()

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