Compare commits

...
Author SHA1 Message Date
Ben Meadors 40ef34aa23 Add experimental OLED emulator vuejs component 2026-02-13 20:43:24 -06:00
Ben Meadors c8ff02cc35 Merge branch 'master' into develop 2026-02-13 18:09:37 -06:00
Ben MeadorsandWessel d770ef27c7 Update src/mesh/NodeDB.cpp
Co-authored-by: Wessel <wessel@weebl.me>
2026-02-13 17:36:12 -06:00
Ben MeadorsandWessel 71edf2fb48 Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp
Co-authored-by: Wessel <wessel@weebl.me>
2026-02-13 17:35:13 -06:00
Ben MeadorsandWessel c79e1925d9 Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp
Co-authored-by: Wessel <wessel@weebl.me>
2026-02-13 17:35:03 -06:00
Ben MeadorsandWessel 3d649d518f Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp
Co-authored-by: Wessel <wessel@weebl.me>
2026-02-13 17:34:53 -06:00
Ben MeadorsandWessel 947f8176dc Update src/detect/ScanI2C.cpp
Co-authored-by: Wessel <wessel@weebl.me>
2026-02-13 17:34:31 -06:00
github-actions[bot]andvidplace7 d5bde83ff5 Upgrade trunk (#9631)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-13 12:29:43 -06:00
TomandChristian Walther c3321771ef Xiao NRF - define suitable i2c pins for the sub-variants (#8866)
Co-authored-by: Christian Walther <cwalther@gmx.ch>
2026-02-12 20:20:54 +01:00
Ben Meadors b6e5534a8c Merge branch 'master' into develop 2026-02-12 13:11:48 -06:00
github-actions[bot] f10d786d82 Update protobufs (#9621) 2026-02-12 19:15:08 +01:00
Wessel 1a7f560372 fix: zero entire public key array instead of only first byte (#9619) 2026-02-12 15:33:57 +01:00
Ben Meadors 38c7ad0ed6 Exclude status message module 2026-02-12 07:11:12 -06:00
renovate[bot] bcc24705a5 chore(deps): update neopixel to v1.15.4 (#9616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-12 06:10:24 -06:00
renovate[bot] 8137200cb8 chore(deps): update sensirion core to v0.7.3 (#9613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-12 05:59:08 -06:00
Mike Robbins bfbce2e314 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) 2026-02-12 05:52:24 -06:00
renovate[bot] f86291546d chore(deps): update adafruit mpu6050 to v2.2.9 (#9611) 2026-02-12 11:20:44 +01:00
github-actions[bot] 640dd5f7d7 Upgrade trunk (#9615) 2026-02-12 11:15:38 +01:00
+1 15297cb56f 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>
2026-02-11 14:13:34 -06:00
oscgonferandBen Meadors 6d299eac67 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>
2026-02-11 13:11:11 -06:00
Jonathan Bennett 31fe15bb73 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
2026-02-11 12:02:22 -06:00
HarukiToredaandJason P 97983d8014 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>
2026-02-11 09:01:26 -06:00
Ben Meadors c366e53326 Merge branch 'master' into develop 2026-02-11 07:06:43 -06:00
Quency-D 0adcadb1ce Modify the dependency library of v4-tft (#9507) 2026-02-11 07:06:15 -06:00
github-actions[bot]andthebentern 21d5a34fec Update protobufs (#9605)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-02-11 07:05:35 -06:00
Quency-D 648148af8a Modify the dependency library of v4-tft (#9507) 2026-02-11 06:50:04 -06:00
Thomas Göttgens 5b03b1fe52 fix some random compiler warnings (#9596) 2026-02-11 06:45:26 -06:00
Thomas Göttgens f96a8593f2 fix some random compiler warnings (#9596) 2026-02-11 06:43:05 -06:00
github-actions[bot]andthebentern 4954723f97 Automated version bumps (#9604)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-02-11 06:30:05 -06:00
Chloe Bethel e047397642 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.
2026-02-11 06:29:55 -06:00
Jonathan Bennett 75f3d123f3 Add sdl libs for native builds (#9595)
* Add sdl libs for native builds

* Alpine try again
2026-02-11 06:29:16 -06:00
github-actions[bot]andvidplace7 6b73ac3850 Upgrade trunk (#9599)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-11 06:24:46 -06:00
Chloe Bethel 44941b79cd 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.
2026-02-11 06:22:01 -06:00
github-actions[bot]andthebentern 65adfa894f Automated version bumps (#9604)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-02-11 06:21:34 -06:00
github-actions[bot]andvidplace7 6df0449408 Upgrade trunk (#9581)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-10 06:30:50 -06:00
Tom a092f6bb22 Refactor logging in ProtobufModule to ensure message details are logged after successful decoding (#9536) 2026-02-09 19:56:48 -06:00
Jason P 927a4e47b8 Update built-in documentation for current method of implementation (#9592) 2026-02-09 18:56:22 -06:00
Jonathan Bennett df0ee74b30 Add battery curve for T-Beam 1 watt (#9585) 2026-02-09 17:33:06 -06:00
Ben Meadors af18f061cf Merge remote-tracking branch 'origin/master' into develop 2026-02-09 13:53:19 -06:00
github-actions[bot]andthebentern 139e45dff8 Update protobufs (#9588)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-02-09 13:52:01 -06:00
Jonathan Bennett 0df8719fc8 Temporarily un-renovate libch341 (#9587) 2026-02-09 13:30:27 -06:00
Ben Meadors b12cac3e00 Merge branch 'master' into develop 2026-02-09 12:52:22 -06:00
renovate[bot] 28c4acf5c4 chore(deps): update gxepd2 to v1.6.7 (#9577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 08:54:35 -06:00
579a0af516 feat: Add SCD4X (#7198)
* 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

* 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

* Add check on polling interval of sen5x

* Add missing serializer

---------

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>
2026-02-09 08:39:12 -06:00
Eric SesterhennandBen Meadors 73adeee385 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>
2026-02-08 19:48:18 -06:00
Nick bb3d6d5326 Fix embedded null byte truncation in ATAK strings (#9570) 2026-02-08 11:20:15 -06:00
7cbab4838c Feat/add sen5x (#7245)
* 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

* 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

* Update library dependencies in platformio.ini

* Fix unitialized variables in SEN5X constructor

* Fix missing import

* Cleanup of SEN5X class

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

* Fix I2C clock change logic

* Fix trunk

* Fix on condition in reclock

* Add check on polling interval of sen5x

---------

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>
2026-02-08 07:08:09 -06:00
Jonathan Bennett 4ce554e09d Make LED_POWER blip even in critical battery (#9545) 2026-02-08 06:49:30 -06:00
Jonathan Bennett eb145f8adc Add support for CW2015 LiPo battery fuel gauge (#9564)
* Add support for CW2015 LiPo battery fuel gauge

* Address Copilot's concerns, minor fixups
2026-02-07 22:30:14 -06:00
Jason P a60e7cfe62 Add Slash Key to VirtualKeyboard (#9563)
Addition of ? and / to the virtual Keyboard via short and long press
2026-02-07 20:43:26 -05:00
53231ae4b1 Added toggable config and default for larger screens to enable / hide bubbles on chat messages (#9560)
* Added toggable config and default for largeer screens to enable / hide bubbles on chat messages

* Refactor message bubble rendering logic for improved layout and consistency

* Move osk_found initialization for trackball/encoder devices before module setup to fix missing keyboard for L1

* Utilize current checks for consistency

* Reverted last changes

---------

Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-02-07 15:41:31 -06:00
Austin 39139cc2ea RPM: Include meshtasticd-start.sh (#9561) 2026-02-07 11:13:01 -05:00
github-actions[bot]andthebentern 5280caf9d8 Update protobufs (#9559)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-02-07 06:24:56 -06:00
Colby DillionandBen Meadors ba016fd91a Fix hop_limit upgrade detection (#9550)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-07 06:19:25 -06:00
renovate[bot] e2cf401ad3 Update meshtastic/device-ui digest to 6c75195 (#9553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-07 06:16:30 -06:00
Austin 4a4b1f4a87 meshtasticd: Fix install on Fedora 43 (#9556) 2026-02-06 19:36:21 -06:00
Colby DillionandBen Meadors 779e446d14 Fix hop_limit upgrade detection (#9550)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-06 13:00:08 -06:00
github-actions[bot]andvidplace7 f73d18384d Upgrade trunk (#9547)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-06 06:22:34 -06:00
github-actions[bot]andvidplace7 11bb2ee84e Upgrade trunk (#9368)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-05 05:53:48 -06:00
Jonathan Bennett 2361776992 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
2026-02-05 05:41:00 -06:00
renovate[bot]andBen Meadors a324c4af10 Update meshtastic-esp8266-oled-ssd1306 digest to 21e484f (#9533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-04 19:30:12 -06:00
renovate[bot] 5df5ab2790 Update Adafruit MPU6050 to v2.2.8 (#9534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 17:19:11 -06:00
renovate[bot] 74ea6206d9 Update NeoPixel to v1.15.3 (#9530)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 17:13:44 -06:00
renovate[bot] b238744445 Update Adafruit MPU6050 to v2.2.7 (#9525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 17:12:01 -06:00
Eric SesterhennandBen Meadors 94b7149958 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>
2026-02-04 17:11:44 -06:00
Jonathan BennettandCopilot ac611c4b62 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>
2026-02-04 14:47:44 -06:00
MaxandBen Meadors 89df5ef669 Undefine LED_BUILTIN (#9531)
Keep variant in sync with 
https://github.com/meshtastic/firmware/commit/df40085

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-04 11:15:49 -06:00
Ben Meadors be5f0a9ade Implement UDP multicast handler start/stop to ensure proper lifecycle (#9524)
* Implement UDP multicast handler start/stop to ensure proper lifecycle

* Add close method to AsyncUDP and improve UDP multicast handler lifecycle management

* Guard portduino
2026-02-04 11:15:38 -06:00
Jason P bfc3eebd54 HotFix for ReplyBot - Modules.cpp included and moved configuration.h (#9532) 2026-02-04 08:56:50 -06:00
538a5f0dfc 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>
2026-02-03 20:45:02 -06:00
Vortetty b7db22055d 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.
2026-02-03 20:02:54 -05:00
Jason P b008c7a170 Fix config.display.use_long_node_name not saving (#9522) 2026-02-03 12:21:51 -06:00
Eric Sesterhenn c8a9cdc148 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.
2026-02-03 06:23:49 -06:00
Eric Sesterhenn 0703e0e6d7 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.
2026-02-03 06:22:33 -06:00
Jonathan BennettandCopilot 644fa5b54e Power off control pin on Thinknode m5 during deepsleep and add RTC (#9510)
* Power off control pin on Thinknode m5 during deepsleep

* Apply suggestion from @Copilot

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

* Finish HAS_RTC cleanup

* Add RTC for Thinknode M5

* Don't double-init Wire

* Specify the RTC chip directly rather than use SensorRtcHelper.
Saves a bit of flash, and avoid mis-detection

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-03 06:07:33 -06:00
Jonathan Bennett f514bc230b Prefer EXT_PWR_DETECT pin over chargingVolt to detect power unplugged (#9511) 2026-02-03 00:13:49 -06:00
Jason P 0022148323 Missed in reviews - fixing send bubble (#9505) 2026-02-01 19:10:00 -06:00
Jonathan Bennett 9d06c1bf34 Add StatusMessage module and config overrides (#9351)
* Add StatusMessage module and config overrides

* Trunk

* Don't reboot node simply for a StatusMessage config update
2026-01-31 14:55:51 -06:00
Jonathan Bennett 1d30342c00 Don't ever define PIN_LED or BLE_LED_INVERTED (#9494)
* Don't ever define PIN_LED

* Deprecate BLE_LED_INVERTED
2026-01-31 12:15:06 -06:00
Ben Meadors e9d4485bb5 Merge remote-tracking branch 'origin/develop' 2026-01-30 13:56:11 -06:00
Jonathan BennettandCopilot 7b03980e0a Refuse to send legacy DMs simply because the remote public key is unknown (#9485)
* Refuse to send legacy DMs simply because the remote public key is unknown

* Update src/mesh/Router.cpp - use more specific error

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

* Update src/mesh/Router.cpp - more detail in warning message

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-30 13:46:24 -06:00
Ben Meadors 004179c045 Fix OTA filename determination to use unified format for ESP32 (#9488) 2026-01-30 13:46:11 -06:00
Jonathan Bennett 8f630bfcf3 Fix typo in LED state comment
And removed unused define
2026-01-30 12:52:17 -06:00
7bbfe99fbe Add on-screen keyboard to InkHUD (#9445)
* Added keyboard option to menu. Shows a keyboard layout but does not type.

* Keyboard types into text box and wraps.

* send FreeText messages from the send submenu

- renamed `KEYBOARD` action to `FREE_TEXT` and moved its menu location
to the send submenu
- opening the FreeText applet from the menu keeps the menu open and
disabled the timeout
- the FreeText applet writes to inkhud->freetext
- the sending a canned message checks inkhud->freetext and if it isn't
empty, sends and clears the inkhud->freetext

* Text scrolls along with input

* handle free text message completion as an event

implements `handleFreeText` and `OnFreeText()` for system applets to
interface with the FreeText Applet

The FreeText Applet generates an `OnFreeText` event when completing a
message which is handled by the first system applet with the
`handleFreeText` flag set to true.

The Menu Applet now handles this event.

* call `onFreeText` whenever the FreeText Applet exits

allows the menu to consistently restart its auto-close timeout

* Add text cursor

* Change UI to remove the header and make text box longer
Keyboard displays captial letters for legibility
Keyboard types captial letters with long press

* center FreeText keys and draw symbolic buttons

Move input field and keyboard drawing to their own functions:
- `drawInputField()`
- `drawKeyboard()`

Store the keys in a 1-dimensional array

Implement a matching array, `keyWidths`, to set key widths relative to
the font size

* Add character limit and counter

* Fix softlock when hitting character limit

* Move text box as its own menu page

* rework FreeTextApplet into KeyboardApplet

- The Keyboard Applet renders an on-screen keyboard at the lower portion
of the screen.
- Calling `inkhud->openKeyboard()` sends all the user applets to the
background and resizes the first system applet with `handleFreeText` set
to True to fit above the on-screen keyboard
- `inkhud->closeKeyboard()` reverses this layout change

* Fix input box rendering and add character limit to menu free text

* remove FREE_TEXT menu page and use the FREE_TEXT menu action solely

* force update when changing the free text message

* reorganize KeyboardApplet

- add comments after each row of `key[]` and `keyWidths[]` to preserve
formatting

- The selected key is now set using the key index directly

- rowWidths are pre-calculated in the KeyboardApplet constructor

- removed `drawKeyboard()` and implemented `drawKeyLabel()`

* implement `Renderer::clearTile()` to clear the region below a tile

* add parameter to forceUpdate() for re-rendering the full screen

setting the `all` parameter to true in `inkhud->forceUpdate()` now
causes the full screen buffer to clear an re-render. This is helpful for
when sending applets to the background and the UI needs a clean canvas.

System Applets can now set the `alwaysRender` flag true which causes it
to re-render on every screen update. This is set to true in the Battery
Icon Applet.

* clean up tile clearing loops

* implement dirty rendering to let applets draw over their previous render

- `Applet::requestUpdate()` now has an optional flag to keep the old
canvas

- If honored, the renderer calls `render(true)` which runs
`onDirtyRender()` instead of `onRender()` for said applet

- The renderer will not call a dirty render if the full screen is
getting re-rendered

* simplify arithmetic in clearTile for better understanding

* combine Applet::onRender() and Applet::onDirtyRender() into Applet::onRender(bool full)

- add new `full` parameter to onRender() in every applet. This parameter
can be ignored by most applets.

- `Applet::requestUpdate()` has an optional flag that requests a full
render by default

* implement tile and partial rendering in KeyboardApplet

* add comment for drawKeyLabel()

* improve clarity of byte operations in clearTile()

* remove typo and commented code

* fix inaccurate comments

* add null check to openKeyboard() and closeKeyboard()

---------

Co-authored-by: zeropt <ferr0fluidmann@gmail.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-01-30 13:35:10 -05:00
caae6bc597 Change canned message recipient's previous page to send page (#9227)
* Change canned message recipient's previous page to send page

* Set previousPage for new menu pages
Set nextPage in back MenuPages to previousPage
Removed back MenuAction

---------

Co-authored-by: zeropt <ferr0fluidmann@gmail.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-01-30 13:31:26 -05:00
Jonathan Bennett 200e79e800 You get an RTC, and you get an RTC! (delete HAS_RTC as it wasn't actually doing much) (#9493) 2026-01-30 11:54:49 -06:00
Ben Meadors c19fc62683 Merge pull request #9492 from meshtastic/master
Master to develop
2026-01-30 10:46:11 -06:00
Jonathan Bennett 4cf01e7e53 Adjust pin poweroff for Thinknode M6 2026-01-30 10:05:26 -06:00
Jonathan Bennett ad4b1d9c2b re-enable RTC support on THINKNODE M3 and M6 2026-01-30 09:50:47 -06:00
Ben Meadors 68733a6c51 Fix issue triage workflow by clarifying device log requirements and improving JSON response handling 2026-01-30 06:32:08 -06:00
renovate[bot] 22617076f8 Update meshtastic/device-ui digest to 63967a4 (#9475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 20:19:12 -06:00
renovate[bot] 6f5a7672b4 Update pschatzmann_arduino-audio-driver to v0.2.1 (#9398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 20:18:34 -06:00
Ben Meadors e08c050720 Add custom ringtone definition for RAK4631 and enable buzzer pin (#9481) 2026-01-29 20:15:08 -06:00
Ben Meadors 28b4f37a93 Revert "Update libch341-spi-userspace digest to af9bc27 (#9472)" (#9483)
This reverts commit b18742c211.
2026-01-29 20:14:50 -06:00
Jonathan Bennett 5dd06edd00 Add ledOff if not defined 2026-01-29 13:24:10 -06:00
Jonathan Bennett eeb7373043 Remove errant symbol 2026-01-29 13:23:49 -06:00
Jonathan Bennett dbded86dcb More variant.h cleanup. LED_NOTIFICATION, remove dead code, etc (#9477) 2026-01-29 12:51:48 -06:00
Jonathan Bennett 45fbc0f9d3 Remove stale variant.h defines (#9470)
* Remove noop CANNED_MESSAGE_MODULE_ENABLE define

* Remove over-eager warning removal

* Remove unused LED_CONN

* Dead defines removal

* Rename oddball LED pin name

* Rename second oddball LED pin name

* Remove another dead define
2026-01-29 10:58:06 -06:00
61b39acc7d Add initial Nix shell (#8530)
* Add initial Nix shell

* Update flake.nix

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

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 10:06:58 -06:00
JorropoandBen Meadors 8af9e7fbdc enable long interleaving mode for LR11x0 and SX128x (#9399)
Using long interleaving is not a breaking change, the receiver node is able
to use the lora header to know if LI encoding is used or not and will
decode LI packets correctly.

However the problem is SX127x and other first generation LoRa IP which do not
support LI at all, for theses it is a breaking change.

HOWEVER due to the sync word bug the LR11x0 already can't talk with SX127x,
so if we enable LI on theses no one would be able to tell.
Same for SX128x altho this is because it works on 2.4Ghz which is incompatible with SX127x.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-29 06:44:17 -06:00
Andrew YongandBen Meadors 1f7ed6888a feat(stm32): Add Milesight GS301 Bathroom Odor Detector (#9359)
- STM32WLE5CCU6
- NFC (unsupported): NXP NT3H2211W0FTTJ (NTAG I2C plus: NFC Forum T2T with I2C interface, password protection and energy harvesting)
- Sensor (unsupported): Analog ADuCM355 (SHTC3 is connected to ADuCM355 and not directly accessible)
- Bicolor LED
- User button (presently not functional in STM32 variants)

The definitions for sensor voltage control are present but commented out to save power, due to lack of sensor support.

Powered by 4x 4000mAh RAMWAY ER18505 Li-SOCl2 batteries.

Flashing:

1. Power down device (remove batteries)
2. Connect USB-UART to J1 (USART2), pinout is below, do not connect +3V3 pin yet
3. Short BOOT pins next to J1
4. Connect +3V3 pin or insert batteries while BOOT pins are shorted
5. Use STM32CubeProgrammer, connect by UART mode
6. Load firmware .hex and download

J1 (USART2); Molex Picoblade (P=1.25mm * 4)

1. +3V3
2. PA3_USART2_RX_J1
3. PA2_USART2_TX_J1
4. GND

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-29 06:43:48 -06:00
treysisandBen Meadors 31bf51b3f2 Add support for the hardware buttons on Bluetooth Nugget device (#9468)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-29 06:38:49 -06:00
Justin E. MannandBen Meadors 334a4f04cd Fix logic for rak12035 sensor default config and improve messaging (#9414)
* better logic to check if the RAK12035 soil sensor is calibrated, better log messaging if either of the default values were used.

* .

* changes to how default calibration is done and a message it the default calibration is used pointing to the actual calibration sketch so the user can find it and use it to improve accuracy.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-29 06:29:15 -06:00
renovate[bot] b18742c211 Update libch341-spi-userspace digest to af9bc27 (#9472)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 06:25:07 -06:00
Ben Meadors 03084f6d3b PRs with needs-review still should get bot labeled 2026-01-29 06:23:52 -06:00
Ben Meadors 94d7b71aa8 Merge branch 'develop' 2026-01-29 06:12:11 -06:00
Ben Meadors 415686dd06 Trunk 2026-01-29 05:56:19 -06:00
Quency-DandBen Meadors b2f2f6b305 Add a watchdog module to meshsolar. (#9337)
* add watchdog module

* Restore the code in power.h

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-28 20:41:27 -06:00
Jonathan Bennett df400850c1 Undefine LED_BUILTIN where needed 2026-01-28 18:56:57 -06:00
Jonathan Bennett 6ab2f02dbc re-add unintentionally removed include 2026-01-28 17:53:12 -06:00
oscgonferandBen Meadors d7d6fe7f0f Avoid short-circuit evaluation issues in Telemetry (#9467)
* Make sensors in telemetry non-definitory

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-28 17:18:42 -06:00
Ben Meadors d44ceb6eb2 Fix NimBLE deinit null check 2026-01-28 17:17:35 -06:00
Jonathan Bennett 4fd0a8276b Just set LED_BUILTIN universally to -1, as we don't use it. (#8830)
* Just set LED_BUILTIN universally to -1, as we don't use it.

* LUD_BUILTIN workarounds

* Squash the LED_BUILTINs that sneaked in

* Don't kill valid pin derfine
2026-01-28 17:09:13 -06:00
1d219a93ab Move input init to an init function in InputBroker (#9463)
* Move input init to an init function in iInputBroker

* Unbreak targets with EXCLUDE_INPUTBROKER

* Update src/input/InputBroker.cpp

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

* Fix conditional compilation for input broker

* Apply suggestions from code review

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

* Trunk

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 14:58:05 -06:00
Eric SeveranceandBen Meadors f710cd6ecb Support fully direct request/responses (#9455)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-28 14:14:01 -06:00
Jonathan BennettandCopilot 571c1ac34c Initial serialModule cleanup (#9465)
* Initial serialModule cleanup

* Move SERIAL_PRINT_PORT definition to variant.h

* Add missed c6 check

* Update src/modules/SerialModule.cpp

Compile error for invalid SERIAL_PRINT_OBJECT value

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 14:08:32 -06:00
Ben Meadors fb635987d1 Merge remote-tracking branch 'origin/master' into develop 2026-01-28 13:31:39 -06:00
Jason P a922751afc External Notification - handleReceived Rewrite (#9454)
* First steps in consolidating code and minimizing rewrite

* Continuing code cleanup

* Merge containsBell and !isMuted to a single code path

* Forgot about alert_message_buzzer in the cleanup

* More code refinements and cleanup

* Fix nagCycleCutoff

* CoPilot Updates
2026-01-28 11:12:02 -06:00
renovate[bot] c1e3f56324 Update LovyanGFX to v1.2.19 (#9405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 11:08:07 -06:00
Ben MeadorsandCopilot d0562e1ee6 Add model workflows (#9462)
* Add GitHub workflows for issue completeness, duplicate detection, onboarding, and contribution quality checks

* Fix indentation

* Refactor GitHub workflows for issue handling

* Consolidate to two triage workflows

* Update .github/workflows/models_pr_triage.yml

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 10:50:56 -06:00
452 changed files with 13465 additions and 2682 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
RUN apt-get update && apt-get install --no-install-recommends -y \
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev && \
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev libsdl2-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir -U \
platformio==6.1.16 \
+1
View File
@@ -0,0 +1 @@
use nix
+213
View File
@@ -0,0 +1,213 @@
name: Issue Triage (Models)
on:
issues:
types: [opened]
permissions:
issues: write
models: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
triage:
if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }}
runs-on: ubuntu-latest
steps:
# ─────────────────────────────────────────────────────────────────────────
# Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam
# ─────────────────────────────────────────────────────────────────────────
- name: Detect spam or low-quality content
uses: actions/ai-inference@v2
id: quality
continue-on-error: true
with:
max-tokens: 20
prompt: |
Is this GitHub issue spam, AI-generated slop, or low quality?
Title: ${{ github.event.issue.title }}
Body: ${{ github.event.issue.body }}
Respond with exactly one of: spam, ai-generated, needs-review, ok
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
model: openai/gpt-4o-mini
- name: Apply quality label if needed
if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
uses: actions/github-script@v8
env:
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
with:
script: |
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
const labelMeta = {
'spam': { color: 'd73a4a', description: 'Possible spam' },
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
};
const meta = labelMeta[label];
if (!meta) return;
// Ensure label exists
try {
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
} catch (e) {
if (e.status !== 404) throw e;
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
}
// Apply label
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
// Set output to skip remaining steps
core.setOutput('is_spam', 'true');
# ─────────────────────────────────────────────────────────────────────────
# Step 2: Duplicate detection - only if not spam
# ─────────────────────────────────────────────────────────────────────────
- name: Detect duplicate issues
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# ─────────────────────────────────────────────────────────────────────────
# Step 3: Completeness check + auto-labeling (combined into one AI call)
# ─────────────────────────────────────────────────────────────────────────
- name: Determine if completeness check should be skipped
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
uses: actions/github-script@v8
id: check-skip
with:
script: |
const title = (context.payload.issue.title || '').toLowerCase();
const labels = (context.payload.issue.labels || []).map(label => label.name);
const hasFeatureRequest = title.includes('feature request');
const hasEnhancement = labels.includes('enhancement');
const shouldSkip = hasFeatureRequest && hasEnhancement;
core.setOutput('should_skip', shouldSkip ? 'true' : 'false');
- name: Analyze issue completeness and determine labels
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true'
uses: actions/ai-inference@v2
id: analysis
continue-on-error: true
with:
prompt: |
Analyze this GitHub issue for completeness and determine if it needs labels.
IMPORTANT: Distinguish between:
- Device/firmware bugs (crashes, reboots, lockups, radio/GPS/display/power issues) - these need device logs
- Build/release/packaging issues (missing files, CI failures, download problems) - these do NOT need device logs
- Documentation or website issues - these do NOT need device logs
If this is a device/firmware bug, request device logs and explain how to get them:
Web Flasher logs:
- Go to https://flasher.meshtastic.org
- Connect the device via USB and click Connect
- Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs
Meshtastic CLI logs:
- Run: meshtastic --port <serial-port> --noproto
- Reproduce the problem, then copy/paste the terminal output
Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual.
Respond ONLY with valid JSON (no markdown, no code fences):
{"complete": true, "comment": "", "label": "none"}
OR
{"complete": false, "comment": "Your helpful comment", "label": "needs-logs"}
Use "needs-logs" ONLY if this is a device/firmware bug AND no logs are attached.
Use "needs-info" if basic info like firmware version or steps to reproduce are missing.
Use "none" if the issue is complete, is a feature request, or is a build/CI/packaging issue.
Title: ${{ github.event.issue.title }}
Body: ${{ github.event.issue.body }}
system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. Only request device logs for actual device/firmware bugs, not for build/release/CI issues.
model: openai/gpt-4o-mini
- name: Process analysis result
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != ''
uses: actions/github-script@v8
id: process
env:
AI_RESPONSE: ${{ steps.analysis.outputs.response }}
with:
script: |
let raw = (process.env.AI_RESPONSE || '').trim();
// Strip markdown code fences if present
raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
let complete = true;
let comment = '';
let label = 'none';
try {
const parsed = JSON.parse(raw);
complete = !!parsed.complete;
comment = (parsed.comment ?? '').toString().trim();
label = (parsed.label ?? 'none').toString().trim().toLowerCase();
} catch {
// If JSON parse fails, log warning and don't comment (avoid posting raw JSON)
console.log('Failed to parse AI response as JSON:', raw);
complete = true;
comment = '';
label = 'none';
}
// Validate label
const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']);
if (!allowedLabels.has(label)) label = 'none';
// Only comment if we have a valid parsed comment (not raw JSON)
const shouldComment = !complete && comment.length > 0 && !comment.startsWith('{');
core.setOutput('should_comment', shouldComment ? 'true' : 'false');
core.setOutput('comment_body', comment);
core.setOutput('label', label);
- name: Apply triage label
if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none'
uses: actions/github-script@v8
env:
LABEL_NAME: ${{ steps.process.outputs.label }}
with:
script: |
const label = process.env.LABEL_NAME;
const labelMeta = {
'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' },
'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' },
};
const meta = labelMeta[label];
if (!meta) return;
// Ensure label exists
try {
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
} catch (e) {
if (e.status !== 404) throw e;
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
}
// Apply label
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
- name: Comment on issue
if: steps.process.outputs.should_comment == 'true'
uses: actions/github-script@v8
env:
COMMENT_BODY: ${{ steps.process.outputs.comment_body }}
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body: process.env.COMMENT_BODY
});
+139
View File
@@ -0,0 +1,139 @@
name: PR Triage (Models)
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
issues: write
models: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
triage:
if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }}
runs-on: ubuntu-latest
steps:
# ─────────────────────────────────────────────────────────────────────────
# Step 1: Check if PR already has automation/type labels (skip if so)
# ─────────────────────────────────────────────────────────────────────────
- name: Check existing labels
uses: actions/github-script@v8
id: check-labels
with:
script: |
const skipLabels = new Set(['automation']);
const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']);
const prLabels = context.payload.pull_request.labels.map(l => l.name);
const shouldSkipAll = prLabels.some(l => skipLabels.has(l));
const hasTypeLabel = prLabels.some(l => typeLabels.has(l));
core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false');
core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false');
# ─────────────────────────────────────────────────────────────────────────
# Step 2: Quality check (spam/AI-slop detection)
# ─────────────────────────────────────────────────────────────────────────
- name: Detect spam or low-quality content
if: steps.check-labels.outputs.skip_all != 'true'
uses: actions/ai-inference@v2
id: quality
continue-on-error: true
with:
max-tokens: 20
prompt: |
Is this GitHub pull request spam, AI-generated slop, or low quality?
Title: ${{ github.event.pull_request.title }}
Body: ${{ github.event.pull_request.body }}
Respond with exactly one of: spam, ai-generated, needs-review, ok
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
model: openai/gpt-4o-mini
- name: Apply quality label if needed
if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
uses: actions/github-script@v8
id: quality-label
env:
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
with:
script: |
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
const labelMeta = {
'spam': { color: 'd73a4a', description: 'Possible spam' },
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
};
const meta = labelMeta[label];
if (!meta) return;
// Ensure label exists
try {
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
} catch (e) {
if (e.status !== 404) throw e;
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
}
// Apply label
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
core.setOutput('is_spam', 'true');
# ─────────────────────────────────────────────────────────────────────────
# Step 3: Auto-label PR type (bugfix/hardware-support/enhancement)
# Only skip for spam/ai-generated; still classify needs-review PRs
# ─────────────────────────────────────────────────────────────────────────
- name: Classify PR for labeling
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.quality.outputs.response != 'spam' && steps.quality.outputs.response != 'ai-generated'
uses: actions/ai-inference@v2
id: classify
continue-on-error: true
with:
max-tokens: 30
prompt: |
Classify this pull request into exactly one category.
Return exactly one of: bugfix, hardware-support, enhancement
Use bugfix if it fixes a bug, crash, or incorrect behavior.
Use hardware-support if it adds or improves support for a specific hardware device/variant.
Use enhancement if it adds a new feature, improves performance, or refactors code.
Title: ${{ github.event.pull_request.title }}
Body: ${{ github.event.pull_request.body }}
system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label.
model: openai/gpt-4o-mini
- name: Apply type label
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != ''
uses: actions/github-script@v8
env:
TYPE_LABEL: ${{ steps.classify.outputs.response }}
with:
script: |
const label = (process.env.TYPE_LABEL || '').trim().toLowerCase();
const labelMeta = {
'bugfix': { color: 'd73a4a', description: 'Bug fix' },
'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' },
'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' },
};
const meta = labelMeta[label];
if (!meta) return;
// Ensure label exists
try {
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
} catch (e) {
if (e.status !== 404) throw e;
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
}
// Apply label
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
+3
View File
@@ -50,3 +50,6 @@ idf_component.yml
CMakeLists.txt
/sdkconfig.*
.dummy/*
# PYTHONPATH used by the Nix shell
.python3
+7 -7
View File
@@ -8,18 +8,18 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.497
- renovate@42.84.2
- prettier@3.8.0
- trufflehog@3.92.5
- checkov@3.2.501
- renovate@43.10.3
- prettier@3.8.1
- trufflehog@3.93.3
- yamllint@1.38.0
- bandit@1.9.3
- trivy@0.68.2
- trivy@0.69.1
- taplo@0.10.0
- ruff@0.14.13
- ruff@0.15.1
- isort@7.0.0
- markdownlint@0.47.0
- oxipng@10.0.0
- oxipng@10.1.0
- svgo@4.0.0
- actionlint@1.7.10
- flake8@7.3.0
+2 -2
View File
@@ -14,7 +14,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
curl wget g++ zip 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 \
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 -U platformio \
&& mkdir /tmp/firmware
@@ -53,7 +53,7 @@ USER root
RUN apt-get update && apt-get --no-install-recommends -y install \
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
liborcania2.3 libulfius2.7t64 libssl3t64 \
libx11-6 libinput10 libxkbcommon-x11-0 \
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/lib/meshtasticd \
&& mkdir -p /etc/meshtasticd/config.d \
+2 -2
View File
@@ -11,7 +11,7 @@ RUN apk --no-cache add \
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
libgpiod-dev yaml-cpp-dev bluez-dev \
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev \
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
&& rm -rf /var/cache/apk/* \
&& pip install --no-cache-dir -U platformio \
&& mkdir /tmp/firmware
@@ -42,7 +42,7 @@ USER root
RUN apk --no-cache add \
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
i2c-tools libuv libx11 libinput libxkbcommon \
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
&& rm -rf /var/cache/apk/* \
&& mkdir -p /var/lib/meshtasticd \
&& mkdir -p /etc/meshtasticd/config.d \
+2 -10
View File
@@ -156,16 +156,8 @@ IF %BPS_RESET% EQU 1 (
SET "PROGNAME=!FILENAME:.factory.bin=!"
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
IF "__!MCU!__" == "__esp32s3__" (
@REM We are working with ESP32-S3
SET "OTA_FILENAME=bleota-s3.bin"
) ELSE IF "__!MCU!__" == "__esp32c3__" (
@REM We are working with ESP32-C3
SET "OTA_FILENAME=bleota-c3.bin"
) ELSE (
@REM Everything else
SET "OTA_FILENAME=bleota.bin"
)
@REM Determine OTA filename based on MCU type (unified OTA format)
SET "OTA_FILENAME=mt-!MCU!-ota.bin"
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
@REM Set SPIFFS filename with "littlefs-" prefix.
+2 -8
View File
@@ -131,14 +131,8 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
exit 1
fi
# Determine OTA filename based on MCU type
if [ "$MCU" == "esp32s3" ]; then
OTAFILE=bleota-s3.bin
elif [ "$MCU" == "esp32c3" ]; then
OTAFILE=bleota-c3.bin
else
OTAFILE=bleota.bin
fi
# Determine OTA filename based on MCU type (unified OTA format)
OTAFILE="mt-${MCU}-ota.bin"
# Set SPIFFS filename with "littlefs-" prefix.
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
@@ -87,6 +87,9 @@
</screenshots>
<releases>
<release version="2.7.20" date="2026-02-11">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20</url>
</release>
<release version="2.7.19" date="2026-01-22">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19</url>
</release>
+2 -1
View File
@@ -32,7 +32,8 @@
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Minesemi ME25LS01",
+2 -1
View File
@@ -33,7 +33,8 @@
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "MeshLink",
+2 -1
View File
@@ -31,7 +31,8 @@
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Minimesh Lite",
+2 -1
View File
@@ -32,7 +32,8 @@
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "MINEWSEMI_MS24SF1_SX1262",
+2 -1
View File
@@ -32,7 +32,8 @@
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "BQ nRF52840",
+2 -1
View File
@@ -33,7 +33,8 @@
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "ProMicro compatible nRF52840",
+2 -1
View File
@@ -33,7 +33,8 @@
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Seeed T1000-E",
+2 -1
View File
@@ -25,7 +25,8 @@
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino", "freertos"],
"name": "Seeed WIO WM1110",
+2 -1
View File
@@ -32,7 +32,8 @@
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Seeed WIO WM1110",
+2 -1
View File
@@ -32,7 +32,8 @@
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Seeed WIO WM1110",
+6
View File
@@ -1,3 +1,9 @@
meshtasticd (2.7.20.0) unstable; urgency=medium
* Version 2.7.20
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 11 Feb 2026 12:19:54 +0000
meshtasticd (2.7.19.0) unstable; urgency=medium
* Version 2.7.19
+2 -1
View File
@@ -26,7 +26,8 @@ Build-Depends: debhelper-compat (= 13),
libx11-dev,
libinput-dev,
libxkbcommon-x11-dev,
libsqlite3-dev
libsqlite3-dev,
libsdl2-dev
Standards-Version: 4.6.2
Homepage: https://github.com/meshtastic/firmware
Rules-Requires-Root: no
Generated
+44
View File
@@ -0,0 +1,44 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1766314097,
"narHash": "sha256-laJftWbghBehazn/zxVJ8NdENVgjccsWAdAqKXhErrM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "306ea70f9eb0fb4e040f8540e2deab32ed7e2055",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+66
View File
@@ -0,0 +1,66 @@
{
description = "Nix flake to compile Meshtastic firmware";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
# Shim to make flake.nix work with stable Nix.
flake-compat = {
url = "github:NixOS/flake-compat";
flake = false;
};
};
outputs =
inputs:
let
lib = inputs.nixpkgs.lib;
forAllSystems =
fn:
lib.genAttrs lib.systems.flakeExposed (
system:
fn {
pkgs = import inputs.nixpkgs {
inherit system;
};
inherit system;
}
);
in
{
devShells = forAllSystems (
{ pkgs, ... }:
let
python3 = pkgs.python312.withPackages (
ps: with ps; [
google
]
);
in
{
default = pkgs.mkShell {
buildInputs = with pkgs; [
python3
platformio
];
shellHook = ''
# Set up PlatformIO to use a local core directory.
export PLATFORMIO_CORE_DIR=$PWD/.platformio
# Tell pip to put packages into $PIP_PREFIX instead of the usual
# location. This is especially necessary under NixOS to avoid having
# pip trying to write to the read-only Nix store. For more info,
# see https://wiki.nixos.org/wiki/Python
export PIP_PREFIX=$PWD/.python3
export PYTHONPATH="$PIP_PREFIX/${python3.sitePackages}"
export PATH="$PIP_PREFIX/bin:$PATH"
# Avoids reproducibility issues with some Python packages
# See https://nixos.org/manual/nixpkgs/stable/#python-setup.py-bdist_wheel-cannot-create-.whl
unset SOURCE_DATE_EPOCH
'';
};
}
);
};
}
+9 -1
View File
@@ -49,6 +49,7 @@ BuildRequires: pkgconfig(libulfius)
BuildRequires: pkgconfig(x11)
BuildRequires: pkgconfig(libinput)
BuildRequires: pkgconfig(xkbcommon-x11)
BuildRequires: pkgconfig(sdl2)
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
@@ -59,8 +60,14 @@ BuildRequires: pkgconfig(libbsd-overlay)
Requires: systemd-udev
# Declare that this package provides the user/group it creates in %pre
# Required for Fedora 43+ which tracks users/groups as RPM dependencies
Provides: user(%{meshtasticd_user})
Provides: group(%{meshtasticd_user})
Provides: group(spi)
%description
Meshtastic daemon for controlling Meshtastic devices. Meshtastic is an off-grid
Meshtastic daemon. Meshtastic is an off-grid
text communication platform that uses inexpensive LoRa radios.
%prep
@@ -151,6 +158,7 @@ fi
%license LICENSE
%doc README.md
%{_bindir}/meshtasticd
%{_bindir}/meshtasticd-start.sh
%dir %{_localstatedir}/lib/meshtasticd
%{_udevrulesdir}/99-meshtasticd-udev.rules
%dir %{_sysconfdir}/meshtasticd
+30
View File
@@ -0,0 +1,30 @@
# Dependencies
node_modules/
# Build output
dist/
# Vite cache
.vite/
# TypeScript cache
*.tsbuildinfo
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
# Local env files
.env
.env.local
.env.*.local
+149
View File
@@ -0,0 +1,149 @@
# Meshtastic OLED Emulator
A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays used in Meshtastic devices. This allows firmware UI development and testing directly in the browser.
## Features
- **Same API as firmware**: Uses the same `OLEDDisplay` API as esp8266-oled-ssd1306 library
- **Multiple display sizes**: 128x64, 128x32, 64x48, 128x128
- **Realistic rendering**: Pixel glow, OLED color tints (blue, white, yellow/blue dual)
- **XBM image support**: Render firmware icons and images
- **Font rendering**: Bitmap font support with text alignment
- **Vue 3 component**: Easy integration with Vue applications
## Installation
```bash
npm install @meshtastic/oled-emulator
```
## Quick Start
```vue
<script setup lang="ts">
import { ref, onMounted } from "vue";
import {
OLEDDisplay,
OLEDEmulator,
ArialMT_Plain_10,
} from "@meshtastic/oled-emulator";
// Create display matching firmware
const display = ref(new OLEDDisplay("128x64"));
onMounted(() => {
display.value.setFont(ArialMT_Plain_10);
display.value.clear();
display.value.drawString(10, 10, "Hello Mesh!");
display.value.drawRect(5, 5, 118, 54);
display.value.fillCircle(64, 32, 15);
});
</script>
<template>
<OLEDEmulator
:display="display"
:pixel-scale="4"
preset="ssd1306-blue"
:enable-glow="true"
/>
</template>
```
## OLEDDisplay API
The `OLEDDisplay` class provides the same methods as the firmware's display library:
```typescript
const display = new OLEDDisplay("128x64");
// Basic drawing
display.clear();
display.setPixel(x, y);
display.drawLine(x0, y0, x1, y1);
display.drawRect(x, y, width, height);
display.fillRect(x, y, width, height);
display.drawCircle(x, y, radius);
display.fillCircle(x, y, radius);
// Text
display.setFont(ArialMT_Plain_10);
display.setTextAlignment("TEXT_ALIGN_CENTER");
display.drawString(x, y, "Hello");
display.drawStringMaxWidth(x, y, maxWidth, "Long text...");
const width = display.getStringWidth("text");
// Images (XBM format)
display.drawXbm(x, y, width, height, imageData);
// Colors
display.setColor("WHITE"); // or 'BLACK', 'INVERSE'
// Buffer access (for custom rendering)
const buffer = display.getBuffer(); // Uint8Array
```
## Display Presets
The emulator includes several realistic display presets:
| Preset | Description |
| --------------------- | ------------------------------- |
| `ssd1306-blue` | Classic blue OLED (default) |
| `ssd1306-white` | White OLED |
| `ssd1306-yellow-blue` | Dual-color (top 16 rows yellow) |
| `sh1106` | SH1106 style |
| `sh1107` | SH1107 128x128 |
| `ssd1306-64x48` | Small 64x48 display |
| `ssd1306-128x32` | Wide 128x32 display |
## Screen Renderers
Pre-built screen renderers matching firmware UI:
```typescript
import { ScreenRenderers } from "@meshtastic/oled-emulator";
// Boot screen
ScreenRenderers.drawBootScreen(display, "2.5.0");
// Node info screen
ScreenRenderers.drawNodeInfoScreen(display, {
shortName: "MESH",
longName: "My Node",
nodeId: "!abcd1234",
lastHeard: "2m ago",
snr: 12.5,
});
// Message screen
ScreenRenderers.drawMessageScreen(display, {
from: "Alice",
text: "Hello from the mesh!",
time: "14:32",
channel: "LongFast",
});
// And more: drawGPSScreen, drawNodeListScreen, drawSystemScreen, drawCompassScreen
```
## Development
```bash
# Install dependencies
npm install
# Run development server
npm run dev
# Build library
npm run build
```
## Future: Protocol Bridge
A future enhancement will allow connecting this emulator to a real Meshtastic firmware instance (running on native/portduino), enabling live framebuffer streaming over WebSocket for real-time display mirroring.
## License
GPL-3.0 - Same as Meshtastic firmware
+22
View File
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Meshtastic OLED Emulator</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #121212;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+2211
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
{
"name": "@meshtastic/oled-emulator",
"version": "0.1.0",
"description": "Vue.js OLED display emulator for Meshtastic firmware UI development",
"keywords": [
"meshtastic",
"oled",
"ssd1306",
"sh1106",
"display",
"emulator",
"vue",
"vue3"
],
"author": "Meshtastic",
"license": "GPL-3.0",
"repository": {
"type": "git",
"url": "https://github.com/meshtastic/firmware.git",
"directory": "oled-emulator"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./style.css": "./dist/style.css"
},
"files": [
"dist",
"src"
],
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"type-check": "vue-tsc --noEmit"
},
"peerDependencies": {
"vue": "^3.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vite-plugin-dts": "^3.7.0",
"vue": "^3.4.0",
"vue-tsc": "^2.0.0"
}
}
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
/**
* DisplayPresets.ts - OLED display presets for emulator
*/
import type { OLEDDISPLAY_GEOMETRY } from "./OLEDDisplay";
// Display type presets with realistic OLED colors
export interface DisplayPreset {
name: string;
geometry: OLEDDISPLAY_GEOMETRY;
pixelOn: { r: number; g: number; b: number };
pixelOff: { r: number; g: number; b: number };
glow: number;
contrast: number;
}
export const DISPLAY_PRESETS: Record<string, DisplayPreset> = {
// Classic blue OLED (SSD1306)
"ssd1306-blue": {
name: "SSD1306 Blue",
geometry: "128x64",
pixelOn: { r: 100, g: 180, b: 255 },
pixelOff: { r: 2, g: 3, b: 8 },
glow: 0.4,
contrast: 1.0,
},
// White OLED (SSD1306)
"ssd1306-white": {
name: "SSD1306 White",
geometry: "128x64",
pixelOn: { r: 255, g: 255, b: 245 },
pixelOff: { r: 5, g: 5, b: 5 },
glow: 0.3,
contrast: 1.0,
},
// Yellow-blue dual color OLED
"ssd1306-yellow-blue": {
name: "SSD1306 Yellow/Blue",
geometry: "128x64",
// Yellow section is top 16 rows, blue is rest
pixelOn: { r: 255, g: 220, b: 50 }, // Will be overridden per-pixel
pixelOff: { r: 3, g: 3, b: 5 },
glow: 0.35,
contrast: 1.0,
},
// SH1106 (slightly different characteristics)
sh1106: {
name: "SH1106",
geometry: "128x64",
pixelOn: { r: 120, g: 200, b: 255 },
pixelOff: { r: 2, g: 2, b: 6 },
glow: 0.45,
contrast: 0.95,
},
// SH1107 128x128
sh1107: {
name: "SH1107 128x128",
geometry: "128x128",
pixelOn: { r: 255, g: 255, b: 240 },
pixelOff: { r: 4, g: 4, b: 6 },
glow: 0.3,
contrast: 1.0,
},
// Small 64x48 OLED
"ssd1306-64x48": {
name: "SSD1306 64x48",
geometry: "64x48",
pixelOn: { r: 100, g: 180, b: 255 },
pixelOff: { r: 2, g: 3, b: 8 },
glow: 0.4,
contrast: 1.0,
},
// Mini 128x32
"ssd1306-128x32": {
name: "SSD1306 128x32",
geometry: "128x32",
pixelOn: { r: 100, g: 180, b: 255 },
pixelOff: { r: 2, g: 3, b: 8 },
glow: 0.4,
contrast: 1.0,
},
};
+557
View File
@@ -0,0 +1,557 @@
/**
* OLEDDisplay.ts - TypeScript port of esp8266-oled-ssd1306 OLEDDisplay API
*
* This provides the same drawing API as the firmware uses, allowing
* Meshtastic screen rendering code to be ported to the web.
*
* Buffer format: Vertical byte packing (8 vertical pixels per byte)
* Same as SSD1306/SH1106 native format.
*/
export type OLEDDISPLAY_GEOMETRY =
| "128x64"
| "128x32"
| "64x48"
| "64x32"
| "128x128"
| "96x16";
export type OLEDDISPLAY_COLOR = "WHITE" | "BLACK" | "INVERSE";
export type OLEDDISPLAY_TEXT_ALIGNMENT =
| "TEXT_ALIGN_LEFT"
| "TEXT_ALIGN_CENTER"
| "TEXT_ALIGN_RIGHT";
export interface OLEDFont {
height: number;
firstChar: number;
charCount: number;
widths: number[];
data: Uint8Array;
}
// Resolution info based on geometry
const GEOMETRY_SIZES: Record<
OLEDDISPLAY_GEOMETRY,
{ width: number; height: number }
> = {
"128x64": { width: 128, height: 64 },
"128x32": { width: 128, height: 32 },
"64x48": { width: 64, height: 48 },
"64x32": { width: 64, height: 32 },
"128x128": { width: 128, height: 128 },
"96x16": { width: 96, height: 16 },
};
export class OLEDDisplay {
private buffer: Uint8Array;
private _width: number;
private _height: number;
private color: OLEDDISPLAY_COLOR = "WHITE";
private textAlignment: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
private font: OLEDFont | null = null;
constructor(geometry: OLEDDISPLAY_GEOMETRY = "128x64") {
const size = GEOMETRY_SIZES[geometry];
this._width = size.width;
this._height = size.height;
// Buffer size: width * (height / 8) - vertical byte packing
this.buffer = new Uint8Array((this._width * this._height) / 8);
}
get width(): number {
return this._width;
}
get height(): number {
return this._height;
}
getWidth(): number {
return this._width;
}
getHeight(): number {
return this._height;
}
/**
* Get the raw framebuffer (vertical byte packing format)
*/
getBuffer(): Uint8Array {
return this.buffer;
}
getBufferSize(): number {
return this.buffer.length;
}
/**
* Clear the display buffer
*/
clear(): void {
this.buffer.fill(0);
}
/**
* Set the drawing color
*/
setColor(color: OLEDDISPLAY_COLOR): void {
this.color = color;
}
/**
* Set text alignment for drawString
*/
setTextAlignment(align: OLEDDISPLAY_TEXT_ALIGNMENT): void {
this.textAlignment = align;
}
/**
* Set the current font
*/
setFont(font: OLEDFont): void {
this.font = font;
}
/**
* Set a single pixel
*/
setPixel(x: number, y: number): void {
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
// Vertical byte packing: each byte represents 8 vertical pixels
const byteIndex = x + Math.floor(y / 8) * this._width;
const bitMask = 1 << (y & 7);
switch (this.color) {
case "WHITE":
this.buffer[byteIndex] |= bitMask;
break;
case "BLACK":
this.buffer[byteIndex] &= ~bitMask;
break;
case "INVERSE":
this.buffer[byteIndex] ^= bitMask;
break;
}
}
/**
* Clear a single pixel (set to black)
*/
clearPixel(x: number, y: number): void {
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
const byteIndex = x + Math.floor(y / 8) * this._width;
const bitMask = 1 << (y & 7);
this.buffer[byteIndex] &= ~bitMask;
}
/**
* Get a pixel value (true = on, false = off)
*/
getPixel(x: number, y: number): boolean {
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return false;
const byteIndex = x + Math.floor(y / 8) * this._width;
const bitMask = 1 << (y & 7);
return (this.buffer[byteIndex] & bitMask) !== 0;
}
/**
* Draw a line using Bresenham's algorithm
*/
drawLine(x0: number, y0: number, x1: number, y1: number): void {
const dx = Math.abs(x1 - x0);
const dy = Math.abs(y1 - y0);
const sx = x0 < x1 ? 1 : -1;
const sy = y0 < y1 ? 1 : -1;
let err = dx - dy;
while (true) {
this.setPixel(x0, y0);
if (x0 === x1 && y0 === y1) break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
/**
* Draw horizontal line (optimized)
*/
drawHorizontalLine(x: number, y: number, length: number): void {
if (y < 0 || y >= this._height) return;
for (let i = 0; i < length; i++) {
this.setPixel(x + i, y);
}
}
/**
* Draw vertical line (optimized)
*/
drawVerticalLine(x: number, y: number, length: number): void {
if (x < 0 || x >= this._width) return;
for (let i = 0; i < length; i++) {
this.setPixel(x, y + i);
}
}
/**
* Draw a rectangle outline
*/
drawRect(x: number, y: number, width: number, height: number): void {
this.drawHorizontalLine(x, y, width);
this.drawHorizontalLine(x, y + height - 1, width);
this.drawVerticalLine(x, y, height);
this.drawVerticalLine(x + width - 1, y, height);
}
/**
* Draw a filled rectangle
*/
fillRect(x: number, y: number, width: number, height: number): void {
for (let i = 0; i < width; i++) {
for (let j = 0; j < height; j++) {
this.setPixel(x + i, y + j);
}
}
}
/**
* Draw a circle outline using midpoint algorithm
*/
drawCircle(x0: number, y0: number, radius: number): void {
let x = radius;
let y = 0;
let err = 0;
while (x >= y) {
this.setPixel(x0 + x, y0 + y);
this.setPixel(x0 + y, y0 + x);
this.setPixel(x0 - y, y0 + x);
this.setPixel(x0 - x, y0 + y);
this.setPixel(x0 - x, y0 - y);
this.setPixel(x0 - y, y0 - x);
this.setPixel(x0 + y, y0 - x);
this.setPixel(x0 + x, y0 - y);
y++;
if (err <= 0) {
err += 2 * y + 1;
}
if (err > 0) {
x--;
err -= 2 * x + 1;
}
}
}
/**
* Draw a filled circle
*/
fillCircle(x0: number, y0: number, radius: number): void {
let x = radius;
let y = 0;
let err = 0;
while (x >= y) {
this.drawHorizontalLine(x0 - x, y0 + y, 2 * x + 1);
this.drawHorizontalLine(x0 - y, y0 + x, 2 * y + 1);
this.drawHorizontalLine(x0 - x, y0 - y, 2 * x + 1);
this.drawHorizontalLine(x0 - y, y0 - x, 2 * y + 1);
y++;
if (err <= 0) {
err += 2 * y + 1;
}
if (err > 0) {
x--;
err -= 2 * x + 1;
}
}
}
/**
* Draw a quarter circle (quadrant 0-3)
*/
drawCircleQuads(x0: number, y0: number, radius: number, quads: number): void {
let x = radius;
let y = 0;
let err = 0;
while (x >= y) {
if (quads & 0x1) {
this.setPixel(x0 + x, y0 - y);
this.setPixel(x0 + y, y0 - x);
}
if (quads & 0x2) {
this.setPixel(x0 - y, y0 - x);
this.setPixel(x0 - x, y0 - y);
}
if (quads & 0x4) {
this.setPixel(x0 - x, y0 + y);
this.setPixel(x0 - y, y0 + x);
}
if (quads & 0x8) {
this.setPixel(x0 + y, y0 + x);
this.setPixel(x0 + x, y0 + y);
}
y++;
if (err <= 0) {
err += 2 * y + 1;
}
if (err > 0) {
x--;
err -= 2 * x + 1;
}
}
}
/**
* Draw an XBM image (like firmware's drawXbm)
* XBM format: LSB first, horizontal
*/
drawXbm(
x: number,
y: number,
width: number,
height: number,
xbm: Uint8Array | number[],
): void {
const widthBytes = Math.ceil(width / 8);
for (let j = 0; j < height; j++) {
for (let i = 0; i < width; i++) {
const byteIndex = j * widthBytes + Math.floor(i / 8);
const bitIndex = i % 8;
if (xbm[byteIndex] & (1 << bitIndex)) {
this.setPixel(x + i, y + j);
}
}
}
}
/**
* Draw a PROGMEM-style image (vertical byte format, like some firmware icons)
*/
drawFastImage(
x: number,
y: number,
width: number,
height: number,
image: Uint8Array | number[],
): void {
const heightInBytes = Math.ceil(height / 8);
for (let i = 0; i < width; i++) {
for (let j = 0; j < heightInBytes; j++) {
const imageByte = image[i + j * width];
for (let bit = 0; bit < 8; bit++) {
if (imageByte & (1 << bit)) {
this.setPixel(x + i, y + j * 8 + bit);
}
}
}
}
}
/**
* Get the width of a string with the current font
*/
getStringWidth(text: string): number {
if (!this.font) return 0;
let width = 0;
for (const char of text) {
const charCode = char.charCodeAt(0);
if (
charCode >= this.font.firstChar &&
charCode < this.font.firstChar + this.font.charCount
) {
width += this.font.widths[charCode - this.font.firstChar];
} else {
// Unknown char - use space width or default
width += this.font.widths[0] || 4;
}
}
return width;
}
/**
* Draw a string at the given position
*/
drawString(x: number, y: number, text: string): void {
if (!this.font) return;
// Adjust x based on alignment
let startX = x;
if (this.textAlignment === "TEXT_ALIGN_CENTER") {
startX = x - Math.floor(this.getStringWidth(text) / 2);
} else if (this.textAlignment === "TEXT_ALIGN_RIGHT") {
startX = x - this.getStringWidth(text);
}
let cursorX = startX;
for (const char of text) {
const charCode = char.charCodeAt(0);
if (
charCode >= this.font.firstChar &&
charCode < this.font.firstChar + this.font.charCount
) {
const charIndex = charCode - this.font.firstChar;
const charWidth = this.font.widths[charIndex];
// Find the offset into the font data
let offset = 0;
const bytesPerColumn = Math.ceil(this.font.height / 8);
for (let i = 0; i < charIndex; i++) {
offset += this.font.widths[i] * bytesPerColumn;
}
// Draw the character
this.drawFontChar(
cursorX,
y,
charWidth,
this.font.height,
this.font.data,
offset,
);
cursorX += charWidth;
} else {
// Unknown character - advance by space
cursorX += this.font.widths[0] || 4;
}
}
}
/**
* Draw a single font character
*/
private drawFontChar(
x: number,
y: number,
width: number,
height: number,
data: Uint8Array,
offset: number,
): void {
const bytesPerColumn = Math.ceil(height / 8);
for (let col = 0; col < width; col++) {
for (let byteRow = 0; byteRow < bytesPerColumn; byteRow++) {
const dataByte = data[offset + col * bytesPerColumn + byteRow];
for (let bit = 0; bit < 8; bit++) {
const pixelY = byteRow * 8 + bit;
if (pixelY < height && dataByte & (1 << bit)) {
this.setPixel(x + col, y + pixelY);
}
}
}
}
}
/**
* Draw a string that wraps within maxWidth
*/
drawStringMaxWidth(
x: number,
y: number,
maxWidth: number,
text: string,
): void {
if (!this.font) return;
const words = text.split(" ");
let line = "";
let lineY = y;
for (const word of words) {
const testLine = line + (line ? " " : "") + word;
const testWidth = this.getStringWidth(testLine);
if (testWidth > maxWidth && line) {
this.drawString(x, lineY, line);
line = word;
lineY += this.font.height;
} else {
line = testLine;
}
}
if (line) {
this.drawString(x, lineY, line);
}
}
/**
* Draw a progress bar
*/
drawProgressBar(
x: number,
y: number,
width: number,
height: number,
progress: number,
): void {
// Clamp progress 0-100
progress = Math.max(0, Math.min(100, progress));
// Draw outline
this.drawRect(x, y, width, height);
// Draw fill
const fillWidth = Math.floor(((width - 4) * progress) / 100);
this.fillRect(x + 2, y + 2, fillWidth, height - 4);
}
/**
* Display - no-op for emulator (would send buffer to hardware)
*/
display(): void {
// In real hardware this sends the buffer to the display
// For emulator, this is a no-op - we render from getBuffer()
}
/**
* Flip the display (useful for some mounting orientations)
*/
flipScreenVertically(): void {
// Rotate buffer 180 degrees
const newBuffer = new Uint8Array(this.buffer.length);
const heightBytes = this._height / 8;
for (let x = 0; x < this._width; x++) {
for (let yByte = 0; yByte < heightBytes; yByte++) {
let srcByte = this.buffer[x + yByte * this._width];
// Reverse the bits
let reversed = 0;
for (let i = 0; i < 8; i++) {
if (srcByte & (1 << i)) {
reversed |= 1 << (7 - i);
}
}
// Place in mirrored position
const newX = this._width - 1 - x;
const newYByte = heightBytes - 1 - yByte;
newBuffer[newX + newYByte * this._width] = reversed;
}
}
this.buffer = newBuffer;
}
}
// Color constants for compatibility with firmware code style
export const WHITE: OLEDDISPLAY_COLOR = "WHITE";
export const BLACK: OLEDDISPLAY_COLOR = "BLACK";
export const INVERSE: OLEDDISPLAY_COLOR = "INVERSE";
export const TEXT_ALIGN_LEFT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
export const TEXT_ALIGN_CENTER: OLEDDISPLAY_TEXT_ALIGNMENT =
"TEXT_ALIGN_CENTER";
export const TEXT_ALIGN_RIGHT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_RIGHT";
+327
View File
@@ -0,0 +1,327 @@
<script setup lang="ts">
/**
* OLEDEmulator.vue - Vue 3 component for OLED display emulation
*
* Renders an OLEDDisplay framebuffer with realistic OLED visual effects
* including pixel glow, color tinting, and various display sizes.
*/
import { ref, computed, watch, onMounted, onUnmounted, type PropType } from 'vue';
import { OLEDDisplay } from './OLEDDisplay';
import { DISPLAY_PRESETS, type DisplayPreset } from './DisplayPresets';
const props = defineProps({
/**
* The OLEDDisplay instance to render
*/
display: {
type: Object as PropType<OLEDDisplay>,
required: true,
},
/**
* Pixel scale factor (how many canvas pixels per OLED pixel)
*/
pixelScale: {
type: Number,
default: 4,
},
/**
* Display preset name or custom preset object
*/
preset: {
type: [String, Object] as PropType<string | DisplayPreset>,
default: 'ssd1306-blue',
},
/**
* Enable pixel glow effect (slight bloom around lit pixels)
*/
enableGlow: {
type: Boolean,
default: true,
},
/**
* Enable sub-pixel rendering for more realistic look
*/
enableSubPixel: {
type: Boolean,
default: false,
},
/**
* Show pixel grid lines
*/
showGrid: {
type: Boolean,
default: false,
},
/**
* Auto-refresh interval in ms (0 = manual refresh only)
*/
refreshInterval: {
type: Number,
default: 50,
},
/**
* Yellow/blue split mode for dual-color OLEDs (rows 0-15 yellow)
*/
dualColor: {
type: Boolean,
default: false,
},
});
const emit = defineEmits<{
(e: 'click', x: number, y: number): void;
(e: 'refresh'): void;
}>();
const canvas = ref<HTMLCanvasElement | null>(null);
const refreshTimer = ref<number | null>(null);
// Get the current display preset
const currentPreset = computed<DisplayPreset>(() => {
if (typeof props.preset === 'string') {
return DISPLAY_PRESETS[props.preset] || DISPLAY_PRESETS['ssd1306-blue'];
}
return props.preset;
});
// Canvas dimensions
const canvasWidth = computed(() => props.display.width * props.pixelScale);
const canvasHeight = computed(() => props.display.height * props.pixelScale);
/**
* Render the framebuffer to canvas with OLED effects
*/
function render(): void {
const cvs = canvas.value;
if (!cvs) return;
const ctx = cvs.getContext('2d');
if (!ctx) return;
const buffer = props.display.getBuffer();
const width = props.display.width;
const height = props.display.height;
const scale = props.pixelScale;
const preset = currentPreset.value;
// Create image data
const imageData = ctx.createImageData(canvasWidth.value, canvasHeight.value);
const data = imageData.data;
// Yellow color for dual-color mode (top 16 rows)
const yellowOn = { r: 255, g: 220, b: 50 };
const blueOn = { r: 100, g: 180, b: 255 };
// Render each OLED pixel
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// Get pixel from framebuffer (vertical byte packing)
const byteIndex = x + Math.floor(y / 8) * width;
const bitMask = 1 << (y & 7);
const isOn = (buffer[byteIndex] & bitMask) !== 0;
// Determine pixel color
let pixelColor: { r: number; g: number; b: number };
if (isOn) {
if (props.dualColor && y < 16) {
pixelColor = yellowOn;
} else if (props.dualColor) {
pixelColor = blueOn;
} else {
pixelColor = preset.pixelOn;
}
} else {
pixelColor = preset.pixelOff;
}
// Apply contrast
pixelColor = {
r: Math.min(255, pixelColor.r * preset.contrast),
g: Math.min(255, pixelColor.g * preset.contrast),
b: Math.min(255, pixelColor.b * preset.contrast),
};
// Scale up pixel to canvas
for (let sy = 0; sy < scale; sy++) {
for (let sx = 0; sx < scale; sx++) {
const px = x * scale + sx;
const py = y * scale + sy;
const idx = (py * canvasWidth.value + px) * 4;
// Apply glow effect (brighter center, dimmer edges)
let intensity = 1.0;
if (props.enableGlow && isOn) {
const centerX = scale / 2;
const centerY = scale / 2;
const dist = Math.sqrt(
Math.pow(sx - centerX, 2) + Math.pow(sy - centerY, 2)
);
const maxDist = Math.sqrt(2) * (scale / 2);
intensity = 1.0 - (dist / maxDist) * preset.glow;
}
// Apply sub-pixel rendering (RGB stripes)
if (props.enableSubPixel && isOn) {
const subPixelPos = sx % 3;
if (subPixelPos === 0) {
data[idx] = pixelColor.r * intensity * 1.2;
data[idx + 1] = pixelColor.g * intensity * 0.8;
data[idx + 2] = pixelColor.b * intensity * 0.8;
} else if (subPixelPos === 1) {
data[idx] = pixelColor.r * intensity * 0.8;
data[idx + 1] = pixelColor.g * intensity * 1.2;
data[idx + 2] = pixelColor.b * intensity * 0.8;
} else {
data[idx] = pixelColor.r * intensity * 0.8;
data[idx + 1] = pixelColor.g * intensity * 0.8;
data[idx + 2] = pixelColor.b * intensity * 1.2;
}
} else {
data[idx] = pixelColor.r * intensity;
data[idx + 1] = pixelColor.g * intensity;
data[idx + 2] = pixelColor.b * intensity;
}
data[idx + 3] = 255; // Alpha
}
}
}
}
// Draw grid lines if enabled
if (props.showGrid) {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// Draw right and bottom edge of each pixel
// Right edge
const rx = (x + 1) * scale - 1;
for (let sy = 0; sy < scale; sy++) {
const py = y * scale + sy;
const idx = (py * canvasWidth.value + rx) * 4;
data[idx] = Math.min(255, data[idx] + 20);
data[idx + 1] = Math.min(255, data[idx + 1] + 20);
data[idx + 2] = Math.min(255, data[idx + 2] + 20);
}
// Bottom edge
const by = (y + 1) * scale - 1;
for (let sx = 0; sx < scale; sx++) {
const px = x * scale + sx;
const idx = (by * canvasWidth.value + px) * 4;
data[idx] = Math.min(255, data[idx] + 20);
data[idx + 1] = Math.min(255, data[idx + 1] + 20);
data[idx + 2] = Math.min(255, data[idx + 2] + 20);
}
}
}
}
ctx.putImageData(imageData, 0, 0);
emit('refresh');
}
/**
* Handle canvas click
*/
function handleClick(event: MouseEvent): void {
const cvs = canvas.value;
if (!cvs) return;
const rect = cvs.getBoundingClientRect();
const scaleX = cvs.width / rect.width;
const scaleY = cvs.height / rect.height;
const canvasX = (event.clientX - rect.left) * scaleX;
const canvasY = (event.clientY - rect.top) * scaleY;
const oledX = Math.floor(canvasX / props.pixelScale);
const oledY = Math.floor(canvasY / props.pixelScale);
emit('click', oledX, oledY);
}
/**
* Start auto-refresh timer
*/
function startRefreshTimer(): void {
stopRefreshTimer();
if (props.refreshInterval > 0) {
refreshTimer.value = window.setInterval(render, props.refreshInterval);
}
}
/**
* Stop auto-refresh timer
*/
function stopRefreshTimer(): void {
if (refreshTimer.value !== null) {
clearInterval(refreshTimer.value);
refreshTimer.value = null;
}
}
// Lifecycle
onMounted(() => {
render();
startRefreshTimer();
});
onUnmounted(() => {
stopRefreshTimer();
});
// Watch for changes
watch(() => props.refreshInterval, startRefreshTimer);
watch(() => props.display, render, { deep: true });
watch(() => props.pixelScale, render);
watch(() => props.preset, render);
watch(() => props.enableGlow, render);
watch(() => props.showGrid, render);
watch(() => props.dualColor, render);
// Expose render method for manual refresh
defineExpose({ render });
</script>
<template>
<div class="oled-emulator-wrapper">
<canvas
ref="canvas"
:width="canvasWidth"
:height="canvasHeight"
class="oled-canvas"
@click="handleClick"
/>
<div class="oled-bezel" v-if="$slots.bezel">
<slot name="bezel" />
</div>
</div>
</template>
<style scoped>
.oled-emulator-wrapper {
display: inline-block;
position: relative;
background: #1a1a1a;
padding: 8px;
border-radius: 6px;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.oled-canvas {
display: block;
border-radius: 2px;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.oled-bezel {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
</style>
+467
View File
@@ -0,0 +1,467 @@
/**
* OLEDFonts.ts - Font data for OLED display emulator
*
* These are simplified bitmap fonts matching the style used in
* Meshtastic firmware (based on ArialMT from esp8266-oled-ssd1306).
*
* Font format: Vertical byte packing, column-major order
* Each character is drawn column by column, with each byte representing
* 8 vertical pixels.
*/
import type { OLEDFont } from "./OLEDDisplay";
/**
* ArialMT Plain 10 - Small font (height ~13 pixels)
* Covers ASCII 32-126 (space through ~)
*/
export const ArialMT_Plain_10: OLEDFont = {
height: 13,
firstChar: 32,
charCount: 95,
widths: [
3, // space
3, // !
4, // "
6, // #
6, // $
9, // %
7, // &
2, // '
4, // (
4, // )
5, // *
6, // +
3, // ,
4, // -
3, // .
4, // /
6, // 0
6, // 1
6, // 2
6, // 3
6, // 4
6, // 5
6, // 6
6, // 7
6, // 8
6, // 9
3, // :
3, // ;
6, // <
6, // =
6, // >
6, // ?
11, // @
8, // A
7, // B
7, // C
7, // D
6, // E
6, // F
8, // G
7, // H
3, // I
5, // J
7, // K
6, // L
9, // M
7, // N
8, // O
6, // P
8, // Q
7, // R
6, // S
6, // T
7, // U
7, // V
11, // W
7, // X
7, // Y
6, // Z
4, // [
4, // \
4, // ]
6, // ^
6, // _
4, // `
6, // a
6, // b
5, // c
6, // d
6, // e
4, // f
6, // g
6, // h
3, // i
3, // j
6, // k
3, // l
9, // m
6, // n
6, // o
6, // p
6, // q
4, // r
5, // s
4, // t
6, // u
6, // v
9, // w
6, // x
6, // y
5, // z
4, // {
3, // |
4, // }
6, // ~
],
data: new Uint8Array([
// Space (32)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// ! (33)
0x00, 0x00, 0xbe, 0x00, 0x00, 0x00,
// " (34)
0x00, 0x0e, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00,
// # (35)
0x00, 0x48, 0xf8, 0x4e, 0xf8, 0x4e, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00,
// $ (36)
0x00, 0x9c, 0x92, 0xff, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// % (37)
0x0c, 0x12, 0x12, 0x8c, 0x60, 0x18, 0xc6, 0x20, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// & (38)
0x00, 0x60, 0x9c, 0x92, 0x6a, 0xa4, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// ' (39)
0x00, 0x0e, 0x00, 0x00,
// ( (40)
0x00, 0x7c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
// ) (41)
0x00, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
// * (42)
0x14, 0x08, 0x3e, 0x08, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
// + (43)
0x10, 0x10, 0x7c, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// , (44)
0x00, 0x00, 0xc0, 0x00, 0x00, 0x00,
// - (45)
0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
// . (46)
0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
// / (47)
0x00, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00, 0x00,
// 0 (48)
0x7c, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 1 (49)
0x00, 0x04, 0x02, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 2 (50)
0x84, 0xc2, 0xa2, 0x92, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 3 (51)
0x44, 0x82, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 4 (52)
0x30, 0x28, 0x24, 0xfe, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 5 (53)
0x4e, 0x8a, 0x8a, 0x8a, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 6 (54)
0x78, 0x94, 0x92, 0x92, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 7 (55)
0x02, 0xc2, 0x32, 0x0a, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 8 (56)
0x6c, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 9 (57)
0x0c, 0x92, 0x92, 0x52, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// : (58)
0x00, 0x00, 0x88, 0x00, 0x00, 0x00,
// ; (59)
0x00, 0x00, 0xc8, 0x00, 0x00, 0x00,
// < (60)
0x10, 0x28, 0x28, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// = (61)
0x28, 0x28, 0x28, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// > (62)
0x44, 0x44, 0x28, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// ? (63)
0x04, 0x02, 0xa2, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// @ (64)
0xf0, 0x08, 0xe4, 0x12, 0x12, 0x12, 0xe2, 0x14, 0x08, 0xf0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// A (65)
0x00, 0xc0, 0x38, 0x26, 0x26, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
// B (66)
0xfe, 0x92, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// C (67)
0x7c, 0x82, 0x82, 0x82, 0x82, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// D (68)
0xfe, 0x82, 0x82, 0x82, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// E (69)
0xfe, 0x92, 0x92, 0x92, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// F (70)
0xfe, 0x12, 0x12, 0x12, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// G (71)
0x7c, 0x82, 0x82, 0x92, 0x92, 0x74, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
// H (72)
0xfe, 0x10, 0x10, 0x10, 0x10, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// I (73)
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
// J (74)
0x40, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// K (75)
0xfe, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// L (76)
0xfe, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// M (77)
0xfe, 0x04, 0x18, 0x60, 0x60, 0x18, 0x04, 0xfe, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// N (78)
0xfe, 0x04, 0x08, 0x30, 0x40, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// O (79)
0x7c, 0x82, 0x82, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
// P (80)
0xfe, 0x12, 0x12, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Q (81)
0x7c, 0x82, 0x82, 0x82, 0x42, 0xc2, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
// R (82)
0xfe, 0x12, 0x12, 0x32, 0x4c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// S (83)
0x4c, 0x92, 0x92, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// T (84)
0x02, 0x02, 0xfe, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// U (85)
0x7e, 0x80, 0x80, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// V (86)
0x06, 0x18, 0x60, 0x80, 0x60, 0x18, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// W (87)
0x06, 0x38, 0xc0, 0x38, 0x06, 0x38, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// X (88)
0x82, 0x44, 0x28, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// Y (89)
0x02, 0x04, 0x08, 0xf0, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// Z (90)
0xc2, 0xa2, 0x92, 0x8a, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// [ (91)
0x00, 0xfe, 0x82, 0x82, 0x00, 0x00, 0x00, 0x00,
// \ (92)
0x06, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
// ] (93)
0x82, 0x82, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00,
// ^ (94)
0x08, 0x04, 0x02, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// _ (95)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// ` (96)
0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
// a (97)
0x40, 0xa8, 0xa8, 0xa8, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// b (98)
0xfe, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// c (99)
0x70, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// d (100)
0x70, 0x88, 0x88, 0x88, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// e (101)
0x70, 0xa8, 0xa8, 0xa8, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// f (102)
0x08, 0xfc, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
// g (103)
0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00,
// h (104)
0xfe, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// i (105)
0x00, 0xfa, 0x00, 0x00, 0x00, 0x00,
// j (106)
0x00, 0x00, 0xfa, 0x01, 0x00, 0x00,
// k (107)
0xfe, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// l (108)
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
// m (109)
0xf8, 0x08, 0x08, 0xf0, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// n (110)
0xf8, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// o (111)
0x70, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// p (112)
0xf8, 0x48, 0x48, 0x48, 0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// q (113)
0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
// r (114)
0xf8, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
// s (115)
0x90, 0xa8, 0xa8, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// t (116)
0x08, 0x7e, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
// u (117)
0x78, 0x80, 0x80, 0x80, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// v (118)
0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// w (119)
0x78, 0x80, 0x60, 0x18, 0x60, 0x80, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// x (120)
0x88, 0x50, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// y (121)
0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
// z (122)
0xc8, 0xa8, 0x98, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// { (123)
0x10, 0x6c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
// | (124)
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
// } (125)
0x82, 0x6c, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
// ~ (126)
0x10, 0x08, 0x10, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]),
};
/**
* ArialMT Plain 16 - Medium font (height ~19 pixels)
* Simplified subset covering 0-9, A-Z, a-z, common punctuation
*/
export const ArialMT_Plain_16: OLEDFont = {
height: 19,
firstChar: 32,
charCount: 95,
widths: [
4, // space
4, // !
5, // "
8, // #
8, // $
12, // %
10, // &
2, // '
5, // (
5, // )
6, // *
8, // +
4, // ,
5, // -
4, // .
4, // /
8, // 0
8, // 1
8, // 2
8, // 3
8, // 4
8, // 5
8, // 6
8, // 7
8, // 8
8, // 9
4, // :
4, // ;
8, // <
8, // =
8, // >
8, // ?
15, // @
10, // A
9, // B
10, // C
10, // D
9, // E
8, // F
11, // G
9, // H
4, // I
6, // J
9, // K
8, // L
11, // M
9, // N
11, // O
9, // P
11, // Q
10, // R
9, // S
8, // T
9, // U
10, // V
14, // W
10, // X
10, // Y
9, // Z
4, // [
4, // \
4, // ]
7, // ^
8, // _
5, // `
8, // a
8, // b
7, // c
8, // d
8, // e
4, // f
8, // g
8, // h
4, // i
4, // j
8, // k
4, // l
12, // m
8, // n
8, // o
8, // p
8, // q
5, // r
7, // s
4, // t
8, // u
8, // v
12, // w
8, // x
8, // y
7, // z
5, // {
4, // |
5, // }
8, // ~
],
// Font data would be similar pattern but larger - using placeholder
data: new Uint8Array(2500).fill(0xff), // Placeholder - would contain actual font bitmap data
};
/**
* Helper to get FONT_HEIGHT_SMALL equivalent
*/
export const FONT_HEIGHT_SMALL = 13;
export const FONT_HEIGHT_MEDIUM = 19;
export const FONT_HEIGHT_LARGE = 28;
/**
* Helper function to create a font from raw byte data (for importing firmware fonts)
*/
export function createFontFromBytes(
height: number,
firstChar: number,
widths: number[],
data: number[],
): OLEDFont {
return {
height,
firstChar,
charCount: widths.length,
widths,
data: new Uint8Array(data),
};
}
+255
View File
@@ -0,0 +1,255 @@
/**
* OLEDImages.ts - Icon and image data for OLED display emulator
*
* These are direct ports of the XBM/bitmap icons from the Meshtastic firmware.
* Format: XBM style (LSB first, horizontal scanning)
*/
// Satellite icon (8x8)
export const imgSatellite_width = 8;
export const imgSatellite_height = 8;
export const imgSatellite = new Uint8Array([
0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111,
0b11011011, 0b00011000,
]);
// USB icon (10x8)
export const imgUSB = new Uint8Array([
0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe, 0x85, 0xfe, 0x89, 0xff, 0xf1,
0xfc, 0x00, 0xfc,
]);
// Power icon (8x16)
export const imgPower = new Uint8Array([
0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1c, 0x22, 0x22, 0x41, 0x7f,
0x22, 0x22, 0x22,
]);
// User icon (8x8)
export const imgUser = new Uint8Array([
0x3c, 0x42, 0x99, 0xa5, 0xa5, 0x99, 0x42, 0x3c,
]);
// Position empty arrow (6x8)
export const imgPositionEmpty = new Uint8Array([
0x20, 0x30, 0x28, 0x24, 0x42, 0xff,
]);
// Position solid arrow (6x8)
export const imgPositionSolid = new Uint8Array([
0x20, 0x30, 0x38, 0x3c, 0x7e, 0xff,
]);
// Mail icon (10x7)
export const mail_width = 10;
export const mail_height = 7;
export const mail = new Uint8Array([
0b11111111,
0b00, // Top line
0b10000001,
0b00, // Edges
0b11000011,
0b00, // Diagonals start
0b10100101,
0b00, // Inner M part
0b10011001,
0b00, // Inner M part
0b10000001,
0b00, // Edges
0b11111111,
0b00, // Bottom line
]);
// Hop icon (9x10)
export const hop_width = 9;
export const hop_height = 10;
export const hop = new Uint8Array([
0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, 0x00, 0xc0,
0x01, 0x40, 0x01, 0xc0, 0x01, 0x40, 0x00,
]);
// Mail icon (8x8)
export const icon_mail = new Uint8Array([
0b11111111, // ████████ top border
0b10000001, // █ █ sides
0b11000011, // ██ ██ diagonal
0b10100101, // █ █ █ █ inner M
0b10011001, // █ ██ █ inner M
0b10000001, // █ █ sides
0b10000001, // █ █ sides
0b11111111, // ████████ bottom
]);
// Compass icon (8x8)
export const icon_compass = new Uint8Array([
0x3c, // Row 0: ..####..
0x52, // Row 1: .#..#.#.
0x91, // Row 2: #...#..#
0x91, // Row 3: #...#..#
0x91, // Row 4: #...#..#
0x81, // Row 5: #......#
0x42, // Row 6: .#....#.
0x3c, // Row 7: ..####..
]);
// Radio icon (8x8)
export const icon_radio = new Uint8Array([
0x0f, // Row 0: ####....
0x10, // Row 1: ....#...
0x27, // Row 2: ###..#..
0x48, // Row 3: ...#..#.
0x93, // Row 4: ##..#..#
0xa4, // Row 5: ..#..#.#
0xa8, // Row 6: ...#.#.#
0xa9, // Row 7: #..#.#.#
]);
// System/settings icon (8x8)
export const icon_system = new Uint8Array([
0x24, // Row 0: ..#..#..
0x3c, // Row 1: ..####..
0xc3, // Row 2: ##....##
0x5a, // Row 3: .#.##.#.
0x5a, // Row 4: .#.##.#.
0xc3, // Row 5: ##....##
0x3c, // Row 6: ..####..
0x24, // Row 7: ..#..#..
]);
// WiFi icon (8x8)
export const icon_wifi = new Uint8Array([
0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000,
0b00011000, 0b00000000,
]);
// Nodes icon (8x8)
export const icon_nodes = new Uint8Array([
0xf9, // Row 0: #..#####
0x00, // Row 1
0xf9, // Row 2: #..#####
0x00, // Row 3
0xf9, // Row 4: #..#####
0x00, // Row 5
0xf9, // Row 6: #..#####
0x00, // Row 7
]);
// Battery vertical (7x11)
export const batteryBitmap_v = new Uint8Array([
0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000,
0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110,
]);
// Battery side gaps (8x3)
export const batteryBitmap_sidegaps_v = new Uint8Array([
0b10000010, 0b10000010, 0b10000010,
]);
// Lightning bolt vertical (5x5)
export const lightning_bolt_v = new Uint8Array([
0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100,
]);
// Horizontal battery bottom (13x9)
export const batteryBitmap_h_bottom = new Uint8Array([
0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
0b00011110, 0b00000000,
]);
// Horizontal battery top (13x9)
export const batteryBitmap_h_top = new Uint8Array([
0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b11000000, 0b00000000,
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
0b00111100, 0b00000000,
]);
// Lightning bolt horizontal (13x9)
export const lightning_bolt_h = new Uint8Array([
0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000,
0b00111000, 0b00000000, 0b00111100, 0b00000000, 0b00011110, 0b00000000,
0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000,
0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000,
0b00000000, 0b00000000,
]);
// Signal bars icon (various signal strengths)
export const signal_bars = {
width: 12,
height: 8,
// 0 bars
none: new Uint8Array([
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000,
]),
// 1 bar
low: new Uint8Array([
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000011, 0b00000000, 0b00000011, 0b00000000,
]),
// 2 bars
medium: new Uint8Array([
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00001100, 0b00000000, 0b00001100, 0b00000000,
0b00001111, 0b00000000, 0b00001111, 0b00000000,
]),
// 3 bars
high: new Uint8Array([
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00110000, 0b00000000,
0b00110000, 0b00000000, 0b00111100, 0b00000000, 0b00111100, 0b00000000,
0b00111111, 0b00000000, 0b00111111, 0b00000000,
]),
// 4 bars (full)
full: new Uint8Array([
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11110000, 0b00000000,
0b11110000, 0b00000000, 0b11111100, 0b00000000, 0b11111100, 0b00000000,
0b11111111, 0b00000000, 0b11111111, 0b00000000,
]),
};
// Meshtastic logo (simplified 32x32)
export const logo_width = 32;
export const logo_height = 32;
export const meshtastic_logo = new Uint8Array([
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00,
0x03, 0xc0, 0x00, 0x80, 0x01, 0x80, 0x01, 0xc0, 0x00, 0x00, 0x03, 0x60, 0x00,
0x00, 0x06, 0x30, 0x00, 0x00, 0x0c, 0x18, 0xf8, 0x1f, 0x18, 0x08, 0x04, 0x20,
0x10, 0x0c, 0x02, 0x40, 0x30, 0x04, 0x01, 0x80, 0x20, 0x06, 0x01, 0x80, 0x60,
0x02, 0xe1, 0x87, 0x40, 0x02, 0x11, 0x88, 0x40, 0x03, 0x09, 0x90, 0xc0, 0x03,
0x09, 0x90, 0xc0, 0x02, 0x11, 0x88, 0x40, 0x02, 0xe1, 0x87, 0x40, 0x06, 0x01,
0x80, 0x60, 0x04, 0x01, 0x80, 0x20, 0x0c, 0x02, 0x40, 0x30, 0x08, 0x04, 0x20,
0x10, 0x18, 0xf8, 0x1f, 0x18, 0x30, 0x00, 0x00, 0x0c, 0x60, 0x00, 0x00, 0x06,
0xc0, 0x00, 0x00, 0x03, 0x80, 0x01, 0x80, 0x01, 0x00, 0x03, 0xc0, 0x00, 0x00,
0x06, 0x60, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00,
]);
// Bluetooth icon (8x11)
export const icon_bluetooth = new Uint8Array([
0b00000000, 0b10000100, 0b01001010, 0b00101010, 0b00010100, 0b11111111,
0b00010100, 0b00101010, 0b01001010, 0b10000100, 0b00000000,
]);
// GPS/Location pin icon (8x12)
export const icon_gps_pin = new Uint8Array([
0b00111100, 0b01111110, 0b11111111, 0b11100111, 0b11100111, 0b11111111,
0b01111110, 0b00111100, 0b00011000, 0b00011000, 0b00001000, 0b00000000,
]);
// Message/chat bubble icon (10x8)
export const icon_message = new Uint8Array([
0b11111111, 0b00, 0b10000001, 0b00, 0b10000001, 0b00, 0b10000001, 0b00,
0b10000001, 0b00, 0b11111111, 0b00, 0b00000110, 0b00, 0b00000010, 0b00,
]);
/**
* Helper to convert icon data to XBM format if needed
*/
export function toXbm(data: Uint8Array | number[]): Uint8Array {
return data instanceof Uint8Array ? data : new Uint8Array(data);
}
+627
View File
@@ -0,0 +1,627 @@
/**
* ScreenRenderers.ts - Sample screen rendering functions
*
* These demonstrate how to port Meshtastic firmware UI screens
* to the OLED emulator. The API matches the firmware's drawing code.
*/
import {
OLEDDisplay,
WHITE,
BLACK,
TEXT_ALIGN_LEFT,
TEXT_ALIGN_CENTER,
TEXT_ALIGN_RIGHT,
} from "./OLEDDisplay";
import { Font_6x8 } from "./SimpleFonts";
import * as images from "./OLEDImages";
// Font height for simple 6x8 font
const FONT_HEIGHT_SMALL = 8;
// Screen resolution detection (matches SharedUIDisplay.cpp)
export type ScreenResolution = "UltraLow" | "Low" | "High";
export function determineScreenResolution(
width: number,
height: number,
): ScreenResolution {
if (width <= 64 || height <= 48) {
return "UltraLow";
}
if (width > 128) {
return "High";
}
return "Low";
}
// Helper for consistent line positioning
export function getTextPositions(display: OLEDDisplay): number[] {
const fontHeight = FONT_HEIGHT_SMALL;
return [
0, // textZeroLine
fontHeight - 1, // textFirstLine
fontHeight - 1 + (fontHeight - 5), // textSecondLine
fontHeight - 1 + 2 * (fontHeight - 5), // textThirdLine
fontHeight - 1 + 3 * (fontHeight - 5), // textFourthLine
fontHeight - 1 + 4 * (fontHeight - 5), // textFifthLine
];
}
/**
* Draw a rounded highlight box (used for inverted headers)
*/
export function drawRoundedHighlight(
display: OLEDDisplay,
x: number,
y: number,
w: number,
h: number,
r: number,
): void {
// Draw the center and side rectangles
display.fillRect(x + r, y, w - 2 * r, h); // center bar
display.fillRect(x, y + r, r, h - 2 * r); // left edge
display.fillRect(x + w - r, y + r, r, h - 2 * r); // right edge
// Draw the rounded corners using filled circles
display.fillCircle(x + r + 1, y + r, r); // top-left
display.fillCircle(x + w - r - 1, y + r, r); // top-right
display.fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left
display.fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right
}
/**
* Draw the common header bar (battery, title, icons)
*/
export function drawCommonHeader(
display: OLEDDisplay,
x: number,
y: number,
titleStr: string,
options: {
inverted?: boolean;
batteryPercent?: number;
isCharging?: boolean;
hasUSB?: boolean;
hasUnreadMessage?: boolean;
} = {},
): void {
const {
inverted = true,
batteryPercent = 75,
isCharging = false,
hasUSB = false,
hasUnreadMessage = false,
} = options;
const HEADER_OFFSET_Y = 1;
y += HEADER_OFFSET_Y;
display.setFont(Font_6x8);
const highlightHeight = FONT_HEIGHT_SMALL - 1;
const screenW = display.getWidth();
if (inverted) {
// Draw inverted header background
display.setColor(WHITE);
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
display.setColor(BLACK);
} else {
// Draw line under header
display.setColor(WHITE);
display.drawLine(0, 14, screenW, 14);
}
// Draw title centered
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(screenW / 2, y, titleStr);
// Reset color for remaining elements
if (inverted) {
display.setColor(WHITE);
}
// Draw battery on left
let batteryX = 1;
const batteryY = HEADER_OFFSET_Y + 1;
if (hasUSB && !isCharging) {
// Draw USB icon
display.drawXbm(batteryX + 1, batteryY + 2, 10, 8, images.imgUSB);
batteryX += 11;
} else {
// Draw battery outline
display.drawXbm(batteryX, batteryY, 7, 11, images.batteryBitmap_v);
if (isCharging) {
// Draw lightning bolt
display.drawXbm(
batteryX + 1,
batteryY + 3,
5,
5,
images.lightning_bolt_v,
);
} else {
// Draw battery level
display.drawXbm(
batteryX - 1,
batteryY + 4,
8,
3,
images.batteryBitmap_sidegaps_v,
);
const fillHeight = Math.floor((8 * batteryPercent) / 100);
const fillY = batteryY - fillHeight + 10;
display.fillRect(batteryX + 1, fillY, 5, fillHeight);
}
batteryX += 9;
}
// Draw mail icon on right if unread message
if (hasUnreadMessage) {
const mailX = screenW - images.mail_width - 2;
display.drawXbm(
mailX,
y + 2,
images.mail_width,
images.mail_height,
images.mail,
);
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setColor(WHITE);
}
/**
* Draw a boot/splash screen with logo
*/
export function drawBootScreen(
display: OLEDDisplay,
appVersion: string = "2.5.0",
): void {
display.clear();
display.setFont(Font_6x8);
display.setColor(WHITE);
const centerX = display.getWidth() / 2;
const centerY = display.getHeight() / 2;
// Draw logo centered
const logoX = centerX - images.logo_width / 2;
const logoY = centerY - images.logo_height / 2 - 8;
display.drawXbm(
logoX,
logoY,
images.logo_width,
images.logo_height,
images.meshtastic_logo,
);
// Draw version below
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(
centerX,
centerY + images.logo_height / 2,
`v${appVersion}`,
);
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
/**
* Draw a node info screen
*/
export function drawNodeInfoScreen(
display: OLEDDisplay,
nodeInfo: {
shortName: string;
longName: string;
nodeId: string;
batteryLevel?: number;
lastHeard?: string;
snr?: number;
hopsAway?: number;
},
): void {
display.clear();
// Draw header
drawCommonHeader(display, 0, 0, "Node Info", {
inverted: true,
batteryPercent: 85,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_LEFT);
const lines = getTextPositions(display);
const screenW = display.getWidth();
// Node name (bolded by drawing twice offset)
display.drawString(0, lines[1], nodeInfo.longName);
display.drawString(1, lines[1], nodeInfo.longName);
// Short name and ID
display.drawString(0, lines[2], `${nodeInfo.shortName} • ${nodeInfo.nodeId}`);
// Last heard
if (nodeInfo.lastHeard) {
display.drawString(0, lines[3], `Heard: ${nodeInfo.lastHeard}`);
}
// Signal info on right side
if (nodeInfo.snr !== undefined) {
const snrStr = `SNR: ${nodeInfo.snr}dB`;
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(screenW, lines[3], snrStr);
}
// Hops
if (nodeInfo.hopsAway !== undefined) {
const hopStr =
nodeInfo.hopsAway === 0 ? "Direct" : `${nodeInfo.hopsAway} hops`;
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.drawString(0, lines[4], hopStr);
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
/**
* Draw a message screen
*/
export function drawMessageScreen(
display: OLEDDisplay,
message: {
from: string;
text: string;
time: string;
channel?: string;
},
): void {
display.clear();
// Draw header with channel name if provided
const headerTitle = message.channel || "Message";
drawCommonHeader(display, 0, 0, headerTitle, {
inverted: true,
batteryPercent: 72,
hasUnreadMessage: true,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_LEFT);
const lines = getTextPositions(display);
const screenW = display.getWidth();
// From line
display.drawString(0, lines[1], `From: ${message.from}`);
// Time on right
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(screenW, lines[1], message.time);
// Message text (with word wrap)
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.drawStringMaxWidth(0, lines[2], screenW, message.text);
}
/**
* Draw a GPS/location screen
*/
export function drawGPSScreen(
display: OLEDDisplay,
gpsInfo: {
latitude?: number;
longitude?: number;
altitude?: number;
satellites?: number;
hasLock: boolean;
speed?: number;
},
): void {
display.clear();
drawCommonHeader(display, 0, 0, "GPS", {
inverted: true,
batteryPercent: 90,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_LEFT);
const lines = getTextPositions(display);
// Draw satellite icon and count
display.drawXbm(
0,
lines[1],
images.imgSatellite_width,
images.imgSatellite_height,
images.imgSatellite,
);
if (!gpsInfo.hasLock) {
display.drawString(12, lines[1], "No Lock");
} else {
display.drawString(12, lines[1], `${gpsInfo.satellites || 0} sats`);
}
if (
gpsInfo.hasLock &&
gpsInfo.latitude !== undefined &&
gpsInfo.longitude !== undefined
) {
// Coordinates
display.drawString(0, lines[2], `Lat: ${gpsInfo.latitude.toFixed(6)}`);
display.drawString(0, lines[3], `Lon: ${gpsInfo.longitude.toFixed(6)}`);
// Altitude
if (gpsInfo.altitude !== undefined) {
display.drawString(0, lines[4], `Alt: ${gpsInfo.altitude.toFixed(0)}m`);
}
// Speed
if (gpsInfo.speed !== undefined) {
const screenW = display.getWidth();
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(screenW, lines[4], `${gpsInfo.speed.toFixed(1)} km/h`);
}
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
/**
* Draw a node list screen
*/
export function drawNodeListScreen(
display: OLEDDisplay,
nodes: Array<{
shortName: string;
longName: string;
lastHeard: string;
snr?: number;
}>,
selectedIndex: number = 0,
): void {
display.clear();
drawCommonHeader(display, 0, 0, `Nodes (${nodes.length})`, {
inverted: true,
batteryPercent: 80,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_LEFT);
const lines = getTextPositions(display);
const screenW = display.getWidth();
const maxVisibleNodes = 4;
const startIndex = Math.max(
0,
selectedIndex - Math.floor(maxVisibleNodes / 2),
);
for (let i = 0; i < maxVisibleNodes && startIndex + i < nodes.length; i++) {
const node = nodes[startIndex + i];
const lineY = lines[i + 1];
const isSelected = startIndex + i === selectedIndex;
// Highlight selected row
if (isSelected) {
display.fillRect(0, lineY - 1, screenW, FONT_HEIGHT_SMALL);
display.setColor(BLACK);
}
// Node short name
display.drawString(0, lineY, node.shortName);
// Last heard on right
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(screenW, lineY, node.lastHeard);
display.setTextAlignment(TEXT_ALIGN_LEFT);
if (isSelected) {
display.setColor(WHITE);
}
}
}
/**
* Draw a system info screen
*/
export function drawSystemScreen(
display: OLEDDisplay,
sysInfo: {
uptime: string;
channelUtil: number;
airUtil: number;
batteryVoltage?: number;
nodes: number;
freeMemory?: number;
},
): void {
display.clear();
drawCommonHeader(display, 0, 0, "System", {
inverted: true,
batteryPercent: 95,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_LEFT);
const lines = getTextPositions(display);
const screenW = display.getWidth();
// Uptime
display.drawString(0, lines[1], `Uptime: ${sysInfo.uptime}`);
// Channel utilization
display.drawString(0, lines[2], `ChUtil: ${sysInfo.channelUtil.toFixed(1)}%`);
// Air utilization
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(
screenW,
lines[2],
`AirTx: ${sysInfo.airUtil.toFixed(1)}%`,
);
display.setTextAlignment(TEXT_ALIGN_LEFT);
// Nodes
display.drawString(0, lines[3], `Nodes: ${sysInfo.nodes}`);
// Battery voltage
if (sysInfo.batteryVoltage !== undefined) {
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(
screenW,
lines[3],
`${sysInfo.batteryVoltage.toFixed(2)}V`,
);
}
// Free memory
if (sysInfo.freeMemory !== undefined) {
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.drawString(
0,
lines[4],
`Free: ${(sysInfo.freeMemory / 1024).toFixed(0)}KB`,
);
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
/**
* Draw a compass screen with bearing arrow
*/
export function drawCompassScreen(
display: OLEDDisplay,
compassInfo: {
heading: number; // Device heading in degrees
bearing: number; // Bearing to target in degrees
distance?: number; // Distance to target in meters
targetName?: string;
},
): void {
display.clear();
drawCommonHeader(display, 0, 0, compassInfo.targetName || "Compass", {
inverted: true,
batteryPercent: 70,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
const screenW = display.getWidth();
const screenH = display.getHeight();
// Compass center and radius
const compassRadius = Math.min(screenW, screenH - 20) / 2 - 4;
const compassX = screenW / 2;
const compassY = (screenH + 16) / 2;
// Draw compass circle
display.drawCircle(compassX, compassY, compassRadius);
// Draw cardinal directions
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(compassX, compassY - compassRadius - 10, "N");
// Calculate relative bearing (bearing - heading)
const relativeBearing =
((compassInfo.bearing - compassInfo.heading) * Math.PI) / 180;
// Draw bearing arrow
const arrowLength = compassRadius - 6;
const arrowX = compassX + Math.sin(relativeBearing) * arrowLength;
const arrowY = compassY - Math.cos(relativeBearing) * arrowLength;
display.drawLine(compassX, compassY, arrowX, arrowY);
// Draw arrowhead
const headAngle = 0.4;
const headLength = 8;
const angle1 = relativeBearing + Math.PI - headAngle;
const angle2 = relativeBearing + Math.PI + headAngle;
display.drawLine(
arrowX,
arrowY,
arrowX + Math.sin(angle1) * headLength,
arrowY - Math.cos(angle1) * headLength,
);
display.drawLine(
arrowX,
arrowY,
arrowX + Math.sin(angle2) * headLength,
arrowY - Math.cos(angle2) * headLength,
);
// Draw distance at bottom
if (compassInfo.distance !== undefined) {
let distStr: string;
if (compassInfo.distance >= 1000) {
distStr = `${(compassInfo.distance / 1000).toFixed(1)} km`;
} else {
distStr = `${Math.round(compassInfo.distance)} m`;
}
display.drawString(compassX, screenH - FONT_HEIGHT_SMALL, distStr);
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
/**
* Draw a progress/loading screen
*/
export function drawProgressScreen(
display: OLEDDisplay,
title: string,
progress: number,
statusText?: string,
): void {
display.clear();
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_CENTER);
const screenW = display.getWidth();
const screenH = display.getHeight();
const centerX = screenW / 2;
const centerY = screenH / 2;
// Title
display.drawString(centerX, centerY - 20, title);
// Progress bar
const barWidth = screenW - 20;
const barHeight = 10;
const barX = 10;
const barY = centerY - barHeight / 2;
display.drawProgressBar(barX, barY, barWidth, barHeight, progress);
// Progress percentage
display.drawString(centerX, barY + barHeight + 4, `${Math.round(progress)}%`);
// Status text
if (statusText) {
display.drawString(centerX, screenH - FONT_HEIGHT_SMALL - 2, statusText);
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
+421
View File
@@ -0,0 +1,421 @@
/**
* SimpleFonts.ts - Simple working bitmap fonts for OLED emulator
*
* These are basic monospace bitmap fonts that are easy to render correctly.
* Format: Each character is stored in column-major order, 1 byte per column.
* For fonts taller than 8px, multiple bytes per column are used.
*/
import type { OLEDFont } from "./OLEDDisplay";
/**
* Simple 6x8 monospace font
* Each character is 6 columns wide, 8 rows tall
* 1 byte per column (8 vertical pixels)
* Includes ASCII 32-126
*/
export const Font_6x8: OLEDFont = {
height: 8,
firstChar: 32,
charCount: 95,
widths: new Array(95).fill(6), // Fixed width font
data: new Uint8Array([
// Space (32)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// ! (33)
0x00, 0x00, 0x5f, 0x00, 0x00, 0x00,
// " (34)
0x00, 0x07, 0x00, 0x07, 0x00, 0x00,
// # (35)
0x14, 0x7f, 0x14, 0x7f, 0x14, 0x00,
// $ (36)
0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x00,
// % (37)
0x23, 0x13, 0x08, 0x64, 0x62, 0x00,
// & (38)
0x36, 0x49, 0x55, 0x22, 0x50, 0x00,
// ' (39)
0x00, 0x05, 0x03, 0x00, 0x00, 0x00,
// ( (40)
0x00, 0x1c, 0x22, 0x41, 0x00, 0x00,
// ) (41)
0x00, 0x41, 0x22, 0x1c, 0x00, 0x00,
// * (42)
0x08, 0x2a, 0x1c, 0x2a, 0x08, 0x00,
// + (43)
0x08, 0x08, 0x3e, 0x08, 0x08, 0x00,
// , (44)
0x00, 0x50, 0x30, 0x00, 0x00, 0x00,
// - (45)
0x08, 0x08, 0x08, 0x08, 0x08, 0x00,
// . (46)
0x00, 0x60, 0x60, 0x00, 0x00, 0x00,
// / (47)
0x20, 0x10, 0x08, 0x04, 0x02, 0x00,
// 0 (48)
0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00,
// 1 (49)
0x00, 0x42, 0x7f, 0x40, 0x00, 0x00,
// 2 (50)
0x42, 0x61, 0x51, 0x49, 0x46, 0x00,
// 3 (51)
0x21, 0x41, 0x45, 0x4b, 0x31, 0x00,
// 4 (52)
0x18, 0x14, 0x12, 0x7f, 0x10, 0x00,
// 5 (53)
0x27, 0x45, 0x45, 0x45, 0x39, 0x00,
// 6 (54)
0x3c, 0x4a, 0x49, 0x49, 0x30, 0x00,
// 7 (55)
0x01, 0x71, 0x09, 0x05, 0x03, 0x00,
// 8 (56)
0x36, 0x49, 0x49, 0x49, 0x36, 0x00,
// 9 (57)
0x06, 0x49, 0x49, 0x29, 0x1e, 0x00,
// : (58)
0x00, 0x36, 0x36, 0x00, 0x00, 0x00,
// ; (59)
0x00, 0x56, 0x36, 0x00, 0x00, 0x00,
// < (60)
0x00, 0x08, 0x14, 0x22, 0x41, 0x00,
// = (61)
0x14, 0x14, 0x14, 0x14, 0x14, 0x00,
// > (62)
0x41, 0x22, 0x14, 0x08, 0x00, 0x00,
// ? (63)
0x02, 0x01, 0x51, 0x09, 0x06, 0x00,
// @ (64)
0x32, 0x49, 0x79, 0x41, 0x3e, 0x00,
// A (65)
0x7e, 0x11, 0x11, 0x11, 0x7e, 0x00,
// B (66)
0x7f, 0x49, 0x49, 0x49, 0x36, 0x00,
// C (67)
0x3e, 0x41, 0x41, 0x41, 0x22, 0x00,
// D (68)
0x7f, 0x41, 0x41, 0x22, 0x1c, 0x00,
// E (69)
0x7f, 0x49, 0x49, 0x49, 0x41, 0x00,
// F (70)
0x7f, 0x09, 0x09, 0x01, 0x01, 0x00,
// G (71)
0x3e, 0x41, 0x41, 0x51, 0x32, 0x00,
// H (72)
0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00,
// I (73)
0x00, 0x41, 0x7f, 0x41, 0x00, 0x00,
// J (74)
0x20, 0x40, 0x41, 0x3f, 0x01, 0x00,
// K (75)
0x7f, 0x08, 0x14, 0x22, 0x41, 0x00,
// L (76)
0x7f, 0x40, 0x40, 0x40, 0x40, 0x00,
// M (77)
0x7f, 0x02, 0x04, 0x02, 0x7f, 0x00,
// N (78)
0x7f, 0x04, 0x08, 0x10, 0x7f, 0x00,
// O (79)
0x3e, 0x41, 0x41, 0x41, 0x3e, 0x00,
// P (80)
0x7f, 0x09, 0x09, 0x09, 0x06, 0x00,
// Q (81)
0x3e, 0x41, 0x51, 0x21, 0x5e, 0x00,
// R (82)
0x7f, 0x09, 0x19, 0x29, 0x46, 0x00,
// S (83)
0x46, 0x49, 0x49, 0x49, 0x31, 0x00,
// T (84)
0x01, 0x01, 0x7f, 0x01, 0x01, 0x00,
// U (85)
0x3f, 0x40, 0x40, 0x40, 0x3f, 0x00,
// V (86)
0x1f, 0x20, 0x40, 0x20, 0x1f, 0x00,
// W (87)
0x7f, 0x20, 0x18, 0x20, 0x7f, 0x00,
// X (88)
0x63, 0x14, 0x08, 0x14, 0x63, 0x00,
// Y (89)
0x03, 0x04, 0x78, 0x04, 0x03, 0x00,
// Z (90)
0x61, 0x51, 0x49, 0x45, 0x43, 0x00,
// [ (91)
0x00, 0x00, 0x7f, 0x41, 0x41, 0x00,
// \ (92)
0x02, 0x04, 0x08, 0x10, 0x20, 0x00,
// ] (93)
0x41, 0x41, 0x7f, 0x00, 0x00, 0x00,
// ^ (94)
0x04, 0x02, 0x01, 0x02, 0x04, 0x00,
// _ (95)
0x40, 0x40, 0x40, 0x40, 0x40, 0x00,
// ` (96)
0x00, 0x01, 0x02, 0x04, 0x00, 0x00,
// a (97)
0x20, 0x54, 0x54, 0x54, 0x78, 0x00,
// b (98)
0x7f, 0x48, 0x44, 0x44, 0x38, 0x00,
// c (99)
0x38, 0x44, 0x44, 0x44, 0x20, 0x00,
// d (100)
0x38, 0x44, 0x44, 0x48, 0x7f, 0x00,
// e (101)
0x38, 0x54, 0x54, 0x54, 0x18, 0x00,
// f (102)
0x08, 0x7e, 0x09, 0x01, 0x02, 0x00,
// g (103)
0x08, 0x14, 0x54, 0x54, 0x3c, 0x00,
// h (104)
0x7f, 0x08, 0x04, 0x04, 0x78, 0x00,
// i (105)
0x00, 0x44, 0x7d, 0x40, 0x00, 0x00,
// j (106)
0x20, 0x40, 0x44, 0x3d, 0x00, 0x00,
// k (107)
0x00, 0x7f, 0x10, 0x28, 0x44, 0x00,
// l (108)
0x00, 0x41, 0x7f, 0x40, 0x00, 0x00,
// m (109)
0x7c, 0x04, 0x18, 0x04, 0x78, 0x00,
// n (110)
0x7c, 0x08, 0x04, 0x04, 0x78, 0x00,
// o (111)
0x38, 0x44, 0x44, 0x44, 0x38, 0x00,
// p (112)
0x7c, 0x14, 0x14, 0x14, 0x08, 0x00,
// q (113)
0x08, 0x14, 0x14, 0x18, 0x7c, 0x00,
// r (114)
0x7c, 0x08, 0x04, 0x04, 0x08, 0x00,
// s (115)
0x48, 0x54, 0x54, 0x54, 0x20, 0x00,
// t (116)
0x04, 0x3f, 0x44, 0x40, 0x20, 0x00,
// u (117)
0x3c, 0x40, 0x40, 0x20, 0x7c, 0x00,
// v (118)
0x1c, 0x20, 0x40, 0x20, 0x1c, 0x00,
// w (119)
0x3c, 0x40, 0x30, 0x40, 0x3c, 0x00,
// x (120)
0x44, 0x28, 0x10, 0x28, 0x44, 0x00,
// y (121)
0x0c, 0x50, 0x50, 0x50, 0x3c, 0x00,
// z (122)
0x44, 0x64, 0x54, 0x4c, 0x44, 0x00,
// { (123)
0x00, 0x08, 0x36, 0x41, 0x00, 0x00,
// | (124)
0x00, 0x00, 0x7f, 0x00, 0x00, 0x00,
// } (125)
0x00, 0x41, 0x36, 0x08, 0x00, 0x00,
// ~ (126)
0x08, 0x08, 0x2a, 0x1c, 0x08, 0x00,
]),
};
/**
* Simple 5x7 monospace font (more compact)
* Each character is 5 columns wide, 7 rows tall
* 1 byte per column
*/
export const Font_5x7: OLEDFont = {
height: 7,
firstChar: 32,
charCount: 95,
widths: new Array(95).fill(5),
data: new Uint8Array([
// Space (32)
0x00, 0x00, 0x00, 0x00, 0x00,
// ! (33)
0x00, 0x00, 0x2f, 0x00, 0x00,
// " (34)
0x00, 0x07, 0x00, 0x07, 0x00,
// # (35)
0x14, 0x3e, 0x14, 0x3e, 0x14,
// $ (36)
0x24, 0x2a, 0x7f, 0x2a, 0x12,
// % (37)
0x23, 0x13, 0x08, 0x64, 0x62,
// & (38)
0x36, 0x49, 0x55, 0x22, 0x50,
// ' (39)
0x00, 0x05, 0x03, 0x00, 0x00,
// ( (40)
0x00, 0x1c, 0x22, 0x41, 0x00,
// ) (41)
0x00, 0x41, 0x22, 0x1c, 0x00,
// * (42)
0x14, 0x08, 0x3e, 0x08, 0x14,
// + (43)
0x08, 0x08, 0x3e, 0x08, 0x08,
// , (44)
0x00, 0x50, 0x30, 0x00, 0x00,
// - (45)
0x08, 0x08, 0x08, 0x08, 0x08,
// . (46)
0x00, 0x30, 0x30, 0x00, 0x00,
// / (47)
0x20, 0x10, 0x08, 0x04, 0x02,
// 0 (48)
0x3e, 0x51, 0x49, 0x45, 0x3e,
// 1 (49)
0x00, 0x42, 0x7f, 0x40, 0x00,
// 2 (50)
0x42, 0x61, 0x51, 0x49, 0x46,
// 3 (51)
0x21, 0x41, 0x45, 0x4b, 0x31,
// 4 (52)
0x18, 0x14, 0x12, 0x7f, 0x10,
// 5 (53)
0x27, 0x45, 0x45, 0x45, 0x39,
// 6 (54)
0x3c, 0x4a, 0x49, 0x49, 0x30,
// 7 (55)
0x01, 0x71, 0x09, 0x05, 0x03,
// 8 (56)
0x36, 0x49, 0x49, 0x49, 0x36,
// 9 (57)
0x06, 0x49, 0x49, 0x29, 0x1e,
// : (58)
0x00, 0x36, 0x36, 0x00, 0x00,
// ; (59)
0x00, 0x56, 0x36, 0x00, 0x00,
// < (60)
0x08, 0x14, 0x22, 0x41, 0x00,
// = (61)
0x14, 0x14, 0x14, 0x14, 0x14,
// > (62)
0x00, 0x41, 0x22, 0x14, 0x08,
// ? (63)
0x02, 0x01, 0x51, 0x09, 0x06,
// @ (64)
0x32, 0x49, 0x79, 0x41, 0x3e,
// A (65)
0x7e, 0x11, 0x11, 0x11, 0x7e,
// B (66)
0x7f, 0x49, 0x49, 0x49, 0x36,
// C (67)
0x3e, 0x41, 0x41, 0x41, 0x22,
// D (68)
0x7f, 0x41, 0x41, 0x22, 0x1c,
// E (69)
0x7f, 0x49, 0x49, 0x49, 0x41,
// F (70)
0x7f, 0x09, 0x09, 0x09, 0x01,
// G (71)
0x3e, 0x41, 0x49, 0x49, 0x7a,
// H (72)
0x7f, 0x08, 0x08, 0x08, 0x7f,
// I (73)
0x00, 0x41, 0x7f, 0x41, 0x00,
// J (74)
0x20, 0x40, 0x41, 0x3f, 0x01,
// K (75)
0x7f, 0x08, 0x14, 0x22, 0x41,
// L (76)
0x7f, 0x40, 0x40, 0x40, 0x40,
// M (77)
0x7f, 0x02, 0x0c, 0x02, 0x7f,
// N (78)
0x7f, 0x04, 0x08, 0x10, 0x7f,
// O (79)
0x3e, 0x41, 0x41, 0x41, 0x3e,
// P (80)
0x7f, 0x09, 0x09, 0x09, 0x06,
// Q (81)
0x3e, 0x41, 0x51, 0x21, 0x5e,
// R (82)
0x7f, 0x09, 0x19, 0x29, 0x46,
// S (83)
0x46, 0x49, 0x49, 0x49, 0x31,
// T (84)
0x01, 0x01, 0x7f, 0x01, 0x01,
// U (85)
0x3f, 0x40, 0x40, 0x40, 0x3f,
// V (86)
0x1f, 0x20, 0x40, 0x20, 0x1f,
// W (87)
0x3f, 0x40, 0x38, 0x40, 0x3f,
// X (88)
0x63, 0x14, 0x08, 0x14, 0x63,
// Y (89)
0x07, 0x08, 0x70, 0x08, 0x07,
// Z (90)
0x61, 0x51, 0x49, 0x45, 0x43,
// [ (91)
0x00, 0x7f, 0x41, 0x41, 0x00,
// \ (92)
0x02, 0x04, 0x08, 0x10, 0x20,
// ] (93)
0x00, 0x41, 0x41, 0x7f, 0x00,
// ^ (94)
0x04, 0x02, 0x01, 0x02, 0x04,
// _ (95)
0x40, 0x40, 0x40, 0x40, 0x40,
// ` (96)
0x00, 0x01, 0x02, 0x04, 0x00,
// a (97)
0x20, 0x54, 0x54, 0x54, 0x78,
// b (98)
0x7f, 0x48, 0x44, 0x44, 0x38,
// c (99)
0x38, 0x44, 0x44, 0x44, 0x20,
// d (100)
0x38, 0x44, 0x44, 0x48, 0x7f,
// e (101)
0x38, 0x54, 0x54, 0x54, 0x18,
// f (102)
0x08, 0x7e, 0x09, 0x01, 0x02,
// g (103)
0x0c, 0x52, 0x52, 0x52, 0x3e,
// h (104)
0x7f, 0x08, 0x04, 0x04, 0x78,
// i (105)
0x00, 0x44, 0x7d, 0x40, 0x00,
// j (106)
0x20, 0x40, 0x44, 0x3d, 0x00,
// k (107)
0x7f, 0x10, 0x28, 0x44, 0x00,
// l (108)
0x00, 0x41, 0x7f, 0x40, 0x00,
// m (109)
0x7c, 0x04, 0x18, 0x04, 0x78,
// n (110)
0x7c, 0x08, 0x04, 0x04, 0x78,
// o (111)
0x38, 0x44, 0x44, 0x44, 0x38,
// p (112)
0x7c, 0x14, 0x14, 0x14, 0x08,
// q (113)
0x08, 0x14, 0x14, 0x18, 0x7c,
// r (114)
0x7c, 0x08, 0x04, 0x04, 0x08,
// s (115)
0x48, 0x54, 0x54, 0x54, 0x20,
// t (116)
0x04, 0x3f, 0x44, 0x40, 0x20,
// u (117)
0x3c, 0x40, 0x40, 0x20, 0x7c,
// v (118)
0x1c, 0x20, 0x40, 0x20, 0x1c,
// w (119)
0x3c, 0x40, 0x30, 0x40, 0x3c,
// x (120)
0x44, 0x28, 0x10, 0x28, 0x44,
// y (121)
0x0c, 0x50, 0x50, 0x50, 0x3c,
// z (122)
0x44, 0x64, 0x54, 0x4c, 0x44,
// { (123)
0x00, 0x08, 0x36, 0x41, 0x00,
// | (124)
0x00, 0x00, 0x7f, 0x00, 0x00,
// } (125)
0x00, 0x41, 0x36, 0x08, 0x00,
// ~ (126)
0x10, 0x08, 0x08, 0x10, 0x08,
]),
};
// Alias for compatibility
export const SimpleFont = Font_6x8;
+50
View File
@@ -0,0 +1,50 @@
/**
* Meshtastic OLED Emulator
*
* A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays.
* Provides the same OLEDDisplay API as the Meshtastic firmware, enabling
* screen development and testing in the browser.
*/
// Core display class
export {
OLEDDisplay,
type OLEDDISPLAY_GEOMETRY,
type OLEDDISPLAY_COLOR,
type OLEDDISPLAY_TEXT_ALIGNMENT,
type OLEDFont,
} from "./OLEDDisplay";
export {
WHITE,
BLACK,
INVERSE,
TEXT_ALIGN_LEFT,
TEXT_ALIGN_CENTER,
TEXT_ALIGN_RIGHT,
} from "./OLEDDisplay";
// Fonts
export {
ArialMT_Plain_10,
ArialMT_Plain_16,
FONT_HEIGHT_SMALL,
FONT_HEIGHT_MEDIUM,
FONT_HEIGHT_LARGE,
createFontFromBytes,
} from "./OLEDFonts";
// Simple working fonts (recommended)
export { Font_6x8, Font_5x7, SimpleFont } from "./SimpleFonts";
// Images and icons
export * as OLEDImages from "./OLEDImages";
// Screen renderers (firmware UI ports)
export * as ScreenRenderers from "./ScreenRenderers";
// Vue component
export { default as OLEDEmulator } from "./OLEDEmulator.vue";
export { DISPLAY_PRESETS, type DisplayPreset } from "./DisplayPresets";
// Demo component
export { default as OLEDEmulatorDemo } from "./Demo.vue";
+4
View File
@@ -0,0 +1,4 @@
import { createApp } from "vue";
import Demo from "./Demo.vue";
createApp(Demo).mount("#app");
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Vue */
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["node_modules", "dist"]
}
+35
View File
@@ -0,0 +1,35 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import dts from "vite-plugin-dts";
import { resolve } from "path";
export default defineConfig({
plugins: [
vue(),
dts({
insertTypesEntry: true,
}),
],
build: {
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "MeshtasticOLEDEmulator",
formats: ["es", "cjs"],
fileName: (format) => `index.${format === "es" ? "mjs" : "js"}`,
},
rollupOptions: {
external: ["vue"],
output: {
globals: {
vue: "Vue",
},
},
},
cssCodeSplit: false,
},
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
});
+14 -6
View File
@@ -50,12 +50,15 @@ build_flags = -Wno-missing-field-initializers
-DRADIOLIB_EXCLUDE_APRS=1
-DRADIOLIB_EXCLUDE_LORAWAN=1
-DMESHTASTIC_EXCLUDE_DROPZONE=1
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
-DMESHTASTIC_EXCLUDE_POWERMON=1
-DMESHTASTIC_EXCLUDE_STATUS=1
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
-DLED_BUILTIN=-1
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
#-D OLED_PL=1
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
@@ -65,7 +68,7 @@ monitor_speed = 115200
monitor_filters = direct
lib_deps =
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/b34c6817c25d6faabb3a8a162b5d14fb75395433.zip
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
@@ -119,7 +122,7 @@ lib_deps =
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/69739b84f87a91568d3c421498bc89977937a141.zip
https://github.com/meshtastic/device-ui/archive/6c75195e9987b7a49563232234f2f868dd343cae.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
@@ -143,7 +146,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
adafruit/Adafruit INA219@1.2.3
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
adafruit/Adafruit MPU6050@2.2.6
adafruit/Adafruit MPU6050@2.2.9
# 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
@@ -209,9 +212,12 @@ lib_deps =
# 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.2
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
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
[environmental_extra_no_bsec]
lib_deps =
@@ -236,6 +242,8 @@ lib_deps =
# 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.2
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
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
+12
View File
@@ -0,0 +1,12 @@
(import (
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
nodeName = lock.nodes.root.inputs.flake-compat;
in
fetchTarball {
url =
lock.nodes.${nodeName}.locked.url
or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
sha256 = lock.nodes.${nodeName}.locked.narHash;
}
) { src = ./.; }).shellNix
+66 -59
View File
@@ -1,19 +1,21 @@
#ifndef AMBIENTLIGHTINGTHREAD_H
#define AMBIENTLIGHTINGTHREAD_H
#include "Observer.h"
#include "configuration.h"
#include "detect/ScanI2C.h"
#include "sleep.h"
#ifdef HAS_NCP5623
#include <graphics/RAKled.h>
NCP5623 rgb;
#include <NCP5623.h>
#endif
#ifdef HAS_LP5562
#include <graphics/NomadStarLED.h>
LP5562 rgbw;
#endif
#ifdef HAS_NEOPIXEL
#include <graphics/NeoPixel.h>
Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
#include <Adafruit_NeoPixel.h>
#endif
#ifdef UNPHONE
@@ -21,10 +23,24 @@ Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
extern unPhone unphone;
#endif
namespace concurrency
{
class AmbientLightingThread : public concurrency::OSThread
{
friend class StatusLEDModule; // Let the LEDStatusModule trigger the ambient lighting for notifications and battery status.
friend class ExternalNotificationModule; // Let the ExternalNotificationModule trigger the ambient lighting for notifications.
private:
#ifdef HAS_NCP5623
NCP5623 rgb;
#endif
#ifdef HAS_LP5562
LP5562 rgbw;
#endif
#ifdef HAS_NEOPIXEL
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
#endif
public:
explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting")
{
@@ -36,14 +52,15 @@ class AmbientLightingThread : public concurrency::OSThread
moduleConfig.ambient_lighting.led_state = true;
#endif
#endif
// Uncomment to test module
// moduleConfig.ambient_lighting.led_state = true;
// moduleConfig.ambient_lighting.current = 10;
#if AMBIENT_LIGHTING_TEST
// define to enable test
moduleConfig.ambient_lighting.led_state = true;
moduleConfig.ambient_lighting.current = 10;
// Default to a color based on our node number
// moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
// moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
// moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
#endif
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
_type = type;
if (_type == ScanI2C::DeviceType::NONE) {
@@ -53,11 +70,6 @@ class AmbientLightingThread : public concurrency::OSThread
}
#endif
#ifdef HAS_RGB_LED
if (!moduleConfig.ambient_lighting.led_state) {
LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF");
disable();
return;
}
LOG_DEBUG("AmbientLighting init");
#ifdef HAS_NCP5623
if (_type == ScanI2C::NCP5623) {
@@ -77,7 +89,13 @@ class AmbientLightingThread : public concurrency::OSThread
pixels.clear(); // Set all pixel colors to 'off'
pixels.setBrightness(moduleConfig.ambient_lighting.current);
#endif
setLighting();
if (!moduleConfig.ambient_lighting.led_state) {
LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF");
disable();
return;
}
setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
#endif
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
}
@@ -91,7 +109,8 @@ class AmbientLightingThread : public concurrency::OSThread
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) {
#endif
setLighting();
setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
}
@@ -148,65 +167,53 @@ class AmbientLightingThread : public concurrency::OSThread
return 0;
}
void setLighting()
protected:
void setLighting(float current, uint8_t red, uint8_t green, uint8_t blue)
{
#ifdef HAS_NCP5623
rgb.setCurrent(moduleConfig.ambient_lighting.current);
rgb.setRed(moduleConfig.ambient_lighting.red);
rgb.setGreen(moduleConfig.ambient_lighting.green);
rgb.setBlue(moduleConfig.ambient_lighting.blue);
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d",
moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
rgb.setCurrent(current);
rgb.setRed(red);
rgb.setGreen(green);
rgb.setBlue(blue);
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
#endif
#ifdef HAS_LP5562
rgbw.setCurrent(moduleConfig.ambient_lighting.current);
rgbw.setRed(moduleConfig.ambient_lighting.red);
rgbw.setGreen(moduleConfig.ambient_lighting.green);
rgbw.setBlue(moduleConfig.ambient_lighting.blue);
LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current,
moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
rgbw.setCurrent(current);
rgbw.setRed(red);
rgbw.setGreen(green);
rgbw.setBlue(blue);
LOG_DEBUG("Init LP5562 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
#endif
#ifdef HAS_NEOPIXEL
pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
moduleConfig.ambient_lighting.blue),
0, NEOPIXEL_COUNT);
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
// RadioMaster Bandit has addressable LED at the two buttons
// this allow us to set different lighting for them in variant.h file.
#ifdef RADIOMASTER_900_BANDIT
#if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX)
pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1);
#endif
#if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX)
pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1);
#endif
#endif
pixels.show();
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
// moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
// moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%f, red=%d, green=%d, blue=%d",
// current, red, green, blue);
#endif
#ifdef RGBLED_CA
analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);
analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green);
analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue);
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
analogWrite(RGBLED_RED, 255 - red);
analogWrite(RGBLED_GREEN, 255 - green);
analogWrite(RGBLED_BLUE, 255 - blue);
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", red, green, blue);
#elif defined(RGBLED_RED)
analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red);
analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green);
analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue);
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
analogWrite(RGBLED_RED, red);
analogWrite(RGBLED_GREEN, green);
analogWrite(RGBLED_BLUE, blue);
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", red, green, blue);
#endif
#ifdef UNPHONE
unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
moduleConfig.ambient_lighting.blue);
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
unphone.rgb(red, green, blue);
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", red, green, blue);
#endif
}
};
} // namespace concurrency
#endif // AMBIENTLIGHTINGTHREAD_H
+2 -10
View File
@@ -89,22 +89,14 @@ class BluetoothStatus : public Status
case ConnectionState::CONNECTED:
LOG_DEBUG("BluetoothStatus CONNECTED");
#ifdef BLE_LED
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, LOW);
#else
digitalWrite(BLE_LED, HIGH);
#endif
digitalWrite(BLE_LED, LED_STATE_ON);
#endif
break;
case ConnectionState::DISCONNECTED:
LOG_DEBUG("BluetoothStatus DISCONNECTED");
#ifdef BLE_LED
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
digitalWrite(BLE_LED, LED_STATE_OFF);
#endif
break;
}
-66
View File
@@ -1,66 +0,0 @@
#include "Led.h"
#include "PowerMon.h"
#include "main.h"
#include "power.h"
GpioVirtPin ledForceOn, ledBlink;
#if defined(LED_PIN)
// Most boards have a GPIO for LED control
static GpioHwPin ledRawHwPin(LED_PIN);
#else
static GpioVirtPin ledRawHwPin; // Dummy pin for no hardware
#endif
#if LED_STATE_ON == 0
static GpioVirtPin ledHwPin;
static GpioNotTransformer ledInverter(&ledHwPin, &ledRawHwPin);
#else
static GpioPin &ledHwPin = ledRawHwPin;
#endif
#if defined(HAS_PMU)
/**
* A GPIO controlled by the PMU
*/
class GpioPmuPin : public GpioPin
{
public:
void set(bool value)
{
if (pmu_found && PMU) {
// blink the axp led
PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
}
}
} ledPmuHwPin;
// In some cases we need to drive a PMU LED and a normal LED
static GpioSplitter ledFinalPin(&ledHwPin, &ledPmuHwPin);
#else
static GpioPin &ledFinalPin = ledHwPin;
#endif
#ifdef USE_POWERMON
/**
* We monitor changes to the LED drive output because we use that as a sanity test in our power monitor stuff.
*/
class MonitoredLedPin : public GpioPin
{
public:
void set(bool value)
{
if (powerMon) {
if (value)
powerMon->setState(meshtastic_PowerMon_State_LED_On);
else
powerMon->clearState(meshtastic_PowerMon_State_LED_On);
}
ledFinalPin.set(value);
}
} monitoredLedPin;
#else
static GpioPin &monitoredLedPin = ledFinalPin;
#endif
static GpioBinaryTransformer ledForcer(&ledForceOn, &ledBlink, &monitoredLedPin, GpioBinaryTransformer::Or);
-7
View File
@@ -1,7 +0,0 @@
#include "GpioLogic.h"
#include "configuration.h"
/**
* ledForceOn and ledForceOff both override the normal ledBlinker behavior (which is controlled by main)
*/
extern GpioVirtPin ledForceOn, ledBlink;
+96 -8
View File
@@ -459,6 +459,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
}
// if it's not HIGH - check the battery
#endif
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
return false;
// technically speaking this should work for all(?) NRF52 boards
// but needs testing across multiple devices. NRF52 USB would not even work if
@@ -690,7 +692,9 @@ bool Power::setup()
bool found = false;
if (axpChipInit()) {
found = true;
} else if (lipoInit()) {
} else if (cw2015Init()) {
found = true;
} else if (max17048Init()) {
found = true;
} else if (lipoChargerInit()) {
found = true;
@@ -816,6 +820,9 @@ void Power::shutdown()
#endif
#ifdef PIN_LED3
ledOff(PIN_LED3);
#endif
#ifdef LED_NOTIFICATION
ledOff(LED_NOTIFICATION);
#endif
doDeepSleep(DELAY_FOREVER, true, true);
#elif defined(ARCH_PORTDUINO)
@@ -1316,7 +1323,7 @@ bool Power::axpChipInit()
/**
* Wrapper class for an I2C MAX17048 Lipo battery sensor.
*/
class LipoBatteryLevel : public HasBatteryLevel
class MAX17048BatteryLevel : public HasBatteryLevel
{
private:
MAX17048Singleton *max17048 = nullptr;
@@ -1364,18 +1371,18 @@ class LipoBatteryLevel : public HasBatteryLevel
virtual bool isCharging() override { return max17048->isBatteryCharging(); }
};
LipoBatteryLevel lipoLevel;
MAX17048BatteryLevel max17048Level;
/**
* Init the Lipo battery level sensor
*/
bool Power::lipoInit()
bool Power::max17048Init()
{
bool result = lipoLevel.runOnce();
LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet");
bool result = max17048Level.runOnce();
LOG_DEBUG("Power::max17048Init lipo sensor is %s", result ? "ready" : "not ready yet");
if (!result)
return false;
batteryLevel = &lipoLevel;
batteryLevel = &max17048Level;
return true;
}
@@ -1383,7 +1390,88 @@ bool Power::lipoInit()
/**
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
*/
bool Power::lipoInit()
bool Power::max17048Init()
{
return false;
}
#endif
#if !MESHTASTIC_EXCLUDE_I2C && HAS_CW2015
class CW2015BatteryLevel : public AnalogBatteryLevel
{
public:
/**
* Battery state of charge, from 0 to 100 or -1 for unknown
*/
virtual int getBatteryPercent() override
{
int data = -1;
Wire.beginTransmission(CW2015_ADDR);
Wire.write(0x04);
if (Wire.endTransmission() == 0) {
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
data = Wire.read();
}
}
return data;
}
/**
* The raw voltage of the battery in millivolts, or NAN if unknown
*/
virtual uint16_t getBattVoltage() override
{
uint16_t mv = 0;
Wire.beginTransmission(CW2015_ADDR);
Wire.write(0x02);
if (Wire.endTransmission() == 0) {
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)2)) {
mv = Wire.read();
mv <<= 8;
mv |= Wire.read();
// Voltage is read in 305uV units, convert to mV
mv = mv * 305 / 1000;
}
}
return mv;
}
};
CW2015BatteryLevel cw2015Level;
/**
* Init the CW2015 battery level sensor
*/
bool Power::cw2015Init()
{
Wire.beginTransmission(CW2015_ADDR);
uint8_t getInfo[] = {0x0a, 0x00};
Wire.write(getInfo, 2);
Wire.endTransmission();
delay(10);
Wire.beginTransmission(CW2015_ADDR);
Wire.write(0x00);
bool result = false;
if (Wire.endTransmission() == 0) {
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
uint8_t data = Wire.read();
LOG_DEBUG("CW2015 init read data: 0x%x", data);
if (data == 0x73) {
result = true;
batteryLevel = &cw2015Level;
}
}
}
return result;
}
#else
/**
* The CW2015 battery level sensor is unavailable - default to AnalogBatteryLevel
*/
bool Power::cw2015Init()
{
return false;
}
+4 -4
View File
@@ -9,13 +9,13 @@
*/
#include "PowerFSM.h"
#include "Default.h"
#include "Led.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PowerMon.h"
#include "configuration.h"
#include "graphics/Screen.h"
#include "main.h"
#include "modules/StatusLEDModule.h"
#include "sleep.h"
#include "target_specific.h"
@@ -103,7 +103,7 @@ static void lsIdle()
uint32_t sleepTime = SLEEP_TIME;
powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep);
ledBlink.set(false); // Never leave led on while in light sleep
statusLEDModule->setPowerLED(false);
esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL);
powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep);
@@ -111,7 +111,7 @@ static void lsIdle()
case ESP_SLEEP_WAKEUP_TIMER:
// Normal case: timer expired, we should just go back to sleep ASAP
ledBlink.set(true); // briefly turn on led
statusLEDModule->setPowerLED(true);
wakeCause2 = doLightSleep(100); // leave led on for 1ms
secsSlept += sleepTime;
@@ -146,7 +146,7 @@ static void lsIdle()
}
} else {
// Time to stop sleeping!
ledBlink.set(false);
statusLEDModule->setPowerLED(false);
LOG_INFO("Reached ls_secs, service loop()");
powerFSM.trigger(EVENT_WAKE_TIMER);
}
+10 -6
View File
@@ -217,6 +217,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define SHTC3_ADDR 0x70
#define LPS22HB_ADDR 0x5C
#define LPS22HB_ADDR_ALT 0x5D
#define SFA30_ADDR 0x5D
#define SHT31_4x_ADDR 0x44
#define SHT31_4x_ADDR_ALT 0x45
#define PMSA003I_ADDR 0x12
@@ -233,6 +234,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define NAU7802_ADDR 0x2A
#define MAX30102_ADDR 0x57
#define SCD4X_ADDR 0x62
#define CW2015_ADDR 0x62
#define MLX90614_ADDR_DEF 0x5A
#define CGRADSENS_ADDR 0x66
#define LTR390UV_ADDR 0x53
@@ -241,6 +243,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
#define BQ25896_ADDR 0x6B
#define LTR553ALS_ADDR 0x23
#define SEN5X_ADDR 0x69
// -----------------------------------------------------------------------------
// ACCELEROMETER
@@ -390,9 +393,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifndef HAS_RADIO
#define HAS_RADIO 0
#endif
#ifndef HAS_RTC
#define HAS_RTC 0
#endif
#ifndef HAS_CPU_SHUTDOWN
#define HAS_CPU_SHUTDOWN 0
#endif
@@ -428,12 +428,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define HAS_RGB_LED
#endif
#ifndef LED_STATE_OFF
#define LED_STATE_OFF 0
#endif
#ifndef LED_STATE_ON
#define LED_STATE_ON 1
#endif
#ifndef LED_STATE_OFF
#define LED_STATE_OFF (LED_STATE_ON ^ 1)
#endif
#ifndef ledOff
#define ledOff(pin) pinMode(pin, INPUT)
#endif
// default mapping of pins
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)
+2 -2
View File
@@ -43,8 +43,8 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
ScanI2C::FoundDevice ScanI2C::firstAQI() const
{
ScanI2C::DeviceType types[] = {PMSA003I, SCD4X};
return firstOfOrNONE(2, types);
ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X, SFA30};
return firstOfOrNONE(4, types);
}
ScanI2C::FoundDevice ScanI2C::firstRGBLED() const
+4 -1
View File
@@ -88,7 +88,10 @@ class ScanI2C
BH1750,
DA217,
CHSC6X,
CST226SE
CST226SE,
SEN5X,
SFA30,
CW2015
} DeviceType;
// typedef uint8_t DeviceAddress;
+86 -9
View File
@@ -1,4 +1,6 @@
#include "ScanI2CTwoWire.h"
#include "configuration.h"
#include "detect/ScanI2C.h"
#if !MESHTASTIC_EXCLUDE_I2C
@@ -8,6 +10,7 @@
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
#include "meshUtils.h" // vformat
#endif
bool in_array(uint8_t *array, int size, uint8_t lookfor)
@@ -114,6 +117,45 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
return value;
}
/// 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
// detection will not work
String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
{
uint8_t cmd[] = {0xD0, 0x14};
uint8_t response[48] = {0};
i2cBus->beginTransmission(address);
i2cBus->write(cmd, 2);
if (i2cBus->endTransmission() != 0)
return "";
delay(20);
if (i2cBus->requestFrom(address, (uint8_t)48) != 48)
return "";
for (int i = 0; i < 48 && i2cBus->available(); ++i) {
response[i] = i2cBus->read();
}
char productName[33] = {0};
int j = 0;
for (int i = 0; i < 48 && j < 32; i += 3) {
if (response[i] >= 32 && response[i] <= 126)
productName[j++] = response[i];
else
break;
if (response[i + 1] >= 32 && response[i + 1] <= 126)
productName[j++] = response[i + 1];
else
break;
}
return String(productName);
}
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
case ADDR: \
logFoundDevice(__VA_ARGS__); \
@@ -416,6 +458,13 @@ 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) {
type = SFA30;
logFoundDevice("SFA30", (uint8_t)addr.address);
break;
}
// TODO - What happens with these two?
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
@@ -541,7 +590,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break;
SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address);
case SCD4X_ADDR: {
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x8), 1);
if (registerValue == 0x18) {
logFoundDevice("CW2015", (uint8_t)addr.address);
type = CW2015;
} else {
logFoundDevice("SCD4X", (uint8_t)addr.address);
type = SCD4X;
}
break;
}
SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address);
#ifdef HAS_TPS65233
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
@@ -568,8 +627,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
case ICM20948_ADDR: // same as BMX160_ADDR
case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
// ICM20948 Register check
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
#ifdef HAS_ICM20948
type = ICM20948;
@@ -580,14 +640,31 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = ICM20948;
logFoundDevice("ICM20948", (uint8_t)addr.address);
break;
} else if (addr.address == BMX160_ADDR) {
type = BMX160;
logFoundDevice("BMX160", (uint8_t)addr.address);
break;
} else {
type = MPU6050;
logFoundDevice("MPU6050", (uint8_t)addr.address);
break;
String prod = "";
prod = readSEN5xProductName(i2cBus, addr.address);
if (prod.startsWith("SEN55")) {
type = SEN5X;
logFoundDevice("Sensirion SEN55", addr.address);
break;
} else if (prod.startsWith("SEN54")) {
type = SEN5X;
logFoundDevice("Sensirion SEN54", addr.address);
break;
} else if (prod.startsWith("SEN50")) {
type = SEN5X;
logFoundDevice("Sensirion SEN50", addr.address);
break;
}
if (addr.address == BMX160_ADDR) {
type = BMX160;
logFoundDevice("BMX160", (uint8_t)addr.address);
break;
} else {
type = MPU6050;
logFoundDevice("MPU6050", (uint8_t)addr.address);
break;
}
}
break;
+31
View File
@@ -0,0 +1,31 @@
#include "reClockI2C.h"
#include "ScanI2CTwoWire.h"
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force)
{
uint32_t currentClock = 0;
/* See https://github.com/arduino/Arduino/issues/11457
Currently, only ESP32 can getClock()
While all cores can setClock()
https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
we need to reclock I2C and set it back to the previous desired speed.
Only for cases where we can know OR predefine the speed, we can do this.
*/
// TODO add getClock function or return a predefined clock speed per variant?
#ifdef CAN_RECLOCK_I2C
currentClock = i2cBus->getClock();
#endif
if ((currentClock != desiredClock) || force) {
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
i2cBus->setClock(desiredClock);
}
return currentClock;
}
+7 -37
View File
@@ -1,41 +1,11 @@
#ifdef CAN_RECLOCK_I2C
#ifndef RECLOCK_I2C_
#define RECLOCK_I2C_
#include "ScanI2CTwoWire.h"
#include <Wire.h>
#include <stdint.h>
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus)
{
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force);
uint32_t currentClock;
/* See https://github.com/arduino/Arduino/issues/11457
Currently, only ESP32 can getClock()
While all cores can setClock()
https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
we need to reclock I2C and set it back to the previous desired speed.
Only for cases where we can know OR predefine the speed, we can do this.
*/
#ifdef ARCH_ESP32
currentClock = i2cBus->getClock();
#elif defined(ARCH_NRF52)
// TODO add getClock function or return a predefined clock speed per variant?
return 0;
#elif defined(ARCH_RP2040)
// TODO add getClock function or return a predefined clock speed per variant
return 0;
#elif defined(ARCH_STM32WL)
// TODO add getClock function or return a predefined clock speed per variant
return 0;
#else
return 0;
#endif
if (currentClock != desiredClock) {
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
i2cBus->setClock(desiredClock);
}
return currentClock;
}
#endif
+6 -6
View File
@@ -72,11 +72,13 @@ RTCSetResult readFromRTC()
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
SensorPCF8563 rtc;
#elif defined(PCF85063_RTC)
if (rtc_found.address == PCF85063_RTC) {
SensorPCF85063 rtc;
#endif
uint32_t now = millis();
SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
@@ -240,10 +242,12 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
SensorPCF8563 rtc;
#elif defined(PCF85063_RTC)
if (rtc_found.address == PCF85063_RTC) {
SensorPCF85063 rtc;
#endif
SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
@@ -276,11 +280,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
settimeofday(tv, NULL);
#endif
// nrf52 doesn't have a readable RTC (yet - software not written)
#if HAS_RTC
readFromRTC();
#endif
return RTCSetResultSuccess;
} else {
return RTCSetResultNotSet; // RTC was already set with a higher quality time
-4
View File
@@ -1,4 +0,0 @@
#ifdef HAS_NEOPIXEL
#include <Adafruit_NeoPixel.h>
extern Adafruit_NeoPixel pixels;
#endif
-5
View File
@@ -1,5 +0,0 @@
#ifdef HAS_NCP5623
#include <NCP5623.h>
extern NCP5623 rgb;
#endif
+22 -29
View File
@@ -221,7 +221,6 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
if (rtc_sec > 0) {
// === Build Time String ===
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
int hour, minute, second;
graphics::decomposeTime(rtc_sec, hour, minute, second);
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
@@ -428,39 +427,33 @@ const int *getTextPositions(OLEDDisplay *display)
// *************************
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
{
bool drawConnectionState = false;
if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI ||
service->api_state == service->STATE_SERIAL || service->api_state == service->STATE_PACKET ||
service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) {
drawConnectionState = true;
}
if (!isAPIConnected(service->api_state))
return;
if (drawConnectionState) {
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
display->setColor(BLACK);
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
(connection_icon_height * scale) + (2 * scale));
display->setColor(WHITE);
if (currentResolution == ScreenResolution::High) {
const int bytesPerRow = (connection_icon_width + 7) / 8;
int iconX = 0;
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
display->setColor(BLACK);
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
(connection_icon_height * scale) + (2 * scale));
display->setColor(WHITE);
if (currentResolution == ScreenResolution::High) {
const int bytesPerRow = (connection_icon_width + 7) / 8;
int iconX = 0;
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
for (int yy = 0; yy < connection_icon_height; ++yy) {
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
for (int xx = 0; xx < connection_icon_width; ++xx) {
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
if (byteVal & bitMask) {
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
}
for (int yy = 0; yy < connection_icon_height; ++yy) {
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
for (int xx = 0; xx < connection_icon_width; ++xx) {
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
if (byteVal & bitMask) {
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
}
}
} else {
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
connection_icon);
}
} else {
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
connection_icon);
}
}
+14
View File
@@ -63,4 +63,18 @@ bool isAllowedPunctuation(char c);
std::string sanitizeString(const std::string &input);
static inline bool isAPIConnected(uint8_t state)
{
static constexpr bool connectedStates[] = {
/* STATE_NONE */ false,
/* STATE_BLE */ true,
/* STATE_WIFI */ true,
/* STATE_SERIAL */ true,
/* STATE_PACKET */ true,
/* STATE_HTTP */ true,
/* STATE_ETH */ true,
};
return state < sizeof(connectedStates) ? connectedStates[state] : false;
}
} // namespace graphics
+6 -6
View File
@@ -110,14 +110,14 @@ void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, ui
uint32_t secs = (uptimeMillis % 60000) / 1000;
if (days) {
snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours);
snprintf(uptimeStr, maxLength, "%s%ud %uh", prefix, days, hours);
} else if (hours) {
snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins);
snprintf(uptimeStr, maxLength, "%s%uh %um", prefix, hours, mins);
} else if (!includeSecs) {
snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins);
snprintf(uptimeStr, maxLength, "%s%um", prefix, mins);
} else if (mins) {
snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs);
snprintf(uptimeStr, maxLength, "%s%um %us", prefix, mins, secs);
} else {
snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs);
snprintf(uptimeStr, maxLength, "%s%us", prefix, secs);
}
}
}
+11 -3
View File
@@ -429,6 +429,10 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
c = c - 'a' + 'A';
}
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
// Show the common "/" pairing next to "?" like on a real keyboard
if (key.type == VK_CHAR && key.character == '?') {
keyText = "?/";
}
}
int textWidth = display->getStringWidth(keyText.c_str());
@@ -518,9 +522,13 @@ char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
char c = key.character;
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
if (isLongPress && c >= 'a' && c <= 'z') {
c = (char)(c - 'a' + 'A');
// Long-press: letters become uppercase; for "?" provide "/" like a typical keyboard
if (isLongPress) {
if (c >= 'a' && c <= 'z') {
c = (char)(c - 'a' + 'A');
} else if (c == '?') {
c = '/';
}
}
return c;
+1 -1
View File
@@ -663,7 +663,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it
char uptimeStr[32] = "";
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr));
textWidth = display->getStringWidth(uptimeStr);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], uptimeStr);
+169 -132
View File
@@ -58,7 +58,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
} // namespace
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
uint32_t menuHandler::pickedNodeNum = 0;
bool test_enabled = false;
uint8_t test_count = 0;
@@ -66,7 +66,7 @@ uint8_t test_count = 0;
void menuHandler::loraMenu()
{
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"};
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, frequency_slot = 3, lora_picker = 4 };
enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "LoRa Actions";
bannerOptions.optionsArrayPtr = optionsArray;
@@ -74,14 +74,14 @@ void menuHandler::loraMenu()
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
// No action
} else if (selected == device_role_picker) {
menuHandler::menuQueue = menuHandler::device_role_picker;
} else if (selected == radio_preset_picker) {
menuHandler::menuQueue = menuHandler::radio_preset_picker;
} else if (selected == frequency_slot) {
menuHandler::menuQueue = menuHandler::frequency_slot;
} else if (selected == lora_picker) {
menuHandler::menuQueue = menuHandler::lora_picker;
} else if (selected == DeviceRolePicker) {
menuHandler::menuQueue = menuHandler::DeviceRolePicker;
} else if (selected == RadioPresetPicker) {
menuHandler::menuQueue = menuHandler::RadioPresetPicker;
} else if (selected == FrequencySlot) {
menuHandler::menuQueue = menuHandler::FrequencySlot;
} else if (selected == LoraPicker) {
menuHandler::menuQueue = menuHandler::LoraPicker;
}
};
screen->showOverlayBanner(bannerOptions);
@@ -102,7 +102,7 @@ void menuHandler::OnboardMessage()
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
menuHandler::menuQueue = menuHandler::no_timeout_lora_picker;
menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker;
screen->runNow();
};
screen->showOverlayBanner(bannerOptions);
@@ -216,7 +216,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::DeviceRolePicker()
void menuHandler::deviceRolePicker()
{
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
enum optionsNumbers {
@@ -232,7 +232,7 @@ void menuHandler::DeviceRolePicker()
bannerOptions.optionsCount = 5;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
menuHandler::menuQueue = menuHandler::LoraMenu;
screen->runNow();
return;
} else if (selected == devicerole_client) {
@@ -300,7 +300,7 @@ void menuHandler::FrequencySlotPicker()
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
menuHandler::menuQueue = menuHandler::LoraMenu;
screen->runNow();
return;
}
@@ -313,7 +313,7 @@ void menuHandler::FrequencySlotPicker()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::RadioPresetPicker()
void menuHandler::radioPresetPicker()
{
static const RadioPresetOption presetOptions[] = {
{"Back", OptionsAction::Back},
@@ -333,7 +333,7 @@ void menuHandler::RadioPresetPicker()
auto bannerOptions =
createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
menuHandler::menuQueue = menuHandler::LoraMenu;
screen->runNow();
return;
}
@@ -352,7 +352,7 @@ void menuHandler::RadioPresetPicker()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::TwelveHourPicker()
void menuHandler::twelveHourPicker()
{
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 };
@@ -362,7 +362,7 @@ void menuHandler::TwelveHourPicker()
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::clock_menu;
menuHandler::menuQueue = menuHandler::ClockMenu;
screen->runNow();
} else if (selected == twelve) {
config.display.use_12h_clock = true;
@@ -390,7 +390,7 @@ void menuHandler::showConfirmationBanner(const char *message, std::function<void
screen->showOverlayBanner(confirmBanner);
}
void menuHandler::ClockFacePicker()
void menuHandler::clockFacePicker()
{
static const ClockFaceOption clockFaceOptions[] = {
{"Back", OptionsAction::Back},
@@ -404,7 +404,7 @@ void menuHandler::ClockFacePicker()
auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels,
[](const ClockFaceOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuHandler::menuQueue = menuHandler::clock_menu;
menuHandler::menuQueue = menuHandler::ClockMenu;
screen->runNow();
return;
}
@@ -456,7 +456,7 @@ void menuHandler::TZPicker()
auto bannerOptions = createStaticBannerOptions(
"Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuHandler::menuQueue = menuHandler::clock_menu;
menuHandler::menuQueue = menuHandler::ClockMenu;
screen->runNow();
return;
}
@@ -503,13 +503,13 @@ void menuHandler::clockMenu()
bannerOptions.optionsCount = 4;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Clock) {
menuHandler::menuQueue = menuHandler::clock_face_picker;
menuHandler::menuQueue = menuHandler::ClockFacePicker;
screen->runNow();
} else if (selected == Time) {
menuHandler::menuQueue = menuHandler::twelve_hour_picker;
menuHandler::menuQueue = menuHandler::TwelveHourPicker;
screen->runNow();
} else if (selected == Timezone) {
menuHandler::menuQueue = menuHandler::TZ_picker;
menuHandler::menuQueue = menuHandler::TzPicker;
screen->runNow();
}
};
@@ -572,12 +572,12 @@ void menuHandler::messageResponseMenu()
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
if (selected == ViewMode) {
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
menuHandler::menuQueue = menuHandler::MessageViewModeMenu;
screen->runNow();
// Reply submenu
} else if (selected == ReplyMenu) {
menuHandler::menuQueue = menuHandler::reply_menu;
menuHandler::menuQueue = menuHandler::ReplyMenu;
screen->runNow();
} else if (selected == MuteChannel) {
@@ -589,7 +589,7 @@ void menuHandler::messageResponseMenu()
}
} else if (selected == DeleteMenu) {
menuHandler::menuQueue = menuHandler::delete_messages_menu;
menuHandler::menuQueue = menuHandler::DeleteMessagesMenu;
screen->runNow();
#ifdef HAS_I2S
@@ -649,7 +649,7 @@ void menuHandler::replyMenu()
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::message_response_menu;
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
screen->runNow();
return;
}
@@ -737,7 +737,7 @@ void menuHandler::deleteMessagesMenu()
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::message_response_menu;
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
screen->runNow();
return;
}
@@ -901,7 +901,7 @@ void menuHandler::messageViewModeMenu()
bannerOptions.bannerCallback = [=](int selected) -> void {
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
if (selected == -1) {
menuHandler::menuQueue = menuHandler::message_response_menu;
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
screen->runNow();
} else if (selected == -2) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
@@ -1083,23 +1083,23 @@ void menuHandler::systemBaseMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Notifications) {
menuHandler::menuQueue = menuHandler::buzzermodemenupicker;
menuHandler::menuQueue = menuHandler::BuzzerModeMenuPicker;
screen->runNow();
} else if (selected == ScreenOptions) {
menuHandler::menuQueue = menuHandler::screen_options_menu;
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
screen->runNow();
} else if (selected == PowerMenu) {
menuHandler::menuQueue = menuHandler::power_menu;
menuHandler::menuQueue = menuHandler::PowerMenu;
screen->runNow();
} else if (selected == Test) {
menuHandler::menuQueue = menuHandler::test_menu;
menuHandler::menuQueue = menuHandler::TestMenu;
screen->runNow();
} else if (selected == Bluetooth) {
menuQueue = bluetooth_toggle_menu;
menuQueue = BluetoothToggleMenu;
screen->runNow();
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
} else if (selected == WiFiToggle) {
menuQueue = wifi_toggle_menu;
menuQueue = WifiToggleMenu;
screen->runNow();
#endif
} else if (selected == Back && !test_enabled) {
@@ -1177,7 +1177,7 @@ void menuHandler::favoriteBaseMenu()
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
screen->handleUIFrameEvent(&evt);
} else if (selected == Remove) {
menuHandler::menuQueue = menuHandler::remove_favorite;
menuHandler::menuQueue = menuHandler::RemoveFavorite;
screen->runNow();
} else if (selected == TraceRoute) {
if (traceRouteModule) {
@@ -1238,15 +1238,15 @@ void menuHandler::positionBaseMenu()
auto action = static_cast<PositionAction>(option.value);
switch (action) {
case PositionAction::GpsToggle:
menuQueue = gps_toggle_menu;
menuQueue = GpsToggleMenu;
screen->runNow();
break;
case PositionAction::GpsFormat:
menuQueue = gps_format_menu;
menuQueue = GpsFormatMenu;
screen->runNow();
break;
case PositionAction::CompassMenu:
menuQueue = compass_point_north_menu;
menuQueue = CompassPointNorthMenu;
screen->runNow();
break;
case PositionAction::CompassCalibrate:
@@ -1255,15 +1255,15 @@ void menuHandler::positionBaseMenu()
}
break;
case PositionAction::GPSSmartPosition:
menuQueue = gps_smart_position_menu;
menuQueue = GpsSmartPositionMenu;
screen->runNow();
break;
case PositionAction::GPSUpdateInterval:
menuQueue = gps_update_interval_menu;
menuQueue = GpsUpdateIntervalMenu;
screen->runNow();
break;
case PositionAction::GPSPositionBroadcast:
menuQueue = gps_position_broadcast_menu;
menuQueue = GpsPositionBroadcastMenu;
screen->runNow();
break;
}
@@ -1303,13 +1303,13 @@ void menuHandler::nodeListMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == NodePicker) {
menuQueue = NodePicker_menu;
menuQueue = NodePickerMenu;
screen->runNow();
} else if (selected == Reset) {
menuQueue = reset_node_db_menu;
menuQueue = ResetNodeDbMenu;
screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
menuHandler::menuQueue = menuHandler::NodeNameLengthMenu;
screen->runNow();
}
};
@@ -1330,12 +1330,12 @@ void menuHandler::NodePicker()
menuHandler::pickedNodeNum = nodenum;
// Keep UI favorite context in sync (used elsewhere for some node-based actions)
graphics::UIRenderer::currentFavoriteNodeNum = nodenum;
menuQueue = Manage_Node_menu;
menuQueue = ManageNodeMenu;
screen->runNow();
});
}
void menuHandler::ManageNodeMenu()
void menuHandler::manageNodeMenu()
{
// If we don't have a node selected yet, go fast exit
auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
@@ -1391,7 +1391,7 @@ void menuHandler::ManageNodeMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuQueue = node_base_menu;
menuQueue = NodeBaseMenu;
screen->runNow();
return;
}
@@ -1483,7 +1483,7 @@ void menuHandler::nodeNameLengthMenu()
auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels,
[](const NodeNameOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = node_base_menu;
menuQueue = NodeBaseMenu;
screen->runNow();
return;
}
@@ -1498,6 +1498,7 @@ void menuHandler::nodeNameLengthMenu()
config.display.use_long_node_name = option.value;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
LOG_INFO("Setting names to %s", option.value ? "long" : "short");
});
@@ -1528,7 +1529,7 @@ void menuHandler::resetNodeDBMenu()
nodeDB->resetNodes(1);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
} else if (selected == 0) {
menuQueue = node_base_menu;
menuQueue = NodeBaseMenu;
screen->runNow();
}
};
@@ -1550,7 +1551,7 @@ void menuHandler::compassNorthMenu()
auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels,
[](const CompassOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
return;
}
@@ -1595,7 +1596,7 @@ void menuHandler::GPSToggleMenu()
auto bannerOptions =
createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
return;
}
@@ -1660,7 +1661,7 @@ void menuHandler::GPSFormatMenu()
auto onSelection = [](const GPSFormatOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
return;
}
@@ -1715,7 +1716,7 @@ void menuHandler::GPSSmartPositionMenu()
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
} else if (selected == 1) {
config.position.position_broadcast_smart_enabled = true;
@@ -1744,7 +1745,7 @@ void menuHandler::GPSUpdateIntervalMenu()
bannerOptions.optionsCount = 16;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
} else if (selected == 1) {
config.position.gps_update_interval = 8;
@@ -1832,7 +1833,7 @@ void menuHandler::GPSPositionBroadcastMenu()
bannerOptions.optionsCount = 17;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
} else if (selected == 1) {
config.position.position_broadcast_secs = 60;
@@ -1915,7 +1916,7 @@ void menuHandler::GPSPositionBroadcastMenu()
#endif
void menuHandler::BluetoothToggleMenu()
void menuHandler::bluetoothToggleMenu()
{
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
BannerOverlayOptions bannerOptions;
@@ -2043,7 +2044,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
auto bannerOptions = createStaticBannerOptions(
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = system_base_menu;
menuQueue = SystemBaseMenu;
screen->runNow();
return;
}
@@ -2138,7 +2139,7 @@ void menuHandler::rebootMenu()
messageStore.saveToFlash();
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
} else {
menuQueue = power_menu;
menuQueue = PowerMenu;
screen->runNow();
}
};
@@ -2160,7 +2161,7 @@ void menuHandler::shutdownMenu()
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
} else {
menuQueue = power_menu;
menuQueue = PowerMenu;
screen->runNow();
}
};
@@ -2221,14 +2222,14 @@ void menuHandler::testMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == NumberPicker) {
menuQueue = number_test;
menuQueue = NumberTest;
screen->runNow();
} else if (selected == ShowChirpy) {
screen->toggleFrameVisibility("chirpy");
screen->setFrames(Screen::FOCUS_SYSTEM);
} else {
menuQueue = system_base_menu;
menuQueue = SystemBaseMenu;
screen->runNow();
}
};
@@ -2252,7 +2253,7 @@ void menuHandler::wifiBaseMenu()
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Wifi_toggle) {
menuQueue = wifi_toggle_menu;
menuQueue = WifiToggleMenu;
screen->runNow();
}
};
@@ -2301,9 +2302,9 @@ void menuHandler::screenOptionsMenu()
hasSupportBrightness = false;
#endif
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits };
static const char *optionsArray[5] = {"Back"};
static int optionsEnumArray[5] = {Back};
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
static const char *optionsArray[6] = {"Back"};
static int optionsEnumArray[6] = {Back};
int options = 1;
// Only show brightness for B&W displays
@@ -2325,6 +2326,9 @@ void menuHandler::screenOptionsMenu()
optionsArray[options] = "Display Units";
optionsEnumArray[options++] = DisplayUnits;
optionsArray[options] = "Message Bubbles";
optionsEnumArray[options++] = MessageBubbles;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Display Options";
bannerOptions.optionsArrayPtr = optionsArray;
@@ -2332,10 +2336,10 @@ void menuHandler::screenOptionsMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Brightness) {
menuHandler::menuQueue = menuHandler::brightness_picker;
menuHandler::menuQueue = menuHandler::BrightnessPicker;
screen->runNow();
} else if (selected == ScreenColor) {
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
menuHandler::menuQueue = menuHandler::TftColorMenuPicker;
screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
@@ -2343,8 +2347,11 @@ void menuHandler::screenOptionsMenu()
} else if (selected == DisplayUnits) {
menuHandler::menuQueue = menuHandler::DisplayUnits;
screen->runNow();
} else if (selected == MessageBubbles) {
menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
screen->runNow();
} else {
menuQueue = system_base_menu;
menuQueue = SystemBaseMenu;
screen->runNow();
}
};
@@ -2380,16 +2387,16 @@ void menuHandler::powerMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Reboot) {
menuHandler::menuQueue = menuHandler::reboot_menu;
menuHandler::menuQueue = menuHandler::RebootMenu;
screen->runNow();
} else if (selected == Shutdown) {
menuHandler::menuQueue = menuHandler::shutdown_menu;
menuHandler::menuQueue = menuHandler::ShutdownMenu;
screen->runNow();
} else if (selected == MUI) {
menuHandler::menuQueue = menuHandler::mui_picker;
menuHandler::menuQueue = menuHandler::MuiPicker;
screen->runNow();
} else {
menuQueue = system_base_menu;
menuQueue = SystemBaseMenu;
screen->runNow();
}
};
@@ -2427,7 +2434,7 @@ void menuHandler::keyVerificationFinalPrompt()
}
}
void menuHandler::FrameToggles_menu()
void menuHandler::frameTogglesMenu()
{
enum optionsNumbers {
Finish,
@@ -2571,7 +2578,7 @@ void menuHandler::FrameToggles_menu()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::DisplayUnits_menu()
void menuHandler::displayUnitsMenu()
{
enum optionsNumbers { Back, MetricUnits, ImperialUnits };
@@ -2592,7 +2599,34 @@ void menuHandler::DisplayUnits_menu()
config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL;
service->reloadConfig(SEGMENT_CONFIG);
} else {
menuHandler::menuQueue = menuHandler::screen_options_menu;
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::messageBubblesMenu()
{
enum optionsNumbers { Back, ShowBubbles, HideBubbles };
static const char *optionsArray[] = {"Back", "Show Bubbles", "Hide Bubbles"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Message Bubbles";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.InitialSelected = config.display.enable_message_bubbles ? 1 : 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == ShowBubbles) {
config.display.enable_message_bubbles = true;
service->reloadConfig(SEGMENT_CONFIG);
LOG_INFO("Message bubbles enabled");
} else if (selected == HideBubbles) {
config.display.enable_message_bubbles = false;
service->reloadConfig(SEGMENT_CONFIG);
LOG_INFO("Message bubbles disabled");
} else {
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
screen->runNow();
}
};
@@ -2601,153 +2635,156 @@ void menuHandler::DisplayUnits_menu()
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
{
if (menuQueue != menu_none)
if (menuQueue != MenuNone)
test_count = 0;
switch (menuQueue) {
case menu_none:
case MenuNone:
break;
case lora_Menu:
case LoraMenu:
loraMenu();
break;
case lora_picker:
case LoraPicker:
LoraRegionPicker();
break;
case device_role_picker:
DeviceRolePicker();
case DeviceRolePicker:
deviceRolePicker();
break;
case radio_preset_picker:
RadioPresetPicker();
case RadioPresetPicker:
radioPresetPicker();
break;
case frequency_slot:
case FrequencySlot:
FrequencySlotPicker();
break;
case no_timeout_lora_picker:
case NoTimeoutLoraPicker:
LoraRegionPicker(0);
break;
case TZ_picker:
case TzPicker:
TZPicker();
break;
case twelve_hour_picker:
TwelveHourPicker();
case TwelveHourPicker:
twelveHourPicker();
break;
case clock_face_picker:
ClockFacePicker();
case ClockFacePicker:
clockFacePicker();
break;
case clock_menu:
case ClockMenu:
clockMenu();
break;
case system_base_menu:
case SystemBaseMenu:
systemBaseMenu();
break;
case position_base_menu:
case PositionBaseMenu:
positionBaseMenu();
break;
case node_base_menu:
case NodeBaseMenu:
nodeListMenu();
break;
#if !MESHTASTIC_EXCLUDE_GPS
case gps_toggle_menu:
case GpsToggleMenu:
GPSToggleMenu();
break;
case gps_format_menu:
case GpsFormatMenu:
GPSFormatMenu();
break;
case gps_smart_position_menu:
case GpsSmartPositionMenu:
GPSSmartPositionMenu();
break;
case gps_update_interval_menu:
case GpsUpdateIntervalMenu:
GPSUpdateIntervalMenu();
break;
case gps_position_broadcast_menu:
case GpsPositionBroadcastMenu:
GPSPositionBroadcastMenu();
break;
#endif
case compass_point_north_menu:
case CompassPointNorthMenu:
compassNorthMenu();
break;
case reset_node_db_menu:
case ResetNodeDbMenu:
resetNodeDBMenu();
break;
case buzzermodemenupicker:
case BuzzerModeMenuPicker:
BuzzerModeMenu();
break;
case mui_picker:
case MuiPicker:
switchToMUIMenu();
break;
case tftcolormenupicker:
case TftColorMenuPicker:
TFTColorPickerMenu(display);
break;
case brightness_picker:
case BrightnessPicker:
BrightnessPickerMenu();
break;
case node_name_length_menu:
case NodeNameLengthMenu:
nodeNameLengthMenu();
break;
case reboot_menu:
case RebootMenu:
rebootMenu();
break;
case shutdown_menu:
case ShutdownMenu:
shutdownMenu();
break;
case NodePicker_menu:
case NodePickerMenu:
NodePicker();
break;
case Manage_Node_menu:
ManageNodeMenu();
case ManageNodeMenu:
manageNodeMenu();
break;
case remove_favorite:
case RemoveFavorite:
removeFavoriteMenu();
break;
case trace_route_menu:
case TraceRouteMenu:
traceRouteMenu();
break;
case test_menu:
case TestMenu:
testMenu();
break;
case number_test:
case NumberTest:
numberTest();
break;
case wifi_toggle_menu:
case WifiToggleMenu:
wifiToggleMenu();
break;
case key_verification_init:
case KeyVerificationInit:
keyVerificationInitMenu();
break;
case key_verification_final_prompt:
case KeyVerificationFinalPrompt:
keyVerificationFinalPrompt();
break;
case bluetooth_toggle_menu:
BluetoothToggleMenu();
case BluetoothToggleMenu:
bluetoothToggleMenu();
break;
case screen_options_menu:
case ScreenOptionsMenu:
screenOptionsMenu();
break;
case power_menu:
case PowerMenu:
powerMenu();
break;
case FrameToggles:
FrameToggles_menu();
frameTogglesMenu();
break;
case DisplayUnits:
DisplayUnits_menu();
displayUnitsMenu();
break;
case throttle_message:
case ThrottleMessage:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break;
case message_response_menu:
case MessageResponseMenu:
messageResponseMenu();
break;
case reply_menu:
case ReplyMenu:
replyMenu();
break;
case delete_messages_menu:
case DeleteMessagesMenu:
deleteMessagesMenu();
break;
case message_viewmode_menu:
case MessageViewModeMenu:
messageViewModeMenu();
break;
case MessageBubblesMenu:
messageBubblesMenu();
break;
}
menuQueue = menu_none;
menuQueue = MenuNone;
}
void menuHandler::saveUIConfig()
+56 -54
View File
@@ -8,53 +8,54 @@ class menuHandler
{
public:
enum screenMenus {
menu_none,
lora_Menu,
lora_picker,
device_role_picker,
radio_preset_picker,
frequency_slot,
no_timeout_lora_picker,
TZ_picker,
twelve_hour_picker,
clock_face_picker,
clock_menu,
position_base_menu,
node_base_menu,
gps_toggle_menu,
gps_format_menu,
gps_smart_position_menu,
gps_update_interval_menu,
gps_position_broadcast_menu,
compass_point_north_menu,
reset_node_db_menu,
buzzermodemenupicker,
mui_picker,
tftcolormenupicker,
brightness_picker,
reboot_menu,
shutdown_menu,
NodePicker_menu,
Manage_Node_menu,
remove_favorite,
test_menu,
number_test,
wifi_toggle_menu,
bluetooth_toggle_menu,
screen_options_menu,
power_menu,
system_base_menu,
key_verification_init,
key_verification_final_prompt,
trace_route_menu,
throttle_message,
message_response_menu,
message_viewmode_menu,
reply_menu,
delete_messages_menu,
node_name_length_menu,
MenuNone,
LoraMenu,
LoraPicker,
DeviceRolePicker,
RadioPresetPicker,
FrequencySlot,
NoTimeoutLoraPicker,
TzPicker,
TwelveHourPicker,
ClockFacePicker,
ClockMenu,
PositionBaseMenu,
NodeBaseMenu,
GpsToggleMenu,
GpsFormatMenu,
GpsSmartPositionMenu,
GpsUpdateIntervalMenu,
GpsPositionBroadcastMenu,
CompassPointNorthMenu,
ResetNodeDbMenu,
BuzzerModeMenuPicker,
MuiPicker,
TftColorMenuPicker,
BrightnessPicker,
RebootMenu,
ShutdownMenu,
NodePickerMenu,
ManageNodeMenu,
RemoveFavorite,
TestMenu,
NumberTest,
WifiToggleMenu,
BluetoothToggleMenu,
ScreenOptionsMenu,
PowerMenu,
SystemBaseMenu,
KeyVerificationInit,
KeyVerificationFinalPrompt,
TraceRouteMenu,
ThrottleMessage,
MessageResponseMenu,
MessageViewModeMenu,
ReplyMenu,
DeleteMessagesMenu,
NodeNameLengthMenu,
FrameToggles,
DisplayUnits
DisplayUnits,
MessageBubblesMenu
};
static screenMenus menuQueue;
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
@@ -62,15 +63,15 @@ class menuHandler
static void OnboardMessage();
static void LoraRegionPicker(uint32_t duration = 30000);
static void loraMenu();
static void DeviceRolePicker();
static void RadioPresetPicker();
static void deviceRolePicker();
static void radioPresetPicker();
static void FrequencySlotPicker();
static void handleMenuSwitch(OLEDDisplay *display);
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
static void clockMenu();
static void TZPicker();
static void TwelveHourPicker();
static void ClockFacePicker();
static void twelveHourPicker();
static void clockFacePicker();
static void messageResponseMenu();
static void messageViewModeMenu();
static void replyMenu();
@@ -95,7 +96,7 @@ class menuHandler
static void rebootMenu();
static void shutdownMenu();
static void NodePicker();
static void ManageNodeMenu();
static void manageNodeMenu();
static void addFavoriteMenu();
static void removeFavoriteMenu();
static void traceRouteMenu();
@@ -106,15 +107,16 @@ class menuHandler
static void screenOptionsMenu();
static void powerMenu();
static void nodeNameLengthMenu();
static void FrameToggles_menu();
static void DisplayUnits_menu();
static void frameTogglesMenu();
static void displayUnitsMenu();
static void messageBubblesMenu();
static void textMessageMenu();
private:
static void saveUIConfig();
static void keyVerificationInitMenu();
static void keyVerificationFinalPrompt();
static void BluetoothToggleMenu();
static void bluetoothToggleMenu();
};
/* Generic Menu Options designations */
+93 -99
View File
@@ -527,8 +527,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
constexpr int BUBBLE_MIN_W = 24;
constexpr int BUBBLE_TEXT_INDENT = 2;
// Check if bubbles are enabled
const bool showBubbles = config.display.enable_message_bubbles;
const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN;
// Derived widths
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2);
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0);
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
// Title string depending on mode
@@ -796,114 +800,105 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
}
}
// Draw bubbles
for (size_t bi = 0; bi < blocks.size(); ++bi) {
const auto &b = blocks[bi];
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
continue;
// Draw bubbles (only if enabled)
if (showBubbles) {
for (size_t bi = 0; bi < blocks.size(); ++bi) {
const auto &b = blocks[bi];
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
continue;
int visualTop = lineTop[b.start];
int visualTop = lineTop[b.start];
int topY;
if (isHeader[b.start]) {
// Header start
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
} else {
// Body start
bool thisLineHasEmote = false;
for (int e = 0; e < numEmotes; ++e) {
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
thisLineHasEmote = true;
break;
int topY;
if (isHeader[b.start]) {
// Header start
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
} else {
// Body start
bool thisLineHasEmote = false;
for (int e = 0; e < numEmotes; ++e) {
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
thisLineHasEmote = true;
break;
}
}
if (thisLineHasEmote) {
constexpr int EMOTE_PADDING_ABOVE = 4;
visualTop -= EMOTE_PADDING_ABOVE;
}
topY = visualTop - BUBBLE_PAD_Y;
}
if (thisLineHasEmote) {
constexpr int EMOTE_PADDING_ABOVE = 4;
visualTop -= EMOTE_PADDING_ABOVE;
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
int bottomY = visualBottom + BUBBLE_PAD_Y;
if (bi + 1 < blocks.size()) {
int nextHeaderIndex = (int)blocks[bi + 1].start;
int nextTop = lineTop[nextHeaderIndex];
int maxBottom = nextTop - 1 - bubbleGapY;
if (bottomY > maxBottom)
bottomY = maxBottom;
}
topY = visualTop - BUBBLE_PAD_Y;
}
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
int bottomY = visualBottom + BUBBLE_PAD_Y;
if (bi + 1 < blocks.size()) {
int nextHeaderIndex = (int)blocks[bi + 1].start;
int nextTop = lineTop[nextHeaderIndex];
int maxBottom = nextTop - 1 - bubbleGapY;
if (bottomY > maxBottom)
bottomY = maxBottom;
}
if (bottomY <= topY + 2)
continue;
if (bottomY <= topY + 2)
continue;
if (bottomY < contentTop || topY > contentBottom - 1)
continue;
if (bottomY < contentTop || topY > contentBottom - 1)
continue;
int maxLineW = 0;
int maxLineW = 0;
for (size_t i = b.start; i <= b.end; ++i) {
int w = 0;
if (isHeader[i]) {
w = display->getStringWidth(cachedLines[i].c_str());
if (b.mine)
w += 12; // room for ACK/NACK/relay mark
} else {
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
for (size_t i = b.start; i <= b.end; ++i) {
int w = 0;
if (isHeader[i]) {
w = display->getStringWidth(cachedLines[i].c_str());
if (b.mine)
w += 12; // room for ACK/NACK/relay mark
} else {
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
}
if (w > maxLineW)
maxLineW = w;
}
if (w > maxLineW)
maxLineW = w;
}
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (BUBBLE_PAD_X * 2));
int bubbleH = (bottomY - topY) + 1;
int bubbleX = 0;
if (b.mine) {
bubbleX = rightEdge - bubbleW;
} else {
bubbleX = x;
}
if (bubbleX < x)
bubbleX = x;
if (bubbleX + bubbleW > rightEdge)
bubbleW = std::max(1, rightEdge - bubbleX);
if (bubbleW > 1 && bubbleH > 1) {
int x1 = bubbleX + bubbleW - 1;
int y1 = topY + bubbleH - 1;
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2));
int bubbleH = (bottomY - topY) + 1;
int bubbleX = 0;
if (b.mine) {
// Send Message (Right side)
display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
// Top Right Corner
display->drawRect(x1, topY, 2, 1);
display->drawRect(x1, topY, 1, 2);
// Bottom Right Corner
display->drawRect(x1 - 1, bottomY - 2, 2, 1);
display->drawRect(x1, bottomY - 3, 1, 2);
// Knock the corners off to make a bubble
display->setColor(BLACK);
display->drawRect(x1 - bubbleW, topY - 1, 1, 1);
display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1);
display->setColor(WHITE);
bubbleX = rightEdge - bubbleW;
} else {
// Received Message (Left Side)
display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH);
// Top Left Corner
display->drawRect(bubbleX + 1, topY + 1, 2, 1);
display->drawRect(bubbleX + 1, topY + 1, 1, 2);
// Bottom Left Corner
display->drawRect(bubbleX + 1, bottomY - 1, 2, 1);
display->drawRect(bubbleX + 1, bottomY - 2, 1, 2);
// Knock the corners off to make a bubble
display->setColor(BLACK);
display->drawRect(bubbleX + bubbleW, topY, 1, 1);
display->drawRect(bubbleX + bubbleW, bottomY, 1, 1);
display->setColor(WHITE);
bubbleX = x;
}
if (bubbleX < x)
bubbleX = x;
if (bubbleX + bubbleW > rightEdge)
bubbleW = std::max(1, rightEdge - bubbleX);
// Draw rounded rectangle bubble
if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) {
const int r = BUBBLE_RADIUS;
const int bx = bubbleX;
const int by = topY;
const int bw = bubbleW;
const int bh = bubbleH;
// Draw the 4 corner arcs using drawCircleQuads
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
// Draw the 4 edges between corners
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
} else if (bubbleW > 1 && bubbleH > 1) {
// Fallback to simple rectangle for very small bubbles
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
}
}
}
} // end if (showBubbles)
// Render visible lines
int lineY = yOffset;
@@ -916,11 +911,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int headerX;
if (isMine[i]) {
// push header left to avoid overlap with scrollbar
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT;
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - (showBubbles ? textIndent : 0);
if (headerX < LEFT_MARGIN)
headerX = LEFT_MARGIN;
} else {
headerX = x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT;
headerX = x + textIndent;
}
display->drawString(headerX, lineY, cachedLines[i].c_str());
@@ -960,14 +955,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
if (isMine[i]) {
// Calculate actual rendered width including emotes
int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT;
int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - (showBubbles ? textIndent : 0);
if (rightX < LEFT_MARGIN)
rightX = LEFT_MARGIN;
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
} else {
drawStringWithEmotes(display, x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes,
numEmotes);
drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes);
}
}
}
+211 -33
View File
@@ -2,6 +2,7 @@
#if HAS_SCREEN
#include "CompassRenderer.h"
#include "GPSStatus.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "NodeListRenderer.h"
#include "UIRenderer.h"
@@ -313,7 +314,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
// === Create the shortName and title string ===
const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
char titlestr[32] = {0};
snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName);
snprintf(titlestr, sizeof(titlestr), "*%s*", shortName);
// === Draw battery/time/mail header (common across screens) ===
graphics::drawCommonHeader(display, x, y, titlestr);
@@ -342,34 +343,162 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
}
// === 2. Signal and Hops (combined on one line, if available) ===
// If both are present: "Sig: 97% [2hops]"
// If only one: show only that one
char signalHopsStr[32] = "";
bool haveSignal = false;
int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100);
int bars = 0;
// Always use "Sig" for the label
const char *signalLabel = " Sig";
// Helper to get SNR limit based on modem preset
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
switch (preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
return -6.0f;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
return -5.5f;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
return -4.5f;
default:
return -6.0f;
}
};
// Calculate signal grade using modem preset and SNR only
float snrLimit = getSnrLimit(config.lora.modem_preset);
float snr = node->snr;
// Determine signal quality label and bars using SNR-only grading
const char *qualityLabel = nullptr;
if (snr > snrLimit + 10) {
qualityLabel = "Good";
bars = 4;
} else if (snr > snrLimit + 6) {
qualityLabel = "Good";
bars = 3;
} else if (snr > snrLimit + 2) {
qualityLabel = "Good";
bars = 2;
} else if (snr > snrLimit - 4) {
qualityLabel = "Fair";
bars = 1;
} else {
qualityLabel = "Bad";
bars = 1;
}
// Add extra spacing on the left if we have an API connection to account for the common footer icons
const char *leftSideSpacing =
graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " ";
// --- Build the Signal/Hops line ---
// If SNR looks reasonable, show signal
if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) {
snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal);
// Only show signal if we have valid SNR
if (snr > -100 && snr != 0) {
snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel);
haveSignal = true;
}
// If hops is valid (>0), show right after signal
if (node->hops_away > 0) {
size_t len = strlen(signalHopsStr);
// Decide between "1 Hop" and "N Hops"
if (haveSignal) {
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away,
(node->hops_away == 1 ? "Hop" : "Hops"));
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]");
} else {
snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops"));
snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]");
}
}
if (signalHopsStr[0] && line < 5) {
display->drawString(x, getTextPositions(display)[line++], signalHopsStr);
if (signalHopsStr[0]) {
int yPos = getTextPositions(display)[line++];
int curX = x;
// Split combined string into signal text and hop suffix
char sigPart[20] = "";
const char *hopPart = nullptr;
char *bracket = strchr(signalHopsStr, '[');
if (bracket) {
size_t n = (size_t)(bracket - signalHopsStr);
if (n >= sizeof(sigPart))
n = sizeof(sigPart) - 1;
memcpy(sigPart, signalHopsStr, n);
sigPart[n] = '\0';
// Trim trailing spaces
while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') {
sigPart[strlen(sigPart) - 1] = '\0';
}
hopPart = bracket; // "[n Hop(s)]"
} else {
strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1);
sigPart[sizeof(sigPart) - 1] = '\0';
}
// Draw signal quality text
display->drawString(curX, yPos, sigPart);
curX += display->getStringWidth(sigPart) + 4;
// Draw signal bars (skip on UltraLow, text only)
if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) {
const int kMaxBars = 4;
if (bars < 1)
bars = 1;
if (bars > kMaxBars)
bars = kMaxBars;
int barX = curX;
const bool hi = (currentResolution == ScreenResolution::High);
int barWidth = hi ? 2 : 1;
int barGap = hi ? 2 : 1;
int maxBarHeight = FONT_HEIGHT_SMALL - 7;
if (!hi)
maxBarHeight -= 1;
int barY = yPos + (FONT_HEIGHT_SMALL - maxBarHeight) / 2;
for (int bi = 0; bi < kMaxBars; bi++) {
int barHeight = maxBarHeight * (bi + 1) / kMaxBars;
if (barHeight < 2)
barHeight = 2;
int bx = barX + bi * (barWidth + barGap);
int by = barY + maxBarHeight - barHeight;
if (bi < bars) {
display->fillRect(bx, by, barWidth, barHeight);
} else {
int baseY = barY + maxBarHeight - 1;
display->drawHorizontalLine(bx, baseY, barWidth);
}
}
curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2;
}
// Draw hops AFTER the bars as: [ number + hop icon ]
if (hopPart && node->hops_away > 0) {
// open bracket
display->drawString(curX, yPos, "[");
curX += display->getStringWidth("[") + 1;
// hop count
char hopCount[6];
snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away);
display->drawString(curX, yPos, hopCount);
curX += display->getStringWidth(hopCount) + 2;
// hop icon
const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2;
display->drawXbm(curX, iconY, hop_width, hop_height, hop);
curX += hop_width + 1;
// closing bracket
display->drawString(curX, yPos, "]");
}
}
// === 3. Heard (last seen, skip if node never seen) ===
@@ -377,8 +506,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
uint32_t seconds = sinceLastSeen(node);
if (seconds != 0 && seconds != UINT32_MAX) {
uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
// Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago"
snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"),
// Format as "Heard:Xm ago", "Heard:Xh ago", or "Heard:Xd ago"
snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard:?" : "%sHeard:%d%c ago"), leftSideSpacing,
(days ? days
: hours ? hours
: minutes),
@@ -386,16 +515,18 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
: hours ? 'h'
: 'm'));
}
if (seenStr[0] && line < 5) {
if (seenStr[0]) {
display->drawString(x, getTextPositions(display)[line++], seenStr);
}
#if !defined(M5STACK_UNITC6L)
// === 4. Uptime (only show if metric is present) ===
char uptimeStr[32] = "";
if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) {
getUptimeStr(node->device_metrics.uptime_seconds * 1000, " Up", uptimeStr, sizeof(uptimeStr));
char upPrefix[12]; // enough for leftSideSpacing + "Up:"
snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing);
getUptimeStr(node->device_metrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr));
}
if (uptimeStr[0] && line < 5) {
if (uptimeStr[0]) {
display->drawString(x, getTextPositions(display)[line++], uptimeStr);
}
@@ -422,16 +553,16 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
if (miles < 0.1) {
int feet = (int)(miles * 5280);
if (feet > 0 && feet < 1000) {
snprintf(distStr, sizeof(distStr), " Distance: %dft", feet);
snprintf(distStr, sizeof(distStr), "%sDistance:%dft", leftSideSpacing, feet);
haveDistance = true;
} else if (feet >= 1000) {
snprintf(distStr, sizeof(distStr), " Distance: ¼mi");
snprintf(distStr, sizeof(distStr), "%sDistance:¼mi", leftSideSpacing);
haveDistance = true;
}
} else {
int roundedMiles = (int)(miles + 0.5);
if (roundedMiles > 0 && roundedMiles < 1000) {
snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles);
snprintf(distStr, sizeof(distStr), "%sDistance:%dmi", leftSideSpacing, roundedMiles);
haveDistance = true;
}
}
@@ -439,26 +570,74 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
if (distanceKm < 1.0) {
int meters = (int)(distanceKm * 1000);
if (meters > 0 && meters < 1000) {
snprintf(distStr, sizeof(distStr), " Distance: %dm", meters);
snprintf(distStr, sizeof(distStr), "%sDistance:%dm", leftSideSpacing, meters);
haveDistance = true;
} else if (meters >= 1000) {
snprintf(distStr, sizeof(distStr), " Distance: 1km");
snprintf(distStr, sizeof(distStr), "%sDistance:1km", leftSideSpacing);
haveDistance = true;
}
} else {
int km = (int)(distanceKm + 0.5);
if (km > 0 && km < 1000) {
snprintf(distStr, sizeof(distStr), " Distance: %dkm", km);
snprintf(distStr, sizeof(distStr), "%sDistance:%dkm", leftSideSpacing, km);
haveDistance = true;
}
}
}
}
// Only display if we actually have a value!
if (haveDistance && distStr[0] && line < 5) {
if (haveDistance && distStr[0]) {
display->drawString(x, getTextPositions(display)[line++], distStr);
}
// === 6. Battery after Distance line, otherwise next available line ===
char batLine[32] = "";
bool haveBatLine = false;
if (node->has_device_metrics) {
bool hasPct = node->device_metrics.has_battery_level;
bool hasVolt = node->device_metrics.has_voltage && node->device_metrics.voltage > 0.001f;
int pct = 0;
float volt = 0.0f;
if (hasPct) {
pct = (int)node->device_metrics.battery_level;
}
if (hasVolt) {
volt = node->device_metrics.voltage;
}
if (hasPct && pct > 0 && pct <= 100) {
// Normal battery percentage
if (hasVolt) {
snprintf(batLine, sizeof(batLine), "%sBat:%d%% (%.2fV)", leftSideSpacing, pct, volt);
} else {
snprintf(batLine, sizeof(batLine), "%sBat:%d%%", leftSideSpacing, pct);
}
haveBatLine = true;
} else if (hasPct && pct > 100) {
// Plugged in
if (hasVolt) {
snprintf(batLine, sizeof(batLine), "%sPlugged In (%.2fV)", leftSideSpacing, volt);
} else {
snprintf(batLine, sizeof(batLine), "%sPlugged In", leftSideSpacing);
}
haveBatLine = true;
} else if (!hasPct && hasVolt) {
// Voltage only
snprintf(batLine, sizeof(batLine), "%sBat:%.2fV", leftSideSpacing, volt);
haveBatLine = true;
}
}
const int maxTextLines = (currentResolution == ScreenResolution::High) ? 6 : 5;
// Only draw battery if it fits within the allowed lines
if (haveBatLine && line <= maxTextLines) {
display->drawString(x, getTextPositions(display)[line++], batLine);
}
// --- Compass Rendering: landscape (wide) screens use the original side-aligned logic ---
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
bool showCompass = false;
@@ -593,7 +772,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
}
char uptimeStr[32] = "";
if (currentResolution != ScreenResolution::UltraLow) {
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr));
}
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr);
@@ -984,7 +1163,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
config.display.heading_bold = false;
const char *displayLine = ""; // Initialize to empty string by default
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
if (config.position.fixed_position) {
@@ -1029,10 +1207,10 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
char uptimeStr[32];
#if defined(USE_EINK)
// E-Ink: skip seconds, show only days/hours/mins
getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), false);
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), false);
#else
// Non E-Ink: include seconds where useful
getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), true);
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true);
#endif
display->drawString(0, getTextPositions(display)[line++], uptimeStr);
+6
View File
@@ -83,6 +83,12 @@ static const unsigned char mail[] PROGMEM = {
0b11111111, 0b00 // Bottom line
};
// Hop icon (9x10)
#define hop_width 9
#define hop_height 10
const uint8_t hop[] PROGMEM = {0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00,
0x38, 0x00, 0xC0, 0x01, 0x40, 0x01, 0xC0, 0x01, 0x40, 0x00};
// 📬 Mail / Message
const uint8_t icon_mail[] PROGMEM = {
0b11111111, // ████████ top border
+22 -3
View File
@@ -1,3 +1,5 @@
#include "graphics/niche/InkHUD/Tile.h"
#include <cstdint>
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./Applet.h"
@@ -55,7 +57,7 @@ InkHUD::Tile *InkHUD::Applet::getTile()
}
// Draw the applet
void InkHUD::Applet::render()
void InkHUD::Applet::render(bool full)
{
assert(assignedTile); // Ensure that we have a tile
assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile
@@ -65,10 +67,11 @@ void InkHUD::Applet::render()
wantRender = false; // Flag set by requestUpdate
wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored.
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted.
wantFullRender = true; // Default to a full render
updateDimensions();
resetDrawingSpace();
onRender(); // Derived applet's drawing takes place here
onRender(full); // Draw the applet
// Handle "Tile Highlighting"
// Some devices may use an auxiliary button to switch between tiles
@@ -115,6 +118,11 @@ Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
return wantUpdateType;
}
bool InkHUD::Applet::wantsFullRender()
{
return wantFullRender;
}
// Get size of the applet's drawing space from its tile
// Performed immediately before derived applet's drawing code runs
void InkHUD::Applet::updateDimensions()
@@ -142,10 +150,11 @@ void InkHUD::Applet::resetDrawingSpace()
// Once the renderer has given other applets a chance to process whatever event we just detected,
// it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground)
// We should requestUpdate even if our applet is currently background, because this might be changed by autoshow
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type)
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type, bool full)
{
wantRender = true;
wantUpdateType = type;
wantFullRender = full;
inkhud->requestUpdate();
}
@@ -778,6 +787,16 @@ void InkHUD::Applet::drawHeader(std::string text)
drawPixel(x, 0, BLACK);
drawPixel(x, headerDivY, BLACK); // Dotted 50%
}
// Dither near battery
if (settings->optionalFeatures.batteryIcon) {
constexpr uint16_t ditherSizePx = 4;
Tile *batteryTile = ((Applet *)inkhud->getSystemApplet("BatteryIcon"))->getTile();
const uint16_t batteryTileLeft = batteryTile->getLeft();
const uint16_t batteryTileTop = batteryTile->getTop();
const uint16_t batteryTileHeight = batteryTile->getHeight();
hatchRegion(batteryTileLeft - ditherSizePx, batteryTileTop, ditherSizePx, batteryTileHeight, 2, WHITE);
}
}
// Get the height of the standard applet header
+10 -4
View File
@@ -64,10 +64,11 @@ class Applet : public GFX
// Rendering
void render(); // Draw the applet
void render(bool full); // Draw the applet
bool wantsToRender(); // Check whether applet wants to render
bool wantsToAutoshow(); // Check whether applet wants to become foreground
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
bool wantsFullRender(); // Check whether applet wants to render over its previous render
void updateDimensions(); // Get current size from tile
void resetDrawingSpace(); // Makes sure every render starts with same parameters
@@ -82,7 +83,7 @@ class Applet : public GFX
// Event handlers
virtual void onRender() = 0; // All drawing happens here
virtual void onRender(bool full) = 0; // For drawing the applet
virtual void onActivate() {}
virtual void onDeactivate() {}
virtual void onForeground() {}
@@ -96,6 +97,9 @@ class Applet : public GFX
virtual void onNavDown() {}
virtual void onNavLeft() {}
virtual void onNavRight() {}
virtual void onFreeText(char c) {}
virtual void onFreeTextDone() {}
virtual void onFreeTextCancel() {}
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
@@ -108,8 +112,9 @@ class Applet : public GFX
protected:
void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update
void requestAutoshow(); // Ask for applet to be moved to foreground
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED,
bool full = true); // Ask WindowManager to schedule a display update
void requestAutoshow(); // Ask for applet to be moved to foreground
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
@@ -164,6 +169,7 @@ class Applet : public GFX
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display
bool wantFullRender = true; // Render with a fresh canvas
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
@@ -4,7 +4,7 @@
using namespace NicheGraphics;
void InkHUD::MapApplet::onRender()
void InkHUD::MapApplet::onRender(bool full)
{
// Abort if no markers to render
if (!enoughMarkers()) {
@@ -27,7 +27,7 @@ namespace NicheGraphics::InkHUD
class MapApplet : public Applet
{
public:
void onRender() override;
void onRender(bool full) override;
protected:
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes
@@ -103,7 +103,7 @@ uint8_t InkHUD::NodeListApplet::maxCards()
}
// Draw, using info which derived applet placed into NodeListApplet::cards for us
void InkHUD::NodeListApplet::onRender()
void InkHUD::NodeListApplet::onRender(bool full)
{
// ================================
@@ -46,7 +46,7 @@ class NodeListApplet : public Applet, public MeshModule
public:
NodeListApplet(const char *name);
void onRender() override;
void onRender(bool full) override;
bool wantPacket(const meshtastic_MeshPacket *p) override;
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
@@ -6,7 +6,7 @@ using namespace NicheGraphics;
// All drawing happens here
// Our basic example doesn't do anything useful. It just passively prints some text.
void InkHUD::BasicExampleApplet::onRender()
void InkHUD::BasicExampleApplet::onRender(bool full)
{
printAt(0, 0, "Hello, World!");
@@ -28,7 +28,7 @@ class BasicExampleApplet : public Applet
// You must have an onRender() method
// All drawing happens here
void onRender() override;
void onRender(bool full) override;
};
} // namespace NicheGraphics::InkHUD
@@ -35,7 +35,7 @@ ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_Mesh
// We can trigger a render by calling requestUpdate()
// Render might be called by some external source
// We should always be ready to draw
void InkHUD::NewMsgExampleApplet::onRender()
void InkHUD::NewMsgExampleApplet::onRender(bool full)
{
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)
@@ -34,7 +34,7 @@ class NewMsgExampleApplet : public Applet, public SinglePortModule
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
// All drawing happens here
void onRender() override;
void onRender(bool full) override;
// Your applet might also want to use some of these
// Useful for setting up or tidying up
@@ -10,7 +10,7 @@ InkHUD::AlignStickApplet::AlignStickApplet()
bringToForeground();
}
void InkHUD::AlignStickApplet::onRender()
void InkHUD::AlignStickApplet::onRender(bool full)
{
setFont(fontMedium);
printAt(0, 0, "Align Joystick:");
@@ -152,19 +152,17 @@ void InkHUD::AlignStickApplet::onBackground()
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
}
void InkHUD::AlignStickApplet::onButtonLongPress()
{
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onExitLong()
{
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavUp()
@@ -172,7 +170,6 @@ void InkHUD::AlignStickApplet::onNavUp()
settings->joystick.aligned = true;
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavDown()
@@ -181,7 +178,6 @@ void InkHUD::AlignStickApplet::onNavDown()
settings->joystick.aligned = true;
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavLeft()
@@ -190,7 +186,6 @@ void InkHUD::AlignStickApplet::onNavLeft()
settings->joystick.aligned = true;
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavRight()
@@ -199,7 +194,6 @@ void InkHUD::AlignStickApplet::onNavRight()
settings->joystick.aligned = true;
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
#endif
@@ -23,7 +23,7 @@ class AlignStickApplet : public SystemApplet
public:
AlignStickApplet();
void onRender() override;
void onRender(bool full) override;
void onForeground() override;
void onBackground() override;
void onButtonLongPress() override;
@@ -6,6 +6,8 @@ using namespace NicheGraphics;
InkHUD::BatteryIconApplet::BatteryIconApplet()
{
alwaysRender = true; // render everytime the screen is updated
// Show at boot, if user has previously enabled the feature
if (settings->optionalFeatures.batteryIcon)
bringToForeground();
@@ -44,39 +46,29 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
return 0; // Tell Observable to continue informing other observers
}
void InkHUD::BatteryIconApplet::onRender()
void InkHUD::BatteryIconApplet::onRender(bool full)
{
// Fill entire tile
// - size of icon controlled by size of tile
int16_t l = 0;
int16_t t = 0;
uint16_t w = width();
int16_t h = height();
// Clear the region beneath the tile
// Clear the region beneath the tile, including the border
// Most applets are drawing onto an empty frame buffer and don't need to do this
// We do need to do this with the battery though, as it is an "overlay"
fillRect(l, t, w, h, WHITE);
// Vertical centerline
const int16_t m = t + (h / 2);
fillRect(0, 0, width(), height(), WHITE);
// =====================
// Draw battery outline
// =====================
// Positive terminal "bump"
const int16_t &bumpL = l;
const uint16_t bumpH = h / 2;
const int16_t bumpT = m - (bumpH / 2);
constexpr uint16_t bumpW = 2;
const int16_t &bumpL = 1;
const uint16_t bumpH = (height() - 2) / 2;
const int16_t bumpT = (1 + ((height() - 2) / 2)) - (bumpH / 2);
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
// Main body of battery
const int16_t bodyL = bumpL + bumpW;
const int16_t &bodyT = t;
const int16_t &bodyH = h;
const int16_t bodyW = w - bumpW;
const int16_t bodyL = 1 + bumpW;
const int16_t &bodyT = 1;
const int16_t &bodyH = height() - 2; // Handle top/bottom padding
const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
// Erase join between bump and body
@@ -87,12 +79,13 @@ void InkHUD::BatteryIconApplet::onRender()
// ===================
constexpr int16_t slicePad = 2;
const int16_t sliceL = bodyL + slicePad;
int16_t sliceL = bodyL + slicePad;
const int16_t sliceT = bodyT + slicePad;
const uint16_t sliceH = bodyH - (slicePad * 2);
uint16_t sliceW = bodyW - (slicePad * 2);
sliceW = (sliceW * socRounded) / 100; // Apply percentage
sliceW = (sliceW * socRounded) / 100; // Apply percentage
sliceL += ((bodyW - (slicePad * 2)) - sliceW); // Shift slice to the battery's negative terminal, correcting drain direction
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
@@ -23,7 +23,7 @@ class BatteryIconApplet : public SystemApplet
public:
BatteryIconApplet();
void onRender() override;
void onRender(bool full) override;
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
private:
@@ -0,0 +1,257 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./KeyboardApplet.h"
using namespace NicheGraphics;
InkHUD::KeyboardApplet::KeyboardApplet()
{
// Calculate row widths
for (uint8_t row = 0; row < KBD_ROWS; row++) {
rowWidths[row] = 0;
for (uint8_t col = 0; col < KBD_COLS; col++)
rowWidths[row] += keyWidths[row * KBD_COLS + col];
}
}
void InkHUD::KeyboardApplet::onRender(bool full)
{
uint16_t em = fontSmall.lineHeight(); // 16 pt
uint16_t keyH = Y(1.0) / KBD_ROWS;
int16_t keyTopPadding = (keyH - fontSmall.lineHeight()) / 2;
if (full) { // Draw full keyboard
for (uint8_t row = 0; row < KBD_ROWS; row++) {
// Calculate the remaining space to be used as padding
int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
// Draw keys
uint16_t xPos = 0;
for (uint8_t col = 0; col < KBD_COLS; col++) {
Color fgcolor = BLACK;
uint8_t index = row * KBD_COLS + col;
uint16_t keyX = ((xPos * em) >> 4) + ((col * keyXPadding) / (KBD_COLS - 1));
uint16_t keyY = row * keyH;
uint16_t keyW = (keyWidths[index] * em) >> 4;
if (index == selectedKey) {
fgcolor = WHITE;
fillRect(keyX, keyY, keyW, keyH, BLACK);
}
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[index], fgcolor);
xPos += keyWidths[index];
}
}
} else { // Only draw the difference
if (selectedKey != prevSelectedKey) {
// Draw previously selected key
uint8_t row = prevSelectedKey / KBD_COLS;
int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
uint16_t xPos = 0;
for (uint8_t i = prevSelectedKey - (prevSelectedKey % KBD_COLS); i < prevSelectedKey; i++)
xPos += keyWidths[i];
uint16_t keyX = ((xPos * em) >> 4) + (((prevSelectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
uint16_t keyY = row * keyH;
uint16_t keyW = (keyWidths[prevSelectedKey] * em) >> 4;
fillRect(keyX, keyY, keyW, keyH, WHITE);
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[prevSelectedKey], BLACK);
// Draw newly selected key
row = selectedKey / KBD_COLS;
keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
xPos = 0;
for (uint8_t i = selectedKey - (selectedKey % KBD_COLS); i < selectedKey; i++)
xPos += keyWidths[i];
keyX = ((xPos * em) >> 4) + (((selectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
keyY = row * keyH;
keyW = (keyWidths[selectedKey] * em) >> 4;
fillRect(keyX, keyY, keyW, keyH, BLACK);
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[selectedKey], WHITE);
}
}
prevSelectedKey = selectedKey;
}
// Draw the key label corresponding to the char
// for most keys it draws the character itself
// for ['\b', '\n', ' ', '\x1b'] it draws special glyphs
void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color)
{
if (key == '\b') {
// Draw backspace glyph: 13 x 9 px
/**
* [][][][][][][][][]
* [][] []
* [][] [] [] []
* [][] [] [] []
* [][] [] []
* [][] [] [] []
* [][] [] [] []
* [][] []
* [][][][][][][][][]
*/
const uint8_t bsBitmap[] = {0x0f, 0xf8, 0x18, 0x08, 0x32, 0x28, 0x61, 0x48, 0xc0,
0x88, 0x61, 0x48, 0x32, 0x28, 0x18, 0x08, 0x0f, 0xf8};
uint16_t leftPadding = (width - 13) >> 1;
drawBitmap(left + leftPadding, top + 1, bsBitmap, 13, 9, color);
} else if (key == '\n') {
// Draw done glyph: 12 x 9 px
/**
* [][]
* [][]
* [][]
* [][]
* [][]
* [][] [][]
* [][] [][]
* [][][]
* []
*/
const uint8_t doneBitmap[] = {0x00, 0x30, 0x00, 0x60, 0x00, 0xc0, 0x01, 0x80, 0x03,
0x00, 0xc6, 0x00, 0x6c, 0x00, 0x38, 0x00, 0x10, 0x00};
uint16_t leftPadding = (width - 12) >> 1;
drawBitmap(left + leftPadding, top + 1, doneBitmap, 12, 9, color);
} else if (key == ' ') {
// Draw space glyph: 13 x 9 px
/**
*
*
*
*
* [] []
* [] []
* [][][][][][][][][][][][][]
*
*
*/
const uint8_t spaceBitmap[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
0x08, 0x80, 0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00};
uint16_t leftPadding = (width - 13) >> 1;
drawBitmap(left + leftPadding, top + 1, spaceBitmap, 13, 9, color);
} else if (key == '\x1b') {
setTextColor(color);
std::string keyText = "ESC";
uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
printAt(left + leftPadding, top, keyText);
} else {
setTextColor(color);
if (key >= 0x61)
key -= 32; // capitalize
std::string keyText = std::string(1, key);
uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
printAt(left + leftPadding, top, keyText);
}
}
void InkHUD::KeyboardApplet::onForeground()
{
handleInput = true; // Intercept the button input for our applet
// Select the first key
selectedKey = 0;
prevSelectedKey = 0;
}
void InkHUD::KeyboardApplet::onBackground()
{
handleInput = false;
}
void InkHUD::KeyboardApplet::onButtonShortPress()
{
char key = keys[selectedKey];
if (key == '\n') {
inkhud->freeTextDone();
inkhud->closeKeyboard();
} else if (key == '\x1b') {
inkhud->freeTextCancel();
inkhud->closeKeyboard();
} else {
inkhud->freeText(key);
}
}
void InkHUD::KeyboardApplet::onButtonLongPress()
{
char key = keys[selectedKey];
if (key == '\n') {
inkhud->freeTextDone();
inkhud->closeKeyboard();
} else if (key == '\x1b') {
inkhud->freeTextCancel();
inkhud->closeKeyboard();
} else {
if (key >= 0x61)
key -= 32; // capitalize
inkhud->freeText(key);
}
}
void InkHUD::KeyboardApplet::onExitShort()
{
inkhud->freeTextCancel();
inkhud->closeKeyboard();
}
void InkHUD::KeyboardApplet::onExitLong()
{
inkhud->freeTextCancel();
inkhud->closeKeyboard();
}
void InkHUD::KeyboardApplet::onNavUp()
{
if (selectedKey < KBD_COLS) // wrap
selectedKey += KBD_COLS * (KBD_ROWS - 1);
else // move 1 row back
selectedKey -= KBD_COLS;
// Request rendering over the previously drawn render
requestUpdate(EInk::UpdateTypes::FAST, false);
// Force an update to bypass lockRequests
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
void InkHUD::KeyboardApplet::onNavDown()
{
selectedKey += KBD_COLS;
selectedKey %= (KBD_COLS * KBD_ROWS);
// Request rendering over the previously drawn render
requestUpdate(EInk::UpdateTypes::FAST, false);
// Force an update to bypass lockRequests
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
void InkHUD::KeyboardApplet::onNavLeft()
{
if (selectedKey % KBD_COLS == 0) // wrap
selectedKey += KBD_COLS - 1;
else // move 1 column back
selectedKey--;
// Request rendering over the previously drawn render
requestUpdate(EInk::UpdateTypes::FAST, false);
// Force an update to bypass lockRequests
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
void InkHUD::KeyboardApplet::onNavRight()
{
if (selectedKey % KBD_COLS == KBD_COLS - 1) // wrap
selectedKey -= KBD_COLS - 1;
else // move 1 column forward
selectedKey++;
// Request rendering over the previously drawn render
requestUpdate(EInk::UpdateTypes::FAST, false);
// Force an update to bypass lockRequests
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
uint16_t InkHUD::KeyboardApplet::getKeyboardHeight()
{
const uint16_t keyH = fontSmall.lineHeight() * 1.2;
return keyH * KBD_ROWS;
}
#endif
@@ -0,0 +1,66 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
System Applet to render an on-screeen keyboard
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/InkHUD.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
#include <string>
namespace NicheGraphics::InkHUD
{
class KeyboardApplet : public SystemApplet
{
public:
KeyboardApplet();
void onRender(bool full) override;
void onForeground() override;
void onBackground() override;
void onButtonShortPress() override;
void onButtonLongPress() override;
void onExitShort() override;
void onExitLong() override;
void onNavUp() override;
void onNavDown() override;
void onNavLeft() override;
void onNavRight() override;
static uint16_t getKeyboardHeight(); // used to set the keyboard tile height
private:
void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color);
static const uint8_t KBD_COLS = 11;
static const uint8_t KBD_ROWS = 4;
const char keys[KBD_COLS * KBD_ROWS] = {
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b', // row 0
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n', // row 1
'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '!', ' ', // row 2
'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '?', '\x1b' // row 3
};
// This array represents the widths of each key in points
// 16 pt = line height of the text
const uint16_t keyWidths[KBD_COLS * KBD_ROWS] = {
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 0
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 1
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 2
16, 16, 16, 16, 16, 16, 16, 10, 10, 12, 40 // row 3
};
uint16_t rowWidths[KBD_ROWS];
uint8_t selectedKey = 0; // selected key index
uint8_t prevSelectedKey = 0;
};
} // namespace NicheGraphics::InkHUD
#endif
@@ -30,7 +30,7 @@ InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
// This is then drawn with a FULL refresh by Renderer::begin
}
void InkHUD::LogoApplet::onRender()
void InkHUD::LogoApplet::onRender(bool full)
{
// Size of the region which the logo should "scale to fit"
uint16_t logoWLimit = X(0.8);
@@ -120,7 +120,7 @@ void InkHUD::LogoApplet::onBackground()
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
}
// Begin displaying the screen which is shown at shutdown
@@ -138,10 +138,10 @@ void InkHUD::LogoApplet::onShutdown()
// Intention is to restore display health.
inverted = true;
inkhud->forceUpdate(Drivers::EInk::FULL, false);
inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
delay(1000); // Cooldown. Back to back updates aren't great for health.
inverted = false;
inkhud->forceUpdate(Drivers::EInk::FULL, false);
inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
delay(1000); // Cooldown
// Prepare for the powered-off screen now
@@ -176,7 +176,7 @@ void InkHUD::LogoApplet::onReboot()
textTitle = "Rebooting...";
fontTitle = fontSmall;
inkhud->forceUpdate(Drivers::EInk::FULL, false);
inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
// Perform the update right now, waiting here until complete
}
@@ -21,7 +21,7 @@ class LogoApplet : public SystemApplet, public concurrency::OSThread
{
public:
LogoApplet();
void onRender() override;
void onRender(bool full) override;
void onForeground() override;
void onBackground() override;
void onShutdown() override;
@@ -19,10 +19,10 @@ namespace NicheGraphics::InkHUD
enum MenuAction {
NO_ACTION,
SEND_PING,
FREE_TEXT,
STORE_CANNEDMESSAGE_SELECTION,
SEND_CANNEDMESSAGE,
SHUTDOWN,
BACK,
NEXT_TILE,
TOGGLE_BACKLIGHT,
TOGGLE_GPS,
@@ -90,6 +90,8 @@ void InkHUD::MenuApplet::onForeground()
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
OSThread::enabled = true;
freeTextMode = false;
// Upgrade the refresh to FAST, for guaranteed responsiveness
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
@@ -116,6 +118,8 @@ void InkHUD::MenuApplet::onBackground()
SystemApplet::lockRequests = false;
SystemApplet::handleInput = false;
handleFreeText = false;
// Restore the user applet whose tile we borrowed
if (borrowedTileOwner)
borrowedTileOwner->bringToForeground();
@@ -325,10 +329,6 @@ void InkHUD::MenuApplet::execute(MenuItem item)
}
break;
case BACK:
showPage(item.nextPage);
return;
case NEXT_TILE:
inkhud->nextTile();
// Unselect menu item after tile change
@@ -344,12 +344,26 @@ void InkHUD::MenuApplet::execute(MenuItem item)
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
break;
case FREE_TEXT:
OSThread::enabled = false;
handleFreeText = true;
cm.freeTextItem.rawText.erase(); // clear the previous freetext message
freeTextMode = true; // render input field instead of normal menu
// Open the on-screen keyboard if the joystick is enabled
if (settings->joystick.enabled)
inkhud->openKeyboard();
break;
case STORE_CANNEDMESSAGE_SELECTION:
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
if (!settings->joystick.enabled)
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
else
cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry
break;
case SEND_CANNEDMESSAGE:
cm.selectedRecipientItem = &cm.recipientItems.at(cursor);
// send selected message
sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str());
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here
break;
@@ -868,6 +882,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
switch (page) {
case ROOT:
previousPage = MenuPage::EXIT;
// Optional: next applet
if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1)
items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown
@@ -878,7 +893,6 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
items.push_back(MenuItem("Node Config", MenuPage::NODE_CONFIG));
items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::EXIT;
break;
case SEND:
@@ -888,11 +902,12 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
case CANNEDMESSAGE_RECIPIENT:
populateRecipientPage();
previousPage = MenuPage::OPTIONS;
previousPage = MenuPage::SEND;
break;
case OPTIONS:
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT));
previousPage = MenuPage::ROOT;
items.push_back(MenuItem("Back", previousPage));
// Optional: backlight
if (settings->optionalMenuItems.backlight)
items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
@@ -916,31 +931,32 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::ROOT;
break;
case APPLETS:
populateAppletPage(); // must be first
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::OPTIONS;
populateAppletPage(); // must be first
items.insert(items.begin(), MenuItem("Back", previousPage));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case AUTOSHOW:
populateAutoshowPage(); // must be first
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::OPTIONS;
populateAutoshowPage(); // must be first
items.insert(items.begin(), MenuItem("Back", previousPage));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case RECENTS:
previousPage = MenuPage::OPTIONS;
populateRecentsPage(); // builds only the options
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
items.insert(items.begin(), MenuItem("Back", previousPage));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case NODE_CONFIG:
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT));
previousPage = MenuPage::ROOT;
items.push_back(MenuItem("Back", previousPage));
// Radio Config Section
items.push_back(MenuItem::Header("Radio Config"));
items.push_back(MenuItem("LoRa", MenuPage::NODE_CONFIG_LORA));
@@ -965,8 +981,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
break;
case NODE_CONFIG_DEVICE: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
const char *role = DisplayFormatters::getDeviceRole(config.device.role);
nodeConfigLabels.emplace_back("Role: " + std::string(role));
@@ -981,7 +997,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_POSITION: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS
const auto mode = config.position.gps_mode;
if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
@@ -996,7 +1013,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_POWER: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
#if defined(ARCH_ESP32)
items.push_back(MenuItem("Powersave", MenuAction::TOGGLE_POWER_SAVE, MenuPage::EXIT, &config.power.is_power_saving));
#endif
@@ -1029,7 +1047,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_POWER_ADC_CAL: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_POWER));
previousPage = MenuPage::NODE_CONFIG_POWER;
items.push_back(MenuItem("Back", previousPage));
// Instruction text (header-style, non-selectable)
items.push_back(MenuItem::Header("Run on full charge Only"));
@@ -1042,7 +1061,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_NETWORK: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
const char *wifiLabel = config.network.wifi_enabled ? "WiFi: On" : "WiFi: Off";
@@ -1099,7 +1119,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_DISPLAY: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY,
&config.display.use_12h_clock));
@@ -1114,7 +1135,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_BLUETOOTH: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
const char *btLabel = config.bluetooth.enabled ? "Bluetooth: On" : "Bluetooth: Off";
items.push_back(MenuItem(btLabel, MenuAction::TOGGLE_BLUETOOTH, MenuPage::EXIT));
@@ -1127,8 +1149,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_LORA: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
const char *region = myRegion ? myRegion->name : "Unset";
nodeConfigLabels.emplace_back("Region: " + std::string(region));
@@ -1150,7 +1172,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_CHANNELS: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
meshtastic_Channel &ch = channels.getByIndex(i);
@@ -1181,7 +1204,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_CHANNEL_DETAIL: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNELS));
previousPage = MenuPage::NODE_CONFIG_CHANNELS;
items.push_back(MenuItem("Back", previousPage));
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
@@ -1226,7 +1250,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_CHANNEL_PRECISION: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL;
items.push_back(MenuItem("Back", previousPage));
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));
@@ -1247,7 +1272,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_DEVICE_ROLE: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE));
previousPage = MenuPage::NODE_CONFIG_DEVICE;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("Client", MenuAction::SET_ROLE_CLIENT, MenuPage::EXIT));
items.push_back(MenuItem("Client Mute", MenuAction::SET_ROLE_CLIENT_MUTE, MenuPage::EXIT));
items.push_back(MenuItem("Router", MenuAction::SET_ROLE_ROUTER, MenuPage::EXIT));
@@ -1257,7 +1283,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case TIMEZONE:
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE));
previousPage = MenuPage::NODE_CONFIG_DEVICE;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("US/Hawaii", SET_TZ_US_HAWAII, MenuPage::NODE_CONFIG_DEVICE));
items.push_back(MenuItem("US/Alaska", SET_TZ_US_ALASKA, MenuPage::NODE_CONFIG_DEVICE));
items.push_back(MenuItem("US/Pacific", SET_TZ_US_PACIFIC, MenuPage::NODE_CONFIG_DEVICE));
@@ -1279,7 +1306,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
break;
case REGION:
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA));
previousPage = MenuPage::NODE_CONFIG_LORA;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT));
items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT));
items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT));
@@ -1310,7 +1338,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
break;
case NODE_CONFIG_PRESET: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA));
previousPage = MenuPage::NODE_CONFIG_LORA;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT));
items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT));
items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT));
@@ -1323,7 +1352,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
// Administration Section
case NODE_CONFIG_ADMIN_RESET:
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("Reset All", MenuAction::RESET_NODEDB_ALL, MenuPage::EXIT));
items.push_back(MenuItem("Keep Favorites Only", MenuAction::RESET_NODEDB_KEEP_FAVORITES, MenuPage::EXIT));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
@@ -1361,8 +1391,14 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
currentPage = page;
}
void InkHUD::MenuApplet::onRender()
void InkHUD::MenuApplet::onRender(bool full)
{
// Free text mode draws a text input field and skips the normal rendering
if (freeTextMode) {
drawInputField(0, fontSmall.lineHeight(), X(1.0), Y(1.0) - fontSmall.lineHeight() - 1, cm.freeTextItem.rawText);
return;
}
if (items.size() == 0)
LOG_ERROR("Empty Menu");
@@ -1481,44 +1517,48 @@ void InkHUD::MenuApplet::onRender()
void InkHUD::MenuApplet::onButtonShortPress()
{
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!settings->joystick.enabled) {
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
cursor = (cursor + 1) % items.size();
} while (items.at(cursor).isHeader);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
} else {
if (cursorShown)
execute(items.at(cursor));
else
showPage(MenuPage::EXIT);
if (!wantsToRender())
if (!settings->joystick.enabled) {
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
cursor = (cursor + 1) % items.size();
} while (items.at(cursor).isHeader);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
} else {
if (cursorShown)
execute(items.at(cursor));
else
showPage(MenuPage::EXIT);
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
}
}
void InkHUD::MenuApplet::onButtonLongPress()
{
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (cursorShown)
execute(items.at(cursor));
else
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
if (cursorShown)
execute(items.at(cursor));
else
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
// If we didn't already request a specialized update, when handling a menu action,
// then perform the usual fast update.
// FAST keeps things responsive: important because we're dealing with user input
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
// If we didn't already request a specialized update, when handling a menu action,
// then perform the usual fast update.
// FAST keeps things responsive: important because we're dealing with user input
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
}
void InkHUD::MenuApplet::onExitShort()
@@ -1531,56 +1571,107 @@ void InkHUD::MenuApplet::onExitShort()
void InkHUD::MenuApplet::onNavUp()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
if (cursor == 0)
cursor = items.size() - 1;
else
cursor--;
} while (items.at(cursor).isHeader);
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
if (cursor == 0)
cursor = items.size() - 1;
else
cursor--;
} while (items.at(cursor).isHeader);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onNavDown()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
cursor = (cursor + 1) % items.size();
} while (items.at(cursor).isHeader);
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
cursor = (cursor + 1) % items.size();
} while (items.at(cursor).isHeader);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onNavLeft()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
// Go to the previous menu page
showPage(previousPage);
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
// Go to the previous menu page
showPage(previousPage);
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
}
void InkHUD::MenuApplet::onNavRight()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (cursorShown)
execute(items.at(cursor));
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
}
if (cursorShown)
execute(items.at(cursor));
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
void InkHUD::MenuApplet::onFreeText(char c)
{
if (cm.freeTextItem.rawText.length() >= menuTextLimit && c != '\b')
return;
if (c == '\b') {
if (!cm.freeTextItem.rawText.empty())
cm.freeTextItem.rawText.pop_back();
} else {
cm.freeTextItem.rawText += c;
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onFreeTextDone()
{
// Restart the auto-close timeout
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
OSThread::enabled = true;
handleFreeText = false;
freeTextMode = false;
if (!cm.freeTextItem.rawText.empty()) {
cm.selectedMessageItem = &cm.freeTextItem;
showPage(MenuPage::CANNEDMESSAGE_RECIPIENT);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onFreeTextCancel()
{
// Restart the auto-close timeout
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
OSThread::enabled = true;
handleFreeText = false;
freeTextMode = false;
// Clear the free text message
cm.freeTextItem.rawText.erase();
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
@@ -1635,6 +1726,10 @@ void InkHUD::MenuApplet::populateSendPage()
// Position / NodeInfo packet
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
// If joystick is available, include the Free Text option
if (settings->joystick.enabled)
items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND));
// One menu item for each canned message
uint8_t count = cm.store->size();
for (uint8_t i = 0; i < count; i++) {
@@ -1734,6 +1829,48 @@ 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)
{
setFont(fontSmall);
uint16_t wrapMaxH = 0;
// Draw the text, input box, and cursor
// Adjusting the box for screen height
while (wrapMaxH < height - fontSmall.lineHeight()) {
wrapMaxH += fontSmall.lineHeight();
}
// If the text is so long that it goes outside of the input box, the text is actually rendered off screen.
uint32_t textHeight = getWrappedTextHeight(0, width - 5, text);
if (!text.empty()) {
uint16_t textPadding = X(1.0) > Y(1.0) ? wrapMaxH - textHeight : wrapMaxH - textHeight + 1;
if (textHeight > wrapMaxH)
printWrapped(2, textPadding, width - 5, text);
else
printWrapped(2, top + 2, width - 5, text);
}
uint16_t textCursorX = text.empty() ? 1 : getCursorX();
uint16_t textCursorY = text.empty() ? fontSmall.lineHeight() + 2 : getCursorY() - fontSmall.lineHeight() + 3;
if (textCursorX + 1 > width - 5) {
textCursorX = getCursorX() - width + 5;
textCursorY += fontSmall.lineHeight();
}
fillRect(textCursorX + 1, textCursorY, 1, fontSmall.lineHeight(), BLACK);
// A white rectangle clears the top part of the screen for any text that's printed beyond the input box
fillRect(0, 0, X(1.0), top, WHITE);
// Draw character limit
std::string ftlen = std::to_string(text.length()) + "/" + to_string(menuTextLimit);
uint16_t textLen = getTextWidth(ftlen);
printAt(X(1.0) - textLen - 2, 0, ftlen);
// Draw the border
drawRect(0, top, width, wrapMaxH + 5, BLACK);
}
// Renders the panel shown at the top of the root menu.
// Displays the clock, and several other pieces of instantaneous system info,
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
@@ -1875,4 +2012,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources()
cm.messageItems.clear();
cm.recipientItems.clear();
}
#endif // MESHTASTIC_INCLUDE_INKHUD
#endif // MESHTASTIC_INCLUDE_INKHUD
@@ -32,7 +32,10 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void onNavDown() override;
void onNavLeft() override;
void onNavRight() override;
void onRender() override;
void onFreeText(char c) override;
void onFreeTextDone() override;
void onFreeTextCancel() override;
void onRender(bool full) override;
void show(Tile *t); // Open the menu, onto a user tile
void setStartPage(MenuPage page);
@@ -51,6 +54,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
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
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
@@ -62,8 +67,9 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
MenuPage previousPage = MenuPage::EXIT;
uint8_t cursor = 0; // Which menu item is currently highlighted
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
bool freeTextMode = false;
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
uint16_t menuTextLimit = 200;
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
std::vector<std::string> nodeConfigLabels; // Persistent labels for Node Config pages
@@ -104,6 +110,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
// Cleared onBackground (when MenuApplet closes)
std::vector<MessageItem> messageItems;
std::vector<RecipientItem> recipientItems;
MessageItem freeTextItem;
} cm;
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
@@ -65,7 +65,7 @@ int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket
return 0;
}
void InkHUD::NotificationApplet::onRender()
void InkHUD::NotificationApplet::onRender(bool full)
{
// Clear the region beneath the tile
// Most applets are drawing onto an empty frame buffer and don't need to do this
@@ -139,54 +139,47 @@ void InkHUD::NotificationApplet::onForeground()
void InkHUD::NotificationApplet::onBackground()
{
handleInput = false;
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
}
void InkHUD::NotificationApplet::onButtonShortPress()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onButtonLongPress()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onExitShort()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onExitLong()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onNavUp()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onNavDown()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onNavLeft()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::NotificationApplet::onNavRight()
{
dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
@@ -26,7 +26,7 @@ class NotificationApplet : public SystemApplet
public:
NotificationApplet();
void onRender() override;
void onRender(bool full) override;
void onForeground() override;
void onBackground() override;
void onButtonShortPress() override;
@@ -9,7 +9,7 @@ InkHUD::PairingApplet::PairingApplet()
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
}
void InkHUD::PairingApplet::onRender()
void InkHUD::PairingApplet::onRender(bool full)
{
// Header
setFont(fontMedium);
@@ -45,7 +45,7 @@ void InkHUD::PairingApplet::onBackground()
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
}
int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status)
@@ -22,7 +22,7 @@ class PairingApplet : public SystemApplet
public:
PairingApplet();
void onRender() override;
void onRender(bool full) override;
void onForeground() override;
void onBackground() override;
@@ -4,7 +4,7 @@
using namespace NicheGraphics;
void InkHUD::PlaceholderApplet::onRender()
void InkHUD::PlaceholderApplet::onRender(bool full)
{
// This placeholder applet fills its area with sparse diagonal lines
hatchRegion(0, 0, width(), height(), 8, BLACK);
@@ -17,7 +17,7 @@ namespace NicheGraphics::InkHUD
class PlaceholderApplet : public SystemApplet
{
public:
void onRender() override;
void onRender(bool full) override;
// Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet.
// The window manager decides when and where it should be rendered
@@ -45,7 +45,7 @@ InkHUD::TipsApplet::TipsApplet()
bringToForeground();
}
void InkHUD::TipsApplet::onRender()
void InkHUD::TipsApplet::onRender(bool full)
{
switch (tipQueue.front()) {
case Tip::WELCOME:
@@ -261,7 +261,7 @@ void InkHUD::TipsApplet::onBackground()
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
}
// While our SystemApplet::handleInput flag is true
@@ -292,9 +292,8 @@ void InkHUD::TipsApplet::onButtonShortPress()
inkhud->persistence->saveSettings();
}
// Close applet and clean the screen
// Close applet
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} else {
requestUpdate();
}
@@ -306,4 +305,4 @@ void InkHUD::TipsApplet::onExitShort()
onButtonShortPress();
}
#endif
#endif
@@ -33,7 +33,7 @@ class TipsApplet : public SystemApplet
public:
TipsApplet();
void onRender() override;
void onRender(bool full) override;
void onForeground() override;
void onBackground() override;
void onButtonShortPress() override;

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