Compare commits

...
64 Commits
Author SHA1 Message Date
Ben Meadors 40ef34aa23 Add experimental OLED emulator vuejs component 2026-02-13 20:43:24 -06:00
Ben MeadorsandGitHub c8ff02cc35 Merge branch 'master' into develop 2026-02-13 18:09:37 -06:00
d770ef27c7 Update src/mesh/NodeDB.cpp
Co-authored-by: Wessel <wessel@weebl.me>
2026-02-13 17:36:12 -06:00
71edf2fb48 Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp
Co-authored-by: Wessel <wessel@weebl.me>
2026-02-13 17:35:13 -06:00
c79e1925d9 Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp
Co-authored-by: Wessel <wessel@weebl.me>
2026-02-13 17:35:03 -06:00
3d649d518f Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp
Co-authored-by: Wessel <wessel@weebl.me>
2026-02-13 17:34:53 -06:00
947f8176dc Update src/detect/ScanI2C.cpp
Co-authored-by: Wessel <wessel@weebl.me>
2026-02-13 17:34:31 -06:00
d5bde83ff5 Upgrade trunk (#9631)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-13 12:29:43 -06:00
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 MeadorsandGitHub b6e5534a8c Merge branch 'master' into develop 2026-02-12 13:11:48 -06:00
github-actions[bot]andGitHub f10d786d82 Update protobufs (#9621) 2026-02-12 19:15:08 +01:00
WesselandGitHub 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]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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 RobbinsandGitHub 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]andGitHub f86291546d chore(deps): update adafruit mpu6050 to v2.2.9 (#9611) 2026-02-12 11:20:44 +01:00
github-actions[bot]andGitHub 640dd5f7d7 Upgrade trunk (#9615) 2026-02-12 11:15:38 +01:00
+2
oscgonferGitHubniklHannes FuchsNashui-YanBen MeadorsTom FifieldMike Robbinsgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>thebenternWillyJLrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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 0847969201.

* 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
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 BennettandGitHub 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
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-DandBen Meadors 0adcadb1ce Modify the dependency library of v4-tft (#9507) 2026-02-11 07:06:15 -06:00
21d5a34fec Update protobufs (#9605)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-02-11 07:05:35 -06:00
Quency-DandGitHub 648148af8a Modify the dependency library of v4-tft (#9507) 2026-02-11 06:50:04 -06:00
Thomas GöttgensandBen Meadors 5b03b1fe52 fix some random compiler warnings (#9596) 2026-02-11 06:45:26 -06:00
Thomas GöttgensandGitHub f96a8593f2 fix some random compiler warnings (#9596) 2026-02-11 06:43:05 -06:00
4954723f97 Automated version bumps (#9604)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-02-11 06:30:05 -06:00
Chloe BethelandBen Meadors 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 BennettandGitHub 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
6b73ac3850 Upgrade trunk (#9599)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-11 06:24:46 -06:00
Chloe BethelandGitHub 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
65adfa894f Automated version bumps (#9604)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-02-11 06:21:34 -06:00
6df0449408 Upgrade trunk (#9581)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-10 06:30:50 -06:00
TomandGitHub a092f6bb22 Refactor logging in ProtobufModule to ensure message details are logged after successful decoding (#9536) 2026-02-09 19:56:48 -06:00
Jason PandGitHub 927a4e47b8 Update built-in documentation for current method of implementation (#9592) 2026-02-09 18:56:22 -06:00
Jonathan BennettandGitHub 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
139e45dff8 Update protobufs (#9588)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-02-09 13:52:01 -06:00
Jonathan BennettandGitHub 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]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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
+1
oscgonferGitHubHannes FuchsNashui-YanBen MeadorsTom FifieldMike Robbinsgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>thebenternWillyJLrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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 0847969201.

* 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
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
NickandGitHub bb3d6d5326 Fix embedded null byte truncation in ATAK strings (#9570) 2026-02-08 11:20:15 -06:00
Jonathan BennettandGitHub 4ce554e09d Make LED_POWER blip even in critical battery (#9545) 2026-02-08 06:49:30 -06:00
Jonathan BennettandGitHub 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 PandGitHub 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
AustinandGitHub 39139cc2ea RPM: Include meshtasticd-start.sh (#9561) 2026-02-07 11:13:01 -05:00
AustinandGitHub 4a4b1f4a87 meshtasticd: Fix install on Fedora 43 (#9556) 2026-02-06 19:36:21 -06:00
779e446d14 Fix hop_limit upgrade detection (#9550)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-06 13:00:08 -06:00
Jonathan BennettandGitHub 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
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
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
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
Jason PandGitHub 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
VortettyandGitHub 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
Eric SesterhennandGitHub 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 BennettandGitHub f514bc230b Prefer EXT_PWR_DETECT pin over chargingVolt to detect power unplugged (#9511) 2026-02-03 00:13:49 -06:00
Jason PandGitHub 0022148323 Missed in reviews - fixing send bubble (#9505) 2026-02-01 19:10:00 -06:00
Jonathan BennettandGitHub 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 BennettandGitHub 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
235 changed files with 9550 additions and 1125 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 \
+4 -4
View File
@@ -8,15 +8,15 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.500
- renovate@43.4.0
- checkov@3.2.501
- renovate@43.10.3
- prettier@3.8.1
- trufflehog@3.93.0
- trufflehog@3.93.3
- yamllint@1.38.0
- bandit@1.9.3
- trivy@0.69.1
- taplo@0.10.0
- ruff@0.15.0
- ruff@0.15.1
- isort@7.0.0
- markdownlint@0.47.0
- oxipng@10.1.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 \
@@ -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
+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"),
},
},
});
+9 -3
View File
@@ -50,11 +50,13 @@ 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
@@ -144,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.8
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
@@ -210,9 +212,11 @@ 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]
@@ -238,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
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
sensirion/Sensirion I2C SFA3x@1.0.0
+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;
+93 -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;
@@ -1319,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;
@@ -1367,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;
}
@@ -1386,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);
}
+2
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
+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
+3 -1
View File
@@ -89,7 +89,9 @@ class ScanI2C
DA217,
CHSC6X,
CST226SE,
SEN5X
SEN5X,
SFA30,
CW2015
} DeviceType;
// typedef uint8_t DeviceAddress;
+20 -1
View File
@@ -1,4 +1,6 @@
#include "ScanI2CTwoWire.h"
#include "configuration.h"
#include "detect/ScanI2C.h"
#if !MESHTASTIC_EXCLUDE_I2C
@@ -456,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)
@@ -581,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);
-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);
+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
+12
View File
@@ -1,3 +1,5 @@
#include "graphics/niche/InkHUD/Tile.h"
#include <cstdint>
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./Applet.h"
@@ -785,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
@@ -48,37 +48,27 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
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
@@ -89,12 +79,13 @@ void InkHUD::BatteryIconApplet::onRender(bool full)
// ===================
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);
+4 -4
View File
@@ -510,10 +510,10 @@ void InkHUD::WindowManager::placeSystemTiles()
const uint16_t batteryIconWidth = batteryIconHeight * 1.8;
inkhud->getSystemApplet("BatteryIcon")
->getTile()
->setRegion(inkhud->width() - batteryIconWidth, // x
2, // y
batteryIconWidth, // width
batteryIconHeight); // height
->setRegion(inkhud->width() - batteryIconWidth - 1, // x
1, // y
batteryIconWidth + 1, // width
batteryIconHeight + 2); // height
// Note: the tiles of placeholder and menu applets are manipulated specially
// - menuApplet borrows user tiles
+26 -63
View File
@@ -7,13 +7,13 @@
#include "NodeDB.h"
#include "PowerFSM.h"
#include "PowerMon.h"
#include "RadioLibInterface.h"
#include "ReliableRouter.h"
#include "airtime.h"
#include "buzz.h"
#include "power/PowerHAL.h"
#include "FSCommon.h"
#include "Led.h"
#include "RTC.h"
#include "SPILock.h"
#include "Throttle.h"
@@ -29,7 +29,6 @@
#include <Wire.h>
#endif
#include "detect/einkScan.h"
#include "graphics/RAKled.h"
#include "graphics/Screen.h"
#include "main.h"
#include "mesh/generated/meshtastic/config.pb.h"
@@ -193,6 +192,8 @@ bool kb_found = false;
// global bool to record that on-screen keyboard (OSK) is present
bool osk_found = false;
unsigned long last_listen = 0;
// The I2C address of the RTC Module (if found)
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
// The I2C address of the Accelerometer (if found)
@@ -242,28 +243,10 @@ const char *getDeviceName()
return name;
}
// TODO remove from main.cpp
static int32_t ledBlinker()
{
// Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if
// config.device.led_heartbeat_disabled is changed
if (config.device.led_heartbeat_disabled)
return 1000;
static bool ledOn;
ledOn ^= 1;
ledBlink.set(ledOn);
// have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that
return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000);
}
uint32_t timeLastPowered = 0;
static Periodic *ledPeriodic;
static OSThread *powerFSMthread;
static OSThread *ambientLightingThread;
OSThread *ambientLightingThread;
RadioInterface *rIf = NULL;
#ifdef ARCH_PORTDUINO
@@ -299,21 +282,16 @@ void earlyInitVariant() {}
// blink user led in 3 flashes sequence to indicate what is happening
void waitUntilPowerLevelSafe()
{
#ifdef LED_PIN
pinMode(LED_PIN, OUTPUT);
#endif
while (powerHAL_isPowerLevelSafe() == false) {
#ifdef LED_PIN
#ifdef LED_POWER
// 3x: blink for 300 ms, pause for 300 ms
for (int i = 0; i < 3; i++) {
digitalWrite(LED_PIN, LED_STATE_ON);
digitalWrite(LED_POWER, LED_STATE_ON);
delay(300);
digitalWrite(LED_PIN, LED_STATE_OFF);
digitalWrite(LED_POWER, LED_STATE_OFF);
delay(300);
}
#endif
@@ -337,6 +315,11 @@ void setup()
// initialize power HAL layer as early as possible
powerHAL_init();
#ifdef LED_POWER
pinMode(LED_POWER, OUTPUT);
digitalWrite(LED_POWER, LED_STATE_ON);
#endif
// prevent booting if device is in power failure mode
// boot sequence will follow when battery level raises to safe mode
waitUntilPowerLevelSafe();
@@ -349,11 +332,6 @@ void setup()
digitalWrite(PIN_POWER_EN, HIGH);
#endif
#ifdef LED_POWER
pinMode(LED_POWER, OUTPUT);
digitalWrite(LED_POWER, LED_STATE_ON);
#endif
#ifdef LED_NOTIFICATION
pinMode(LED_NOTIFICATION, OUTPUT);
digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON);
@@ -366,17 +344,14 @@ void setup()
#ifdef BLE_LED
pinMode(BLE_LED, OUTPUT);
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
digitalWrite(BLE_LED, LED_STATE_OFF);
#endif
concurrency::hasBeenSetup = true;
#if HAS_SCREEN
meshtastic_Config_DisplayConfig_OledType screen_model =
meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO;
#endif
OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64;
#ifdef USE_SEGGER
@@ -488,14 +463,6 @@ void setup()
OSThread::setup();
// TODO make this ifdef based on defined pins and move from main.cpp
#if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2)
// The ThinkNodes have their own blink logic
// ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
#else
ledPeriodic = new Periodic("Blink", ledBlinker);
#endif
fsInit();
#if !MESHTASTIC_EXCLUDE_I2C
@@ -601,6 +568,7 @@ void setup()
}
#endif
#if HAS_SCREEN
auto screenInfo = i2cScanner->firstScreen();
screen_found = screenInfo.type != ScanI2C::DeviceType::NONE ? screenInfo.address : ScanI2C::ADDRESS_NONE;
@@ -618,6 +586,7 @@ void setup()
screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO;
}
}
#endif
#define UPDATE_FROM_SCANNER(FIND_FN)
#if defined(USE_VIRTUAL_KEYBOARD)
@@ -712,20 +681,12 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X);
#endif
#ifdef HAS_SDCARD
setupSDCard();
#endif
// LED init
#ifdef LED_PIN
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now
#endif
// Hello
printInfo();
#ifdef BUILD_EPOCH
@@ -763,6 +724,7 @@ void setup()
else
playStartMelody();
#if HAS_SCREEN
// fixed screen override?
if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO)
screen_model = config.display.oled;
@@ -775,6 +737,7 @@ void setup()
#if defined(USE_SH1107_128_64)
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64
#endif
#endif
#if !MESHTASTIC_EXCLUDE_I2C
#if !defined(ARCH_STM32WL)
@@ -833,7 +796,7 @@ void setup()
SPI.begin();
#endif
#else
// ESP32
// ESP32
#if defined(HW_SPI1_DEVICE)
SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
@@ -966,12 +929,6 @@ void setup()
setupNicheGraphics();
#endif
#ifdef LED_PIN
// Turn LED off after boot, if heartbeat by config
if (config.device.led_heartbeat_disabled)
digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON);
#endif
// Do this after service.init (because that clears error_code)
#ifdef HAS_PMU
if (!pmu_found)
@@ -1168,6 +1125,12 @@ void loop()
#endif
power->powerCommandsCheck();
if (RadioLibInterface::instance != nullptr && !Throttle::isWithinTimespanMs(last_listen, 1000 * 60) &&
!(RadioLibInterface::instance->isSending() || RadioLibInterface::instance->isActivelyReceiving())) {
RadioLibInterface::instance->startReceive();
LOG_DEBUG("attempting AGC reset");
}
#ifdef DEBUG_STACK
static uint32_t lastPrint = 0;
if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) {
+1
View File
@@ -33,6 +33,7 @@ extern ScanI2C::DeviceAddress cardkb_found;
extern uint8_t kb_model;
extern bool kb_found;
extern bool osk_found;
extern unsigned long last_listen;
extern ScanI2C::DeviceAddress rtc_found;
extern ScanI2C::DeviceAddress accelerometer_found;
extern ScanI2C::FoundDevice rgb_found;
+11 -1
View File
@@ -1408,6 +1408,15 @@ void NodeDB::loadFromDisk()
if (portduino_config.has_configDisplayMode) {
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode;
}
if (portduino_config.has_statusMessage) {
moduleConfig.has_statusmessage = true;
strncpy(moduleConfig.statusmessage.node_status, portduino_config.statusMessage.c_str(),
sizeof(moduleConfig.statusmessage.node_status));
moduleConfig.statusmessage.node_status[sizeof(moduleConfig.statusmessage.node_status) - 1] = '\0';
}
if (portduino_config.enable_UDP) {
config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST;
}
#endif
}
@@ -1548,6 +1557,7 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
moduleConfig.has_ambient_lighting = true;
moduleConfig.has_audio = true;
moduleConfig.has_paxcounter = true;
moduleConfig.has_statusmessage = true;
success &=
saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);
@@ -1762,7 +1772,7 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
info->has_device_metrics = false;
info->has_position = false;
info->user.public_key.size = 0;
info->user.public_key.bytes[0] = 0;
memset(info->user.public_key.bytes, 0, sizeof(info->user.public_key.bytes));
} else {
/* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with
* public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM!
+2 -1
View File
@@ -82,7 +82,6 @@ template <class T> class ProtobufModule : protected SinglePortModule
// it would be better to update even if the message was destined to others.
auto &p = mp.decoded;
LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum, p.payload.size);
T scratch;
T *decoded = NULL;
@@ -90,6 +89,8 @@ template <class T> class ProtobufModule : protected SinglePortModule
memset(&scratch, 0, sizeof(scratch));
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) {
decoded = &scratch;
LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum,
p.payload.size);
} else {
LOG_ERROR("Error decoding proto module!");
// if we can't decode it, nobody can process it!
+2 -2
View File
@@ -943,9 +943,9 @@ void RadioInterface::limitPower(int8_t loraMaxPower)
}
} else if (!devicestate.owner.is_licensed) {
// we have an array of PA gain values. Find the highest power setting that works.
for (int radio_dbm = 0; radio_dbm < num_pa_points; radio_dbm++) {
for (int radio_dbm = 0; radio_dbm < (int)num_pa_points; radio_dbm++) {
if (((radio_dbm + tx_gain[radio_dbm]) > power) ||
((radio_dbm == (num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) {
((radio_dbm == (int)(num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) {
// we've exceeded the power limit, or hit the max we can do
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]);
power -= tx_gain[radio_dbm];
+7 -2
View File
@@ -453,8 +453,11 @@ void RadioLibInterface::handleReceiveInterrupt()
}
#endif
if (state != RADIOLIB_ERR_NONE) {
LOG_ERROR("Ignore received packet due to error=%d (maybe to=0x%08x, from=0x%08x, flags=0x%02x)", state,
radioBuffer.header.to, radioBuffer.header.from, radioBuffer.header.flags);
// Log PacketHeader similar to RadioInterface::printPacket so we can try to match RX errors to other packets in the logs.
LOG_ERROR("Ignore received packet due to error=%d (maybe id=0x%08x fr=0x%08x to=0x%08x flags=0x%02x rxSNR=%g rxRSSI=%i "
"nextHop=0x%x relay=0x%x)",
state, radioBuffer.header.id, radioBuffer.header.from, radioBuffer.header.to, radioBuffer.header.flags,
iface->getSNR(), lround(iface->getRSSI()), radioBuffer.header.next_hop, radioBuffer.header.relay_node);
rxBad++;
airTime->logAirtime(RX_ALL_LOG, rxMsec);
@@ -514,6 +517,8 @@ void RadioLibInterface::handleReceiveInterrupt()
void RadioLibInterface::startReceive()
{
// Note the updated timestamp, to avoid unneeded AGC resets
last_listen = millis();
isReceiving = true;
powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn);
}
@@ -36,6 +36,9 @@ PB_BIND(meshtastic_SCD4X_config, meshtastic_SCD4X_config, AUTO)
PB_BIND(meshtastic_SEN5X_config, meshtastic_SEN5X_config, AUTO)
PB_BIND(meshtastic_SCD30_config, meshtastic_SCD30_config, AUTO)
+58 -7
View File
@@ -79,7 +79,9 @@ typedef enum _meshtastic_AdminMessage_ModuleConfigType {
/* TODO: REPLACE */
meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG = 12,
/* TODO: REPLACE */
meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG = 13
meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG = 13,
/* Traffic management module config */
meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG = 14
} meshtastic_AdminMessage_ModuleConfigType;
typedef enum _meshtastic_AdminMessage_BackupLocation {
@@ -204,6 +206,27 @@ typedef struct _meshtastic_SEN5X_config {
bool set_one_shot_mode;
} meshtastic_SEN5X_config;
typedef struct _meshtastic_SCD30_config {
/* Set Automatic self-calibration enabled */
bool has_set_asc;
bool set_asc;
/* Recalibration target CO2 concentration in ppm (FRC or ASC) */
bool has_set_target_co2_conc;
uint32_t set_target_co2_conc;
/* Reference temperature in degC */
bool has_set_temperature;
float set_temperature;
/* Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure) */
bool has_set_altitude;
uint32_t set_altitude;
/* Power mode for sensor (true for low power, false for normal) */
bool has_set_measurement_interval;
uint32_t set_measurement_interval;
/* Perform a factory reset of the sensor */
bool has_soft_reset;
bool soft_reset;
} meshtastic_SCD30_config;
typedef struct _meshtastic_SensorConfig {
/* SCD4X CO2 Sensor configuration */
bool has_scd4x_config;
@@ -211,6 +234,9 @@ typedef struct _meshtastic_SensorConfig {
/* SEN5X PM Sensor configuration */
bool has_sen5x_config;
meshtastic_SEN5X_config sen5x_config;
/* SCD30 CO2 Sensor configuration */
bool has_scd30_config;
meshtastic_SCD30_config scd30_config;
} meshtastic_SensorConfig;
typedef PB_BYTES_ARRAY_T(8) meshtastic_AdminMessage_session_passkey_t;
@@ -369,8 +395,8 @@ extern "C" {
#define _meshtastic_AdminMessage_ConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ConfigType)(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG+1))
#define _meshtastic_AdminMessage_ModuleConfigType_MIN meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG
#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG
#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG+1))
#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG
#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG+1))
#define _meshtastic_AdminMessage_BackupLocation_MIN meshtastic_AdminMessage_BackupLocation_FLASH
#define _meshtastic_AdminMessage_BackupLocation_MAX meshtastic_AdminMessage_BackupLocation_SD
@@ -398,6 +424,7 @@ extern "C" {
/* Initializer values for message structs */
#define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}}
#define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0}
@@ -406,9 +433,10 @@ extern "C" {
#define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0}
#define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0}
#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default}
#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default, false, meshtastic_SCD30_config_init_default}
#define meshtastic_SCD4X_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_SEN5X_config_init_default {false, 0, false, 0}
#define meshtastic_SCD30_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}}
#define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0}
#define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}}
@@ -416,9 +444,10 @@ extern "C" {
#define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0}
#define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0}
#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero}
#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero, false, meshtastic_SCD30_config_init_zero}
#define meshtastic_SCD4X_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_SEN5X_config_init_zero {false, 0, false, 0}
#define meshtastic_SCD30_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
/* Field tags (for use in manual encoding/decoding) */
#define meshtastic_AdminMessage_InputEvent_event_code_tag 1
@@ -449,8 +478,15 @@ extern "C" {
#define meshtastic_SCD4X_config_set_power_mode_tag 7
#define meshtastic_SEN5X_config_set_temperature_tag 1
#define meshtastic_SEN5X_config_set_one_shot_mode_tag 2
#define meshtastic_SCD30_config_set_asc_tag 1
#define meshtastic_SCD30_config_set_target_co2_conc_tag 2
#define meshtastic_SCD30_config_set_temperature_tag 3
#define meshtastic_SCD30_config_set_altitude_tag 4
#define meshtastic_SCD30_config_set_measurement_interval_tag 5
#define meshtastic_SCD30_config_soft_reset_tag 6
#define meshtastic_SensorConfig_scd4x_config_tag 1
#define meshtastic_SensorConfig_sen5x_config_tag 2
#define meshtastic_SensorConfig_scd30_config_tag 3
#define meshtastic_AdminMessage_get_channel_request_tag 1
#define meshtastic_AdminMessage_get_channel_response_tag 2
#define meshtastic_AdminMessage_get_owner_request_tag 3
@@ -640,11 +676,13 @@ X(a, STATIC, OPTIONAL, UINT32, security_number, 4)
#define meshtastic_SensorConfig_FIELDLIST(X, a) \
X(a, STATIC, OPTIONAL, MESSAGE, scd4x_config, 1) \
X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2)
X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2) \
X(a, STATIC, OPTIONAL, MESSAGE, scd30_config, 3)
#define meshtastic_SensorConfig_CALLBACK NULL
#define meshtastic_SensorConfig_DEFAULT NULL
#define meshtastic_SensorConfig_scd4x_config_MSGTYPE meshtastic_SCD4X_config
#define meshtastic_SensorConfig_sen5x_config_MSGTYPE meshtastic_SEN5X_config
#define meshtastic_SensorConfig_scd30_config_MSGTYPE meshtastic_SCD30_config
#define meshtastic_SCD4X_config_FIELDLIST(X, a) \
X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \
@@ -663,6 +701,16 @@ X(a, STATIC, OPTIONAL, BOOL, set_one_shot_mode, 2)
#define meshtastic_SEN5X_config_CALLBACK NULL
#define meshtastic_SEN5X_config_DEFAULT NULL
#define meshtastic_SCD30_config_FIELDLIST(X, a) \
X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \
X(a, STATIC, OPTIONAL, UINT32, set_target_co2_conc, 2) \
X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 3) \
X(a, STATIC, OPTIONAL, UINT32, set_altitude, 4) \
X(a, STATIC, OPTIONAL, UINT32, set_measurement_interval, 5) \
X(a, STATIC, OPTIONAL, BOOL, soft_reset, 6)
#define meshtastic_SCD30_config_CALLBACK NULL
#define meshtastic_SCD30_config_DEFAULT NULL
extern const pb_msgdesc_t meshtastic_AdminMessage_msg;
extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg;
extern const pb_msgdesc_t meshtastic_AdminMessage_OTAEvent_msg;
@@ -673,6 +721,7 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg;
extern const pb_msgdesc_t meshtastic_SensorConfig_msg;
extern const pb_msgdesc_t meshtastic_SCD4X_config_msg;
extern const pb_msgdesc_t meshtastic_SEN5X_config_msg;
extern const pb_msgdesc_t meshtastic_SCD30_config_msg;
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
#define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg
@@ -685,6 +734,7 @@ extern const pb_msgdesc_t meshtastic_SEN5X_config_msg;
#define meshtastic_SensorConfig_fields &meshtastic_SensorConfig_msg
#define meshtastic_SCD4X_config_fields &meshtastic_SCD4X_config_msg
#define meshtastic_SEN5X_config_fields &meshtastic_SEN5X_config_msg
#define meshtastic_SCD30_config_fields &meshtastic_SCD30_config_msg
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size
@@ -694,9 +744,10 @@ extern const pb_msgdesc_t meshtastic_SEN5X_config_msg;
#define meshtastic_HamParameters_size 31
#define meshtastic_KeyVerificationAdmin_size 25
#define meshtastic_NodeRemoteHardwarePinsResponse_size 496
#define meshtastic_SCD30_config_size 27
#define meshtastic_SCD4X_config_size 29
#define meshtastic_SEN5X_config_size 7
#define meshtastic_SensorConfig_size 40
#define meshtastic_SensorConfig_size 69
#define meshtastic_SharedContact_size 127
#ifdef __cplusplus
@@ -361,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
/* Maximum encoded size of messages (where known) */
/* meshtastic_NodeDatabase_size depends on runtime parameters */
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
#define meshtastic_BackupPreferences_size 2364
#define meshtastic_BackupPreferences_size 2419
#define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1737
#define meshtastic_NodeInfoLite_size 196
+10 -4
View File
@@ -90,6 +90,9 @@ typedef struct _meshtastic_LocalModuleConfig {
/* StatusMessage Config */
bool has_statusmessage;
meshtastic_ModuleConfig_StatusMessageConfig statusmessage;
/* The part of the config that is specific to the Traffic Management module */
bool has_traffic_management;
meshtastic_ModuleConfig_TrafficManagementConfig traffic_management;
} meshtastic_LocalModuleConfig;
@@ -99,9 +102,9 @@ extern "C" {
/* Initializer values for message structs */
#define meshtastic_LocalConfig_init_default {false, meshtastic_Config_DeviceConfig_init_default, false, meshtastic_Config_PositionConfig_init_default, false, meshtastic_Config_PowerConfig_init_default, false, meshtastic_Config_NetworkConfig_init_default, false, meshtastic_Config_DisplayConfig_init_default, false, meshtastic_Config_LoRaConfig_init_default, false, meshtastic_Config_BluetoothConfig_init_default, 0, false, meshtastic_Config_SecurityConfig_init_default}
#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default, false, meshtastic_ModuleConfig_StatusMessageConfig_init_default}
#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default, false, meshtastic_ModuleConfig_StatusMessageConfig_init_default, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_default}
#define meshtastic_LocalConfig_init_zero {false, meshtastic_Config_DeviceConfig_init_zero, false, meshtastic_Config_PositionConfig_init_zero, false, meshtastic_Config_PowerConfig_init_zero, false, meshtastic_Config_NetworkConfig_init_zero, false, meshtastic_Config_DisplayConfig_init_zero, false, meshtastic_Config_LoRaConfig_init_zero, false, meshtastic_Config_BluetoothConfig_init_zero, 0, false, meshtastic_Config_SecurityConfig_init_zero}
#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero, false, meshtastic_ModuleConfig_StatusMessageConfig_init_zero}
#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero, false, meshtastic_ModuleConfig_StatusMessageConfig_init_zero, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_zero}
/* Field tags (for use in manual encoding/decoding) */
#define meshtastic_LocalConfig_device_tag 1
@@ -128,6 +131,7 @@ extern "C" {
#define meshtastic_LocalModuleConfig_detection_sensor_tag 13
#define meshtastic_LocalModuleConfig_paxcounter_tag 14
#define meshtastic_LocalModuleConfig_statusmessage_tag 15
#define meshtastic_LocalModuleConfig_traffic_management_tag 16
/* Struct field encoding specification for nanopb */
#define meshtastic_LocalConfig_FIELDLIST(X, a) \
@@ -166,7 +170,8 @@ X(a, STATIC, OPTIONAL, MESSAGE, neighbor_info, 11) \
X(a, STATIC, OPTIONAL, MESSAGE, ambient_lighting, 12) \
X(a, STATIC, OPTIONAL, MESSAGE, detection_sensor, 13) \
X(a, STATIC, OPTIONAL, MESSAGE, paxcounter, 14) \
X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15)
X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15) \
X(a, STATIC, OPTIONAL, MESSAGE, traffic_management, 16)
#define meshtastic_LocalModuleConfig_CALLBACK NULL
#define meshtastic_LocalModuleConfig_DEFAULT NULL
#define meshtastic_LocalModuleConfig_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig
@@ -183,6 +188,7 @@ X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15)
#define meshtastic_LocalModuleConfig_detection_sensor_MSGTYPE meshtastic_ModuleConfig_DetectionSensorConfig
#define meshtastic_LocalModuleConfig_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig
#define meshtastic_LocalModuleConfig_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig
#define meshtastic_LocalModuleConfig_traffic_management_MSGTYPE meshtastic_ModuleConfig_TrafficManagementConfig
extern const pb_msgdesc_t meshtastic_LocalConfig_msg;
extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
@@ -194,7 +200,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
#define meshtastic_LocalConfig_size 751
#define meshtastic_LocalModuleConfig_size 758
#define meshtastic_LocalModuleConfig_size 813
#ifdef __cplusplus
} /* extern "C" */
@@ -30,6 +30,9 @@ PB_BIND(meshtastic_ModuleConfig_AudioConfig, meshtastic_ModuleConfig_AudioConfig
PB_BIND(meshtastic_ModuleConfig_PaxcounterConfig, meshtastic_ModuleConfig_PaxcounterConfig, AUTO)
PB_BIND(meshtastic_ModuleConfig_TrafficManagementConfig, meshtastic_ModuleConfig_TrafficManagementConfig, AUTO)
PB_BIND(meshtastic_ModuleConfig_SerialConfig, meshtastic_ModuleConfig_SerialConfig, AUTO)
@@ -228,6 +228,39 @@ typedef struct _meshtastic_ModuleConfig_PaxcounterConfig {
int32_t ble_threshold;
} meshtastic_ModuleConfig_PaxcounterConfig;
/* Config for the Traffic Management module.
Provides packet inspection and traffic shaping to help reduce channel utilization */
typedef struct _meshtastic_ModuleConfig_TrafficManagementConfig {
/* Master enable for traffic management module */
bool enabled;
/* Enable position deduplication to drop redundant position broadcasts */
bool position_dedup_enabled;
/* Number of bits of precision for position deduplication (0-32) */
uint32_t position_precision_bits;
/* Minimum interval in seconds between position updates from the same node */
uint32_t position_min_interval_secs;
/* Enable direct response to NodeInfo requests from local cache */
bool nodeinfo_direct_response;
/* Minimum hop distance from requestor before responding to NodeInfo requests */
uint32_t nodeinfo_direct_response_max_hops;
/* Enable per-node rate limiting to throttle chatty nodes */
bool rate_limit_enabled;
/* Time window in seconds for rate limiting calculations */
uint32_t rate_limit_window_secs;
/* Maximum packets allowed per node within the rate limit window */
uint32_t rate_limit_max_packets;
/* Enable dropping of unknown/undecryptable packets per rate_limit_window_secs */
bool drop_unknown_enabled;
/* Number of unknown packets before dropping from a node */
uint32_t unknown_packet_threshold;
/* Set hop_limit to 0 for relayed telemetry broadcasts (own packets unaffected) */
bool exhaust_hop_telemetry;
/* Set hop_limit to 0 for relayed position broadcasts (own packets unaffected) */
bool exhaust_hop_position;
/* Preserve hop_limit for router-to-router traffic */
bool router_preserve_hops;
} meshtastic_ModuleConfig_TrafficManagementConfig;
/* Serial Config */
typedef struct _meshtastic_ModuleConfig_SerialConfig {
/* Preferences for the SerialModule */
@@ -468,6 +501,8 @@ typedef struct _meshtastic_ModuleConfig {
meshtastic_ModuleConfig_PaxcounterConfig paxcounter;
/* TODO: REPLACE */
meshtastic_ModuleConfig_StatusMessageConfig statusmessage;
/* Traffic management module config for mesh network optimization */
meshtastic_ModuleConfig_TrafficManagementConfig traffic_management;
} payload_variant;
} meshtastic_ModuleConfig;
@@ -511,6 +546,7 @@ extern "C" {
#define meshtastic_ModuleConfig_AudioConfig_bitrate_ENUMTYPE meshtastic_ModuleConfig_AudioConfig_Audio_Baud
#define meshtastic_ModuleConfig_SerialConfig_baud_ENUMTYPE meshtastic_ModuleConfig_SerialConfig_Serial_Baud
#define meshtastic_ModuleConfig_SerialConfig_mode_ENUMTYPE meshtastic_ModuleConfig_SerialConfig_Serial_Mode
@@ -536,6 +572,7 @@ extern "C" {
#define meshtastic_ModuleConfig_DetectionSensorConfig_init_default {0, 0, 0, 0, "", 0, _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN, 0}
#define meshtastic_ModuleConfig_AudioConfig_init_default {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_PaxcounterConfig_init_default {0, 0, 0, 0}
#define meshtastic_ModuleConfig_TrafficManagementConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_SerialConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0}
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0}
@@ -553,6 +590,7 @@ extern "C" {
#define meshtastic_ModuleConfig_DetectionSensorConfig_init_zero {0, 0, 0, 0, "", 0, _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN, 0}
#define meshtastic_ModuleConfig_AudioConfig_init_zero {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_PaxcounterConfig_init_zero {0, 0, 0, 0}
#define meshtastic_ModuleConfig_TrafficManagementConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_SerialConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0}
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0}
@@ -600,6 +638,20 @@ extern "C" {
#define meshtastic_ModuleConfig_PaxcounterConfig_paxcounter_update_interval_tag 2
#define meshtastic_ModuleConfig_PaxcounterConfig_wifi_threshold_tag 3
#define meshtastic_ModuleConfig_PaxcounterConfig_ble_threshold_tag 4
#define meshtastic_ModuleConfig_TrafficManagementConfig_enabled_tag 1
#define meshtastic_ModuleConfig_TrafficManagementConfig_position_dedup_enabled_tag 2
#define meshtastic_ModuleConfig_TrafficManagementConfig_position_precision_bits_tag 3
#define meshtastic_ModuleConfig_TrafficManagementConfig_position_min_interval_secs_tag 4
#define meshtastic_ModuleConfig_TrafficManagementConfig_nodeinfo_direct_response_tag 5
#define meshtastic_ModuleConfig_TrafficManagementConfig_nodeinfo_direct_response_max_hops_tag 6
#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_enabled_tag 7
#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_window_secs_tag 8
#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_max_packets_tag 9
#define meshtastic_ModuleConfig_TrafficManagementConfig_drop_unknown_enabled_tag 10
#define meshtastic_ModuleConfig_TrafficManagementConfig_unknown_packet_threshold_tag 11
#define meshtastic_ModuleConfig_TrafficManagementConfig_exhaust_hop_telemetry_tag 12
#define meshtastic_ModuleConfig_TrafficManagementConfig_exhaust_hop_position_tag 13
#define meshtastic_ModuleConfig_TrafficManagementConfig_router_preserve_hops_tag 14
#define meshtastic_ModuleConfig_SerialConfig_enabled_tag 1
#define meshtastic_ModuleConfig_SerialConfig_echo_tag 2
#define meshtastic_ModuleConfig_SerialConfig_rxd_tag 3
@@ -685,6 +737,7 @@ extern "C" {
#define meshtastic_ModuleConfig_detection_sensor_tag 12
#define meshtastic_ModuleConfig_paxcounter_tag 13
#define meshtastic_ModuleConfig_statusmessage_tag 14
#define meshtastic_ModuleConfig_traffic_management_tag 15
/* Struct field encoding specification for nanopb */
#define meshtastic_ModuleConfig_FIELDLIST(X, a) \
@@ -701,7 +754,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,neighbor_info,payload_varian
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ambient_lighting,payload_variant.ambient_lighting), 11) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,detection_sensor,payload_variant.detection_sensor), 12) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,paxcounter,payload_variant.paxcounter), 13) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_variant.statusmessage), 14)
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_variant.statusmessage), 14) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,traffic_management,payload_variant.traffic_management), 15)
#define meshtastic_ModuleConfig_CALLBACK NULL
#define meshtastic_ModuleConfig_DEFAULT NULL
#define meshtastic_ModuleConfig_payload_variant_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig
@@ -718,6 +772,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_varian
#define meshtastic_ModuleConfig_payload_variant_detection_sensor_MSGTYPE meshtastic_ModuleConfig_DetectionSensorConfig
#define meshtastic_ModuleConfig_payload_variant_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig
#define meshtastic_ModuleConfig_payload_variant_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig
#define meshtastic_ModuleConfig_payload_variant_traffic_management_MSGTYPE meshtastic_ModuleConfig_TrafficManagementConfig
#define meshtastic_ModuleConfig_MQTTConfig_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, enabled, 1) \
@@ -788,6 +843,24 @@ X(a, STATIC, SINGULAR, INT32, ble_threshold, 4)
#define meshtastic_ModuleConfig_PaxcounterConfig_CALLBACK NULL
#define meshtastic_ModuleConfig_PaxcounterConfig_DEFAULT NULL
#define meshtastic_ModuleConfig_TrafficManagementConfig_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, enabled, 1) \
X(a, STATIC, SINGULAR, BOOL, position_dedup_enabled, 2) \
X(a, STATIC, SINGULAR, UINT32, position_precision_bits, 3) \
X(a, STATIC, SINGULAR, UINT32, position_min_interval_secs, 4) \
X(a, STATIC, SINGULAR, BOOL, nodeinfo_direct_response, 5) \
X(a, STATIC, SINGULAR, UINT32, nodeinfo_direct_response_max_hops, 6) \
X(a, STATIC, SINGULAR, BOOL, rate_limit_enabled, 7) \
X(a, STATIC, SINGULAR, UINT32, rate_limit_window_secs, 8) \
X(a, STATIC, SINGULAR, UINT32, rate_limit_max_packets, 9) \
X(a, STATIC, SINGULAR, BOOL, drop_unknown_enabled, 10) \
X(a, STATIC, SINGULAR, UINT32, unknown_packet_threshold, 11) \
X(a, STATIC, SINGULAR, BOOL, exhaust_hop_telemetry, 12) \
X(a, STATIC, SINGULAR, BOOL, exhaust_hop_position, 13) \
X(a, STATIC, SINGULAR, BOOL, router_preserve_hops, 14)
#define meshtastic_ModuleConfig_TrafficManagementConfig_CALLBACK NULL
#define meshtastic_ModuleConfig_TrafficManagementConfig_DEFAULT NULL
#define meshtastic_ModuleConfig_SerialConfig_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, enabled, 1) \
X(a, STATIC, SINGULAR, BOOL, echo, 2) \
@@ -900,6 +973,7 @@ extern const pb_msgdesc_t meshtastic_ModuleConfig_NeighborInfoConfig_msg;
extern const pb_msgdesc_t meshtastic_ModuleConfig_DetectionSensorConfig_msg;
extern const pb_msgdesc_t meshtastic_ModuleConfig_AudioConfig_msg;
extern const pb_msgdesc_t meshtastic_ModuleConfig_PaxcounterConfig_msg;
extern const pb_msgdesc_t meshtastic_ModuleConfig_TrafficManagementConfig_msg;
extern const pb_msgdesc_t meshtastic_ModuleConfig_SerialConfig_msg;
extern const pb_msgdesc_t meshtastic_ModuleConfig_ExternalNotificationConfig_msg;
extern const pb_msgdesc_t meshtastic_ModuleConfig_StoreForwardConfig_msg;
@@ -919,6 +993,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg;
#define meshtastic_ModuleConfig_DetectionSensorConfig_fields &meshtastic_ModuleConfig_DetectionSensorConfig_msg
#define meshtastic_ModuleConfig_AudioConfig_fields &meshtastic_ModuleConfig_AudioConfig_msg
#define meshtastic_ModuleConfig_PaxcounterConfig_fields &meshtastic_ModuleConfig_PaxcounterConfig_msg
#define meshtastic_ModuleConfig_TrafficManagementConfig_fields &meshtastic_ModuleConfig_TrafficManagementConfig_msg
#define meshtastic_ModuleConfig_SerialConfig_fields &meshtastic_ModuleConfig_SerialConfig_msg
#define meshtastic_ModuleConfig_ExternalNotificationConfig_fields &meshtastic_ModuleConfig_ExternalNotificationConfig_msg
#define meshtastic_ModuleConfig_StoreForwardConfig_fields &meshtastic_ModuleConfig_StoreForwardConfig_msg
@@ -946,6 +1021,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg;
#define meshtastic_ModuleConfig_StatusMessageConfig_size 81
#define meshtastic_ModuleConfig_StoreForwardConfig_size 24
#define meshtastic_ModuleConfig_TelemetryConfig_size 50
#define meshtastic_ModuleConfig_TrafficManagementConfig_size 52
#define meshtastic_ModuleConfig_size 227
#define meshtastic_RemoteHardwarePin_size 21
@@ -21,6 +21,9 @@ PB_BIND(meshtastic_AirQualityMetrics, meshtastic_AirQualityMetrics, AUTO)
PB_BIND(meshtastic_LocalStats, meshtastic_LocalStats, AUTO)
PB_BIND(meshtastic_TrafficManagementStats, meshtastic_TrafficManagementStats, AUTO)
PB_BIND(meshtastic_HealthMetrics, meshtastic_HealthMetrics, AUTO)
+59 -4
View File
@@ -103,7 +103,15 @@ typedef enum _meshtastic_TelemetrySensorType {
/* TSL2561 light sensor */
meshtastic_TelemetrySensorType_TSL2561 = 44,
/* BH1750 light sensor */
meshtastic_TelemetrySensorType_BH1750 = 45
meshtastic_TelemetrySensorType_BH1750 = 45,
/* HDC1080 Temperature and Humidity Sensor */
meshtastic_TelemetrySensorType_HDC1080 = 46,
/* STH21 Temperature and R. Humidity sensor */
meshtastic_TelemetrySensorType_SHT21 = 47,
/* Sensirion STC31 CO2 sensor */
meshtastic_TelemetrySensorType_STC31 = 48,
/* SCD30 CO2, humidity, temperature sensor */
meshtastic_TelemetrySensorType_SCD30 = 49
} meshtastic_TelemetrySensorType;
/* Struct definitions */
@@ -365,6 +373,24 @@ typedef struct _meshtastic_LocalStats {
int32_t noise_floor;
} meshtastic_LocalStats;
/* Traffic management statistics for mesh network optimization */
typedef struct _meshtastic_TrafficManagementStats {
/* Total number of packets inspected by traffic management */
uint32_t packets_inspected;
/* Number of position packets dropped due to deduplication */
uint32_t position_dedup_drops;
/* Number of NodeInfo requests answered from cache */
uint32_t nodeinfo_cache_hits;
/* Number of packets dropped due to rate limiting */
uint32_t rate_limit_drops;
/* Number of unknown/undecryptable packets dropped */
uint32_t unknown_packet_drops;
/* Number of packets with hop_limit exhausted for local-only broadcast */
uint32_t hop_exhausted_packets;
/* Number of times router hop preservation was applied */
uint32_t router_hops_preserved;
} meshtastic_TrafficManagementStats;
/* Health telemetry metrics */
typedef struct _meshtastic_HealthMetrics {
/* Heart rate (beats per minute) */
@@ -424,6 +450,8 @@ typedef struct _meshtastic_Telemetry {
meshtastic_HealthMetrics health_metrics;
/* Linux host metrics */
meshtastic_HostMetrics host_metrics;
/* Traffic management statistics */
meshtastic_TrafficManagementStats traffic_management_stats;
} variant;
} meshtastic_Telemetry;
@@ -461,8 +489,9 @@ extern "C" {
/* Helper constants for enums */
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_BH1750
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_BH1750+1))
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SCD30
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SCD30+1))
@@ -481,6 +510,7 @@ extern "C" {
#define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_TrafficManagementStats_init_default {0, 0, 0, 0, 0, 0, 0}
#define meshtastic_HealthMetrics_init_default {false, 0, false, 0, false, 0}
#define meshtastic_HostMetrics_init_default {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""}
#define meshtastic_Telemetry_init_default {0, 0, {meshtastic_DeviceMetrics_init_default}}
@@ -491,6 +521,7 @@ extern "C" {
#define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_TrafficManagementStats_init_zero {0, 0, 0, 0, 0, 0, 0}
#define meshtastic_HealthMetrics_init_zero {false, 0, false, 0, false, 0}
#define meshtastic_HostMetrics_init_zero {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""}
#define meshtastic_Telemetry_init_zero {0, 0, {meshtastic_DeviceMetrics_init_zero}}
@@ -581,6 +612,13 @@ extern "C" {
#define meshtastic_LocalStats_heap_free_bytes_tag 13
#define meshtastic_LocalStats_num_tx_dropped_tag 14
#define meshtastic_LocalStats_noise_floor_tag 15
#define meshtastic_TrafficManagementStats_packets_inspected_tag 1
#define meshtastic_TrafficManagementStats_position_dedup_drops_tag 2
#define meshtastic_TrafficManagementStats_nodeinfo_cache_hits_tag 3
#define meshtastic_TrafficManagementStats_rate_limit_drops_tag 4
#define meshtastic_TrafficManagementStats_unknown_packet_drops_tag 5
#define meshtastic_TrafficManagementStats_hop_exhausted_packets_tag 6
#define meshtastic_TrafficManagementStats_router_hops_preserved_tag 7
#define meshtastic_HealthMetrics_heart_bpm_tag 1
#define meshtastic_HealthMetrics_spO2_tag 2
#define meshtastic_HealthMetrics_temperature_tag 3
@@ -601,6 +639,7 @@ extern "C" {
#define meshtastic_Telemetry_local_stats_tag 6
#define meshtastic_Telemetry_health_metrics_tag 7
#define meshtastic_Telemetry_host_metrics_tag 8
#define meshtastic_Telemetry_traffic_management_stats_tag 9
#define meshtastic_Nau7802Config_zeroOffset_tag 1
#define meshtastic_Nau7802Config_calibrationFactor_tag 2
#define meshtastic_SEN5XState_last_cleaning_time_tag 1
@@ -714,6 +753,17 @@ X(a, STATIC, SINGULAR, INT32, noise_floor, 15)
#define meshtastic_LocalStats_CALLBACK NULL
#define meshtastic_LocalStats_DEFAULT NULL
#define meshtastic_TrafficManagementStats_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, packets_inspected, 1) \
X(a, STATIC, SINGULAR, UINT32, position_dedup_drops, 2) \
X(a, STATIC, SINGULAR, UINT32, nodeinfo_cache_hits, 3) \
X(a, STATIC, SINGULAR, UINT32, rate_limit_drops, 4) \
X(a, STATIC, SINGULAR, UINT32, unknown_packet_drops, 5) \
X(a, STATIC, SINGULAR, UINT32, hop_exhausted_packets, 6) \
X(a, STATIC, SINGULAR, UINT32, router_hops_preserved, 7)
#define meshtastic_TrafficManagementStats_CALLBACK NULL
#define meshtastic_TrafficManagementStats_DEFAULT NULL
#define meshtastic_HealthMetrics_FIELDLIST(X, a) \
X(a, STATIC, OPTIONAL, UINT32, heart_bpm, 1) \
X(a, STATIC, OPTIONAL, UINT32, spO2, 2) \
@@ -742,7 +792,8 @@ X(a, STATIC, ONEOF, MESSAGE, (variant,air_quality_metrics,variant.air_qual
X(a, STATIC, ONEOF, MESSAGE, (variant,power_metrics,variant.power_metrics), 5) \
X(a, STATIC, ONEOF, MESSAGE, (variant,local_stats,variant.local_stats), 6) \
X(a, STATIC, ONEOF, MESSAGE, (variant,health_metrics,variant.health_metrics), 7) \
X(a, STATIC, ONEOF, MESSAGE, (variant,host_metrics,variant.host_metrics), 8)
X(a, STATIC, ONEOF, MESSAGE, (variant,host_metrics,variant.host_metrics), 8) \
X(a, STATIC, ONEOF, MESSAGE, (variant,traffic_management_stats,variant.traffic_management_stats), 9)
#define meshtastic_Telemetry_CALLBACK NULL
#define meshtastic_Telemetry_DEFAULT NULL
#define meshtastic_Telemetry_variant_device_metrics_MSGTYPE meshtastic_DeviceMetrics
@@ -752,6 +803,7 @@ X(a, STATIC, ONEOF, MESSAGE, (variant,host_metrics,variant.host_metrics),
#define meshtastic_Telemetry_variant_local_stats_MSGTYPE meshtastic_LocalStats
#define meshtastic_Telemetry_variant_health_metrics_MSGTYPE meshtastic_HealthMetrics
#define meshtastic_Telemetry_variant_host_metrics_MSGTYPE meshtastic_HostMetrics
#define meshtastic_Telemetry_variant_traffic_management_stats_MSGTYPE meshtastic_TrafficManagementStats
#define meshtastic_Nau7802Config_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, INT32, zeroOffset, 1) \
@@ -774,6 +826,7 @@ extern const pb_msgdesc_t meshtastic_EnvironmentMetrics_msg;
extern const pb_msgdesc_t meshtastic_PowerMetrics_msg;
extern const pb_msgdesc_t meshtastic_AirQualityMetrics_msg;
extern const pb_msgdesc_t meshtastic_LocalStats_msg;
extern const pb_msgdesc_t meshtastic_TrafficManagementStats_msg;
extern const pb_msgdesc_t meshtastic_HealthMetrics_msg;
extern const pb_msgdesc_t meshtastic_HostMetrics_msg;
extern const pb_msgdesc_t meshtastic_Telemetry_msg;
@@ -786,6 +839,7 @@ extern const pb_msgdesc_t meshtastic_SEN5XState_msg;
#define meshtastic_PowerMetrics_fields &meshtastic_PowerMetrics_msg
#define meshtastic_AirQualityMetrics_fields &meshtastic_AirQualityMetrics_msg
#define meshtastic_LocalStats_fields &meshtastic_LocalStats_msg
#define meshtastic_TrafficManagementStats_fields &meshtastic_TrafficManagementStats_msg
#define meshtastic_HealthMetrics_fields &meshtastic_HealthMetrics_msg
#define meshtastic_HostMetrics_fields &meshtastic_HostMetrics_msg
#define meshtastic_Telemetry_fields &meshtastic_Telemetry_msg
@@ -804,6 +858,7 @@ extern const pb_msgdesc_t meshtastic_SEN5XState_msg;
#define meshtastic_PowerMetrics_size 81
#define meshtastic_SEN5XState_size 27
#define meshtastic_Telemetry_size 272
#define meshtastic_TrafficManagementStats_size 42
#ifdef __cplusplus
} /* extern "C" */
-43
View File
@@ -9,7 +9,6 @@
#if HAS_WIFI
#include "mesh/wifi/WiFiAPClient.h"
#endif
#include "Led.h"
#include "SPILock.h"
#include "power.h"
#include "serialization/JSON.h"
@@ -92,7 +91,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload);
ResourceNode *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks);
ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED);
ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport);
ResourceNode *nodeJsonNodes = new ResourceNode("/json/nodes", "GET", &handleNodes);
ResourceNode *nodeJsonFsBrowseStatic = new ResourceNode("/json/fs/browse/static", "GET", &handleFsBrowseStatic);
@@ -110,7 +108,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
secureServer->registerNode(nodeRestart);
secureServer->registerNode(nodeFormUpload);
secureServer->registerNode(nodeJsonScanNetworks);
secureServer->registerNode(nodeJsonBlinkLED);
secureServer->registerNode(nodeJsonFsBrowseStatic);
secureServer->registerNode(nodeJsonDelete);
secureServer->registerNode(nodeJsonReport);
@@ -133,7 +130,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
insecureServer->registerNode(nodeRestart);
insecureServer->registerNode(nodeFormUpload);
insecureServer->registerNode(nodeJsonScanNetworks);
insecureServer->registerNode(nodeJsonBlinkLED);
insecureServer->registerNode(nodeJsonFsBrowseStatic);
insecureServer->registerNode(nodeJsonDelete);
insecureServer->registerNode(nodeJsonReport);
@@ -904,45 +900,6 @@ void handleRestart(HTTPRequest *req, HTTPResponse *res)
webServerThread->requestRestart = (millis() / 1000) + 5;
}
void handleBlinkLED(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "application/json");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "POST");
ResourceParameters *params = req->getParams();
std::string blink_target;
if (!params->getQueryParameter("blink_target", blink_target)) {
// if no blink_target was supplied in the URL parameters of the
// POST request, then assume we should blink the LED
blink_target = "LED";
}
if (blink_target == "LED") {
uint8_t count = 10;
while (count > 0) {
ledBlink.set(true);
delay(50);
ledBlink.set(false);
delay(50);
count = count - 1;
}
} else {
#if HAS_SCREEN
if (screen)
screen->blink();
#endif
}
JSONObject jsonObjOuter;
jsonObjOuter["status"] = new JSONValue("ok");
JSONValue *value = new JSONValue(jsonObjOuter);
std::string jsonString = value->Stringify();
res->print(jsonString.c_str());
delete value;
}
void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "application/json");
-1
View File
@@ -11,7 +11,6 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res);
void handleScanNetworks(HTTPRequest *req, HTTPResponse *res);
void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res);
void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res);
void handleBlinkLED(HTTPRequest *req, HTTPResponse *res);
void handleReport(HTTPRequest *req, HTTPResponse *res);
void handleNodes(HTTPRequest *req, HTTPResponse *res);
void handleUpdateFs(HTTPRequest *req, HTTPResponse *res);
+11
View File
@@ -106,4 +106,15 @@ const std::string vformat(const char *const zcFormat, ...)
std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs);
va_end(vaArgs);
return std::string(zc.data(), iLen);
}
size_t pb_string_length(const char *str, size_t max_len)
{
size_t len = 0;
for (size_t i = 0; i < max_len; i++) {
if (str[i] != '\0') {
len = i + 1;
}
}
return len;
}
+3
View File
@@ -35,4 +35,7 @@ bool isOneOf(int item, int count, ...);
const std::string vformat(const char *const zcFormat, ...);
// Get actual string length for nanopb char array fields.
size_t pb_string_length(const char *str, size_t max_len);
#define IS_ONE_OF(item, ...) isOneOf(item, sizeof((int[]){__VA_ARGS__}) / sizeof(int), __VA_ARGS__)
+16 -10
View File
@@ -391,7 +391,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
node->has_device_metrics = false;
node->has_position = false;
node->user.public_key.size = 0;
node->user.public_key.bytes[0] = 0;
memset(node->user.public_key.bytes, 0, sizeof(node->user.public_key.bytes));
saveChanges(SEGMENT_NODEDATABASE, false);
}
break;
@@ -643,12 +643,6 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
accelerometerThread->enabled = true;
accelerometerThread->start();
}
#endif
#ifdef LED_PIN
// Turn LED off if heartbeat by config
if (c.payload_variant.device.led_heartbeat_disabled) {
digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON);
}
#endif
if (config.device.button_gpio == c.payload_variant.device.button_gpio &&
config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio &&
@@ -905,10 +899,11 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
{
bool shouldReboot = true;
// If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth
// Otherwise, disable Bluetooth to prevent the phone from interfering with the config
if (!hasOpenEditTransaction &&
!IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) {
if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag,
meshtastic_ModuleConfig_serial_tag, meshtastic_ModuleConfig_statusmessage_tag)) {
disableBluetooth();
}
@@ -1000,8 +995,14 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
moduleConfig.has_paxcounter = true;
moduleConfig.paxcounter = c.payload_variant.paxcounter;
break;
case meshtastic_ModuleConfig_statusmessage_tag:
LOG_INFO("Set module config: StatusMessage");
moduleConfig.has_statusmessage = true;
moduleConfig.statusmessage = c.payload_variant.statusmessage;
shouldReboot = false;
break;
}
saveChanges(SEGMENT_MODULECONFIG);
saveChanges(SEGMENT_MODULECONFIG, shouldReboot);
return true;
}
@@ -1180,6 +1181,11 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
break;
case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG:
LOG_INFO("Get module config: StatusMessage");
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag;
res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage;
break;
}
// NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior.
+39 -30
View File
@@ -6,6 +6,7 @@
#include "configuration.h"
#include "main.h"
#include "mesh/compression/unishox2.h"
#include "meshUtils.h"
#include "meshtastic/atak.pb.h"
AtakPluginModule *atakPluginModule;
@@ -70,16 +71,17 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
auto compressed = cloneTAKPacketData(t);
compressed.is_compressed = true;
if (t->has_contact) {
auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign,
sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
auto length = unishox2_compress_lines(
t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
compressed.contact.callsign, sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet");
return;
}
LOG_DEBUG("Compressed callsign: %d bytes", length);
length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1,
USX_PSET_DFLT, NULL);
length = unishox2_compress_lines(
t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet");
return;
@@ -87,9 +89,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
LOG_DEBUG("Compressed device_callsign: %d bytes", length);
}
if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) {
auto length = unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
compressed.payload_variant.chat.message,
sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
auto length = unishox2_compress_lines(
t->payload_variant.chat.message,
pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
compressed.payload_variant.chat.message, sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet");
return;
@@ -98,9 +102,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to) {
compressed.payload_variant.chat.has_to = true;
length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
compressed.payload_variant.chat.to,
sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
length = unishox2_compress_lines(
t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
compressed.payload_variant.chat.to, sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet");
return;
@@ -110,9 +114,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to_callsign) {
compressed.payload_variant.chat.has_to_callsign = true;
length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
compressed.payload_variant.chat.to_callsign,
sizeof(compressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
length = unishox2_compress_lines(
t->payload_variant.chat.to_callsign,
pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
compressed.payload_variant.chat.to_callsign, sizeof(compressed.payload_variant.chat.to_callsign) - 1,
USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet");
return;
@@ -134,18 +140,18 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
auto uncompressed = cloneTAKPacketData(t);
uncompressed.is_compressed = false;
if (t->has_contact) {
auto length =
unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign,
sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
auto length = unishox2_decompress_lines(
t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
uncompressed.contact.callsign, sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow contact.callsign. Bailing out");
return;
}
LOG_DEBUG("Decompressed callsign: %d bytes", length);
length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
uncompressed.contact.device_callsign,
sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
length = unishox2_decompress_lines(
t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
uncompressed.contact.device_callsign, sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow contact.device_callsign. Bailing out");
return;
@@ -153,9 +159,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
LOG_DEBUG("Decompressed device_callsign: %d bytes", length);
}
if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) {
auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
uncompressed.payload_variant.chat.message,
sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
auto length = unishox2_decompress_lines(
t->payload_variant.chat.message,
pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
uncompressed.payload_variant.chat.message, sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.message. Bailing out");
return;
@@ -164,9 +172,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to) {
uncompressed.payload_variant.chat.has_to = true;
length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
uncompressed.payload_variant.chat.to,
sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
length = unishox2_decompress_lines(
t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
uncompressed.payload_variant.chat.to, sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.to. Bailing out");
return;
@@ -176,10 +184,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to_callsign) {
uncompressed.payload_variant.chat.has_to_callsign = true;
length =
unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
uncompressed.payload_variant.chat.to_callsign,
sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
length = unishox2_decompress_lines(
t->payload_variant.chat.to_callsign,
pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
uncompressed.payload_variant.chat.to_callsign, sizeof(uncompressed.payload_variant.chat.to_callsign) - 1,
USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.to_callsign. Bailing out");
return;
+2 -96
View File
@@ -24,24 +24,8 @@
#include "mesh/generated/meshtastic/rtttl.pb.h"
#include <Arduino.h>
#ifdef HAS_NCP5623
#include <graphics/RAKled.h>
#endif
#ifdef HAS_LP5562
#include <graphics/NomadStarLED.h>
#endif
#ifdef HAS_NEOPIXEL
#include <graphics/NeoPixel.h>
#endif
#ifdef UNPHONE
#include "unPhone.h"
extern unPhone unphone;
#endif
#if defined(HAS_RGB_LED)
#include "AmbientLightingThread.h"
uint8_t red = 0;
uint8_t green = 0;
uint8_t blue = 0;
@@ -123,32 +107,6 @@ int32_t ExternalNotificationModule::runOnce()
green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7
blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7
white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0;
#ifdef HAS_NCP5623
if (rgb_found.type == ScanI2C::NCP5623) {
rgb.setColor(red, green, blue);
}
#endif
#ifdef HAS_LP5562
if (rgb_found.type == ScanI2C::LP5562) {
rgbw.setColor(red, green, blue, white);
}
#endif
#ifdef RGBLED_CA
analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic
analogWrite(RGBLED_GREEN, 255 - green);
analogWrite(RGBLED_BLUE, 255 - blue);
#elif defined(RGBLED_RED)
analogWrite(RGBLED_RED, red);
analogWrite(RGBLED_GREEN, green);
analogWrite(RGBLED_BLUE, blue);
#endif
#ifdef HAS_NEOPIXEL
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
pixels.show();
#endif
#ifdef UNPHONE
unphone.rgb(red, green, blue);
#endif
if (ascending) { // fade in
brightnessIndex++;
if (brightnessIndex == (sizeof(brightnessValues) - 1)) {
@@ -255,34 +213,9 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on)
blue = 0;
white = 0;
}
ambientLightingThread->setLighting(moduleConfig.ambient_lighting.current, red, green, blue);
#endif
#ifdef HAS_NCP5623
if (rgb_found.type == ScanI2C::NCP5623) {
rgb.setColor(red, green, blue);
}
#endif
#ifdef HAS_LP5562
if (rgb_found.type == ScanI2C::LP5562) {
rgbw.setColor(red, green, blue, white);
}
#endif
#ifdef RGBLED_CA
analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic
analogWrite(RGBLED_GREEN, 255 - green);
analogWrite(RGBLED_BLUE, 255 - blue);
#elif defined(RGBLED_RED)
analogWrite(RGBLED_RED, red);
analogWrite(RGBLED_GREEN, green);
analogWrite(RGBLED_BLUE, blue);
#endif
#ifdef HAS_NEOPIXEL
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
pixels.show();
#endif
#ifdef UNPHONE
unphone.rgb(red, green, blue);
#endif
#ifdef HAS_DRV2605
if (on) {
drv.go();
@@ -407,33 +340,6 @@ ExternalNotificationModule::ExternalNotificationModule()
LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio);
}
}
#ifdef HAS_NCP5623
if (rgb_found.type == ScanI2C::NCP5623) {
rgb.begin();
rgb.setCurrent(10);
}
#endif
#ifdef HAS_LP5562
if (rgb_found.type == ScanI2C::LP5562) {
rgbw.begin();
rgbw.setCurrent(20);
}
#endif
#ifdef RGBLED_RED
pinMode(RGBLED_RED, OUTPUT); // set up the RGB led pins
pinMode(RGBLED_GREEN, OUTPUT);
pinMode(RGBLED_BLUE, OUTPUT);
#endif
#ifdef RGBLED_CA
analogWrite(RGBLED_RED, 255); // with a common anode type, logic is reversed
analogWrite(RGBLED_GREEN, 255); // so we want to initialise with lights off
analogWrite(RGBLED_BLUE, 255);
#endif
#ifdef HAS_NEOPIXEL
pixels.begin(); // Initialise the pixel(s)
pixels.clear(); // Set all pixel colors to 'off'
pixels.setBrightness(moduleConfig.ambient_lighting.current);
#endif
} else {
LOG_INFO("External Notification Module Disabled");
disable();
+5
View File
@@ -5,6 +5,11 @@
#include "configuration.h"
#include "input/InputBroker.h"
#ifdef HAS_RGB_LED
#include "AmbientLightingThread.h"
extern AmbientLightingThread *ambientLightingThread;
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6)
#include <NonBlockingRtttl.h>
#else
+12 -3
View File
@@ -1,9 +1,12 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
#include "buzz/BuzzerFeedbackThread.h"
#include "modules/StatusLEDModule.h"
#include "modules/SystemCommandsModule.h"
#endif
#include "modules/StatusLEDModule.h"
#if !MESHTASTIC_EXCLUDE_REPLYBOT
#include "ReplyBotModule.h"
#endif
#if !MESHTASTIC_EXCLUDE_PKI
#include "KeyVerificationModule.h"
#endif
@@ -90,6 +93,9 @@
#if !MESHTASTIC_EXCLUDE_DROPZONE
#include "modules/DropzoneModule.h"
#endif
#if !MESHTASTIC_EXCLUDE_STATUS
#include "modules/StatusMessageModule.h"
#endif
#if defined(HAS_HARDWARE_WATCHDOG)
#include "watchdog/watchdogThread.h"
@@ -106,10 +112,10 @@ void setupModules()
buzzerFeedbackThread = new BuzzerFeedbackThread();
}
#endif
#if defined(LED_CHARGE) || defined(LED_PAIRING)
statusLEDModule = new StatusLEDModule();
#if !MESHTASTIC_EXCLUDE_REPLYBOT
new ReplyBotModule();
#endif
#if !MESHTASTIC_EXCLUDE_ADMIN
adminModule = new AdminModule();
#endif
@@ -150,6 +156,9 @@ void setupModules()
#if !MESHTASTIC_EXCLUDE_DROPZONE
dropzoneModule = new DropzoneModule();
#endif
#if !MESHTASTIC_EXCLUDE_STATUS
statusMessageModule = new StatusMessageModule();
#endif
#if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE
new GenericThreadModule();
#endif
+1 -1
View File
@@ -148,7 +148,7 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
// Strip the public key if the user is licensed
if (u.is_licensed && u.public_key.size > 0) {
u.public_key.bytes[0] = 0;
memset(u.public_key.bytes, 0, sizeof(u.public_key.bytes));
u.public_key.size = 0;
}
+4 -3
View File
@@ -1,5 +1,4 @@
#include "PowerStressModule.h"
#include "Led.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PowerMon.h"
@@ -78,10 +77,12 @@ int32_t PowerStressModule::runOnce()
switch (p.cmd) {
case meshtastic_PowerStressMessage_Opcode_LED_ON:
ledForceOn.set(true);
// FIXME - implement
// ledForceOn.set(true);
break;
case meshtastic_PowerStressMessage_Opcode_LED_OFF:
ledForceOn.set(false);
// FIXME - implement
// ledForceOn.set(false);
break;
case meshtastic_PowerStressMessage_Opcode_GPS_ON:
// FIXME - implement
+183
View File
@@ -0,0 +1,183 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_REPLYBOT
/*
* ReplyBotModule.cpp
*
* This module implements a simple reply bot for the Meshtastic firmware. It listens for
* specific text commands ("/ping", "/hello" and "/test") delivered either via a direct
* message (DM) or a broadcast on the primary channel. When a supported command is
* received the bot responds with a short status message that includes the hop count
* (minimum number of relays), RSSI and SNR of the received packet. To avoid spamming
* the network it enforces a persender cooldown between responses. By default the
* module is disabled. See the official firmware documentation for guidance on adding modules.
* To enable this module, set `#undef MESHTASTIC_EXCLUDE_REPLYBOT` in your variant.h file.
*/
#include "Channels.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "ReplyBotModule.h"
#include "mesh/MeshTypes.h"
#include <Arduino.h>
#include <cctype>
#include <cstring>
//
// Rate limiting data structures
//
// Each sender is tracked in a small ring buffer. When a message arrives from a
// sender we check the last time we responded to them. If the difference is
// less than the configured cooldown (different values for DM vs broadcast)
// the message is ignored; otherwise we update the last response time and
// proceed with replying.
struct ReplyBotCooldownEntry {
uint32_t from = 0;
uint32_t lastMs = 0;
};
static constexpr uint8_t REPLYBOT_COOLDOWN_SLOTS = 8; // ring buffer size
static constexpr uint32_t REPLYBOT_DM_COOLDOWN_MS = 15 * 1000; // 15 seconds for DMs
static constexpr uint32_t REPLYBOT_LF_COOLDOWN_MS = 60 * 1000; // 60 seconds for LongFast broadcasts
static ReplyBotCooldownEntry replybotCooldown[REPLYBOT_COOLDOWN_SLOTS];
static uint8_t replybotCooldownIdx = 0;
// Return true if a reply should be ratelimited for this sender, updating the
// entry table as needed.
static bool replybotRateLimited(uint32_t from, uint32_t cooldownMs)
{
const uint32_t now = millis();
for (auto &e : replybotCooldown) {
if (e.from == from) {
// Found existing entry; check if cooldown expired
if ((uint32_t)(now - e.lastMs) < cooldownMs) {
return true;
}
e.lastMs = now;
return false;
}
}
// No entry found insert new sender into the ring
replybotCooldown[replybotCooldownIdx].from = from;
replybotCooldown[replybotCooldownIdx].lastMs = now;
replybotCooldownIdx = (replybotCooldownIdx + 1) % REPLYBOT_COOLDOWN_SLOTS;
return false;
}
// Constructor registers a single text port and marks the module promiscuous
// so that broadcast messages on the primary channel are visible.
ReplyBotModule::ReplyBotModule() : SinglePortModule("replybot", meshtastic_PortNum_TEXT_MESSAGE_APP)
{
isPromiscuous = true;
}
void ReplyBotModule::setup()
{
// In future we may add a protobuf configuration; for now the module is
// always enabled when compiled in.
}
// Determine whether we want to process this packet. We only care about
// plain text messages addressed to our port.
bool ReplyBotModule::wantPacket(const meshtastic_MeshPacket *p)
{
return (p && p->decoded.portnum == ourPortNum);
}
ProcessMessage ReplyBotModule::handleReceived(const meshtastic_MeshPacket &mp)
{
// Accept only direct messages to us or broadcasts on the Primary channel
// (regardless of modem preset: LongFast, MediumFast, etc).
const uint32_t ourNode = nodeDB->getNodeNum();
const bool isDM = (mp.to == ourNode);
const bool isPrimaryChannel = (mp.channel == channels.getPrimaryIndex()) && isBroadcast(mp.to);
if (!isDM && !isPrimaryChannel) {
return ProcessMessage::CONTINUE;
}
// Ignore empty payloads
if (mp.decoded.payload.size == 0) {
return ProcessMessage::CONTINUE;
}
// Copy payload into a nullterminated buffer
char buf[260];
memset(buf, 0, sizeof(buf));
size_t n = mp.decoded.payload.size;
if (n > sizeof(buf) - 1)
n = sizeof(buf) - 1;
memcpy(buf, mp.decoded.payload.bytes, n);
// React only to supported slash commands
if (!isCommand(buf)) {
return ProcessMessage::CONTINUE;
}
// Apply rate limiting per sender depending on DM/broadcast
const uint32_t cooldownMs = isDM ? REPLYBOT_DM_COOLDOWN_MS : REPLYBOT_LF_COOLDOWN_MS;
if (replybotRateLimited(mp.from, cooldownMs)) {
return ProcessMessage::CONTINUE;
}
// Compute hop count indicator if the relay_node is nonzero we know
// there was at least one relay. Some firmware builds support a hop_start
// field which could be used for more accurate counts, but here we use
// the available relay_node flag only.
// int hopsAway = mp.hop_start - mp.hop_limit;
int hopsAway = getHopsAway(mp);
// Normalize RSSI: if positive adjust down by 200 to align with typical values
int rssi = mp.rx_rssi;
if (rssi > 0) {
rssi -= 200;
}
float snr = mp.rx_snr;
// Build the reply message and send it back via DM
char reply[96];
snprintf(reply, sizeof(reply), "🎙️ Mic Check : %d Hops away | RSSI %d | SNR %.1f", hopsAway, rssi, snr);
sendDm(mp, reply);
return ProcessMessage::CONTINUE;
}
// Check if the message starts with one of the supported commands. Leading
// whitespace is skipped and commands must be followed by endofstring or
// whitespace.
bool ReplyBotModule::isCommand(const char *msg) const
{
if (!msg)
return false;
while (*msg == ' ' || *msg == '\t')
msg++;
auto isEndOrSpace = [](char c) { return c == '\0' || std::isspace(static_cast<unsigned char>(c)); };
if (strncmp(msg, "/ping", 5) == 0 && isEndOrSpace(msg[5]))
return true;
if (strncmp(msg, "/hello", 6) == 0 && isEndOrSpace(msg[6]))
return true;
if (strncmp(msg, "/test", 5) == 0 && isEndOrSpace(msg[5]))
return true;
return false;
}
// Send a direct message back to the originating node.
void ReplyBotModule::sendDm(const meshtastic_MeshPacket &rx, const char *text)
{
if (!text)
return;
meshtastic_MeshPacket *p = allocDataPacket();
p->to = rx.from;
p->channel = rx.channel;
p->want_ack = false;
p->decoded.want_response = false;
size_t len = strlen(text);
if (len > sizeof(p->decoded.payload.bytes)) {
len = sizeof(p->decoded.payload.bytes);
}
p->decoded.payload.size = len;
memcpy(p->decoded.payload.bytes, text, len);
service->sendToMesh(p);
}
#endif // MESHTASTIC_EXCLUDE_REPLYBOT
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_REPLYBOT
#include "SinglePortModule.h"
#include "mesh/generated/meshtastic/mesh.pb.h"
class ReplyBotModule : public SinglePortModule
{
public:
ReplyBotModule();
void setup() override;
bool wantPacket(const meshtastic_MeshPacket *p) override;
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
protected:
bool isCommand(const char *msg) const;
void sendDm(const meshtastic_MeshPacket &rx, const char *text);
};
#endif // MESHTASTIC_EXCLUDE_REPLYBOT
+87 -7
View File
@@ -13,8 +13,10 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule")
{
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
powerStatusObserver.observe(&powerStatus->onNewStatus);
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
if (inputBroker)
inputObserver.observe(inputBroker);
#endif
}
int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
@@ -62,19 +64,22 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
}
return 0;
};
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
int StatusLEDModule::handleInputEvent(const InputEvent *event)
{
lastUserbuttonTime = millis();
return 0;
}
#endif
int32_t StatusLEDModule::runOnce()
{
my_interval = 1000;
if (power_state == charging) {
#ifndef POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING
CHARGE_LED_state = !CHARGE_LED_state;
#endif
} else if (power_state == charged) {
CHARGE_LED_state = LED_STATE_ON;
} else if (power_state == critical) {
@@ -88,13 +93,19 @@ int32_t StatusLEDModule::runOnce()
my_interval = 250;
if (POWER_LED_starttime + 2000 < millis()) {
doing_fast_blink = false;
CHARGE_LED_state = LED_STATE_OFF;
}
} else {
CHARGE_LED_state = LED_STATE_OFF;
}
}
} else {
CHARGE_LED_state = LED_STATE_OFF;
if (power_state != charging && power_state != charged && !doing_fast_blink) {
if (CHARGE_LED_state == LED_STATE_ON) {
CHARGE_LED_state = LED_STATE_OFF;
my_interval = 999;
} else {
CHARGE_LED_state = LED_STATE_ON;
my_interval = 1;
}
}
if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) {
@@ -112,6 +123,11 @@ int32_t StatusLEDModule::runOnce()
PAIRING_LED_state = LED_STATE_ON;
}
// Override if disabled in config
if (config.device.led_heartbeat_disabled) {
CHARGE_LED_state = LED_STATE_OFF;
}
#ifdef Battery_LED_1
bool chargeIndicatorLED1 = LED_STATE_OFF;
bool chargeIndicatorLED2 = LED_STATE_OFF;
bool chargeIndicatorLED3 = LED_STATE_OFF;
@@ -126,14 +142,38 @@ int32_t StatusLEDModule::runOnce()
if (powerStatus && powerStatus->getBatteryChargePercent() >= 75)
chargeIndicatorLED4 = LED_STATE_ON;
}
#endif
#ifdef LED_CHARGE
digitalWrite(LED_CHARGE, CHARGE_LED_state);
#if defined(HAS_PMU)
if (pmu_found && PMU) {
// blink the axp led
PMU->setChargingLedMode(CHARGE_LED_state ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
}
#endif
#ifdef PCA_LED_POWER
io.digitalWrite(PCA_LED_POWER, CHARGE_LED_state);
#endif
#ifdef PCA_LED_ENABLE
io.digitalWrite(PCA_LED_ENABLE, CHARGE_LED_state);
#endif
#ifdef LED_POWER
digitalWrite(LED_POWER, CHARGE_LED_state);
#endif
#ifdef LED_PAIRING
digitalWrite(LED_PAIRING, PAIRING_LED_state);
#endif
#ifdef RGB_LED_POWER
if (!config.device.led_heartbeat_disabled) {
if (CHARGE_LED_state == LED_STATE_ON) {
ambientLightingThread->setLighting(10, 255, 0, 0);
} else {
ambientLightingThread->setLighting(0, 0, 0, 0);
}
}
#endif
#ifdef Battery_LED_1
digitalWrite(Battery_LED_1, chargeIndicatorLED1);
#endif
@@ -149,3 +189,43 @@ int32_t StatusLEDModule::runOnce()
return (my_interval);
}
void StatusLEDModule::setPowerLED(bool LEDon)
{
#if defined(HAS_PMU)
if (pmu_found && PMU) {
// blink the axp led
PMU->setChargingLedMode(LEDon ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
}
#endif
if (LEDon)
LEDon = LED_STATE_ON;
else
LEDon = LED_STATE_OFF;
#ifdef PCA_LED_POWER
io.digitalWrite(PCA_LED_POWER, LEDon);
#endif
#ifdef PCA_LED_ENABLE
io.digitalWrite(PCA_LED_ENABLE, LEDon);
#endif
#ifdef LED_POWER
digitalWrite(LED_POWER, LEDon);
#endif
#ifdef LED_PAIRING
digitalWrite(LED_PAIRING, LEDon);
#endif
#ifdef Battery_LED_1
digitalWrite(Battery_LED_1, LEDon);
#endif
#ifdef Battery_LED_2
digitalWrite(Battery_LED_2, LEDon);
#endif
#ifdef Battery_LED_3
digitalWrite(Battery_LED_3, LEDon);
#endif
#ifdef Battery_LED_4
digitalWrite(Battery_LED_4, LEDon);
#endif
}
+15 -2
View File
@@ -5,10 +5,14 @@
#include "PowerStatus.h"
#include "concurrency/OSThread.h"
#include "configuration.h"
#include "input/InputBroker.h"
#include "main.h"
#include <Arduino.h>
#include <functional>
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
#include "input/InputBroker.h"
#endif
class StatusLEDModule : private concurrency::OSThread
{
bool slowTrack = false;
@@ -17,8 +21,11 @@ class StatusLEDModule : private concurrency::OSThread
StatusLEDModule();
int handleStatusUpdate(const meshtastic::Status *);
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
int handleInputEvent(const InputEvent *arg);
#endif
void setPowerLED(bool);
protected:
unsigned int my_interval = 1000; // interval in millisconds
@@ -28,8 +35,10 @@ class StatusLEDModule : private concurrency::OSThread
CallbackObserver<StatusLEDModule, const meshtastic::Status *>(this, &StatusLEDModule::handleStatusUpdate);
CallbackObserver<StatusLEDModule, const meshtastic::Status *> powerStatusObserver =
CallbackObserver<StatusLEDModule, const meshtastic::Status *>(this, &StatusLEDModule::handleStatusUpdate);
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
CallbackObserver<StatusLEDModule, const InputEvent *> inputObserver =
CallbackObserver<StatusLEDModule, const InputEvent *>(this, &StatusLEDModule::handleInputEvent);
#endif
private:
bool CHARGE_LED_state = LED_STATE_OFF;
@@ -50,3 +59,7 @@ class StatusLEDModule : private concurrency::OSThread
};
extern StatusLEDModule *statusLEDModule;
#ifdef RGB_LED_POWER
#include "AmbientLightingThread.h"
extern AmbientLightingThread *ambientLightingThread;
#endif
+41
View File
@@ -0,0 +1,41 @@
#if !MESHTASTIC_EXCLUDE_STATUS
#include "StatusMessageModule.h"
#include "MeshService.h"
#include "ProtobufModule.h"
StatusMessageModule *statusMessageModule;
int32_t StatusMessageModule::runOnce()
{
if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') {
// create and send message with the status message set
meshtastic_StatusMessage ourStatus = meshtastic_StatusMessage_init_zero;
strncpy(ourStatus.status, moduleConfig.statusmessage.node_status, sizeof(ourStatus.status));
ourStatus.status[sizeof(ourStatus.status) - 1] = '\0'; // ensure null termination
meshtastic_MeshPacket *p = allocDataPacket();
p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes),
meshtastic_StatusMessage_fields, &ourStatus);
p->to = NODENUM_BROADCAST;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0;
service->sendToMesh(p);
}
return 1000 * 12 * 60 * 60;
}
ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
{
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
meshtastic_StatusMessage incomingMessage;
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields,
&incomingMessage)) {
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
}
}
return ProcessMessage::CONTINUE;
}
#endif
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#if !MESHTASTIC_EXCLUDE_STATUS
#include "SinglePortModule.h"
#include "configuration.h"
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
{
public:
/** Constructor
* name is for debugging output
*/
StatusMessageModule()
: SinglePortModule("statusMessage", meshtastic_PortNum_NODE_STATUS_APP), concurrency::OSThread("StatusMessage")
{
if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') {
this->setInterval(2 * 60 * 1000);
} else {
this->setInterval(1000 * 12 * 60 * 60);
}
// TODO: If we have a string, set the initial delay (15 minutes maybe)
}
virtual int32_t runOnce() override;
protected:
/** Called to handle a particular incoming message
*/
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
private:
};
extern StatusMessageModule *statusMessageModule;
#endif
+73 -21
View File
@@ -1,3 +1,4 @@
#include "DebugConfiguration.h"
#include "configuration.h"
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
@@ -22,6 +23,12 @@
#include "Sensor/AddI2CSensorTemplate.h"
#include "Sensor/PMSA003ISensor.h"
#include "Sensor/SEN5XSensor.h"
#if __has_include(<SensirionI2cScd4x.h>)
#include "Sensor/SCD4XSensor.h"
#endif
#if __has_include(<SensirionI2cSfa3x.h>)
#include "Sensor/SFA30Sensor.h"
#endif
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{
@@ -44,6 +51,12 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
// order by priority of metrics/values (low top, high bottom)
addSensor<PMSA003ISensor>(i2cScanner, ScanI2C::DeviceType::PMSA003I);
addSensor<SEN5XSensor>(i2cScanner, ScanI2C::DeviceType::SEN5X);
#if __has_include(<SensirionI2cScd4x.h>)
addSensor<SCD4XSensor>(i2cScanner, ScanI2C::DeviceType::SCD4X);
#endif
#if __has_include(<SensirionI2cSfa3x.h>)
addSensor<SFA30Sensor>(i2cScanner, ScanI2C::DeviceType::SFA30);
#endif
}
int32_t AirQualityTelemetryModule::runOnce()
@@ -65,7 +78,8 @@ int32_t AirQualityTelemetryModule::runOnce()
}
if (firstTime) {
// This is the first time the OSThread library has called this function, so do some setup
// This is the first time the OSThread library has called this function, so
// do some setup
firstTime = false;
if (moduleConfig.telemetry.air_quality_enabled) {
@@ -131,9 +145,9 @@ int32_t AirQualityTelemetryModule::runOnce()
LOG_DEBUG("Sending sensors to sleep");
for (TelemetrySensor *sensor : sensors) {
if (sensor->isActive() && sensor->canSleep()) {
if (sensor->wakeUpTimeMs() < Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs,
numOnlineNodes)) {
if (sensor->wakeUpTimeMs() <
(int32_t)Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes)) {
LOG_DEBUG("Disabling %s until next period", sensor->sensorName);
sensor->sleep();
} else {
@@ -186,7 +200,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
const auto &m = telemetry.variant.air_quality_metrics;
// Check if any telemetry field has valid data
bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard;
bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_co2;
if (!hasAny) {
display->drawString(x, currentY, "No Telemetry");
@@ -213,6 +227,10 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3");
if (m.has_pm100_standard)
entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3");
if (m.has_co2)
entries.push_back("CO2: " + String(m.co2) + "ppm");
if (m.has_form_formaldehyde)
entries.push_back("HCHO: " + String(m.form_formaldehyde) + "ppb");
// === Show first available metric on top-right of first line ===
if (!entries.empty()) {
@@ -248,14 +266,19 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
const char *sender = getSenderShortName(mp);
LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender,
t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
t->variant.air_quality_metrics.pm100_standard);
if (t->variant.air_quality_metrics.has_pm10_standard)
LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, "
"pm100_standard=%i",
sender, t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
t->variant.air_quality_metrics.pm100_standard);
// TODO - Decide what to do with these
// LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
// t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
// t->variant.air_quality_metrics.pm100_environmental);
if (t->variant.air_quality_metrics.has_co2)
LOG_INFO("CO2=%i, CO2_T=%.2f, CO2_H=%.2f", t->variant.air_quality_metrics.co2,
t->variant.air_quality_metrics.co2_temperature, t->variant.air_quality_metrics.co2_humidity);
if (t->variant.air_quality_metrics.has_form_formaldehyde)
LOG_INFO("HCHO=%.2f, HCHO_T=%.2f, HCHO_H=%.2f", t->variant.air_quality_metrics.form_formaldehyde,
t->variant.air_quality_metrics.form_temperature, t->variant.air_quality_metrics.form_humidity);
#endif
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
@@ -269,15 +292,20 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
{
bool valid = true;
// Note: this is different to the case in EnvironmentTelemetryModule
// There, if any sensor fails to read - valid = false.
bool valid = false;
bool hasSensor = false;
m->time = getTime();
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero;
bool sensor_get = false;
for (TelemetrySensor *sensor : sensors) {
LOG_DEBUG("Reading %s", sensor->sensorName);
valid = valid && sensor->getMetrics(m);
// Note - this function doesn't get properly called if within a conditional
sensor_get = sensor->getMetrics(m);
valid = valid || sensor_get;
hasSensor = true;
}
@@ -319,12 +347,36 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
m.time = getTime();
if (getAirQualityTelemetry(&m)) {
LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", m.variant.air_quality_metrics.pm10_standard,
m.variant.air_quality_metrics.pm25_standard, m.variant.air_quality_metrics.pm100_standard);
if (m.variant.air_quality_metrics.has_pm10_environmental)
LOG_INFO("pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u",
m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental,
m.variant.air_quality_metrics.pm100_environmental);
bool hasAnyPM =
m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard ||
m.variant.air_quality_metrics.has_pm100_standard || m.variant.air_quality_metrics.has_pm10_environmental ||
m.variant.air_quality_metrics.has_pm25_environmental || m.variant.air_quality_metrics.has_pm100_environmental;
if (hasAnyPM) {
LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", m.variant.air_quality_metrics.pm10_standard,
m.variant.air_quality_metrics.pm25_standard, m.variant.air_quality_metrics.pm100_standard);
if (m.variant.air_quality_metrics.has_pm10_environmental)
LOG_INFO("pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u",
m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental,
m.variant.air_quality_metrics.pm100_environmental);
}
bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature ||
m.variant.air_quality_metrics.has_co2_humidity;
if (hasAnyCO2) {
LOG_INFO("Send: co2=%i, co2_t=%.2f, co2_rh=%.2f", m.variant.air_quality_metrics.co2,
m.variant.air_quality_metrics.co2_temperature, m.variant.air_quality_metrics.co2_humidity);
}
bool hasAnyHCHO = m.variant.air_quality_metrics.has_form_formaldehyde ||
m.variant.air_quality_metrics.has_form_temperature || m.variant.air_quality_metrics.has_form_humidity;
if (hasAnyHCHO) {
LOG_INFO("Send: hcho=%.2f, hcho_t=%.2f, hcho_rh=%.2f", m.variant.air_quality_metrics.form_formaldehyde,
m.variant.air_quality_metrics.form_temperature, m.variant.air_quality_metrics.form_humidity);
}
meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest;
@@ -394,4 +446,4 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(
return result;
}
#endif
#endif
+2 -1
View File
@@ -64,8 +64,9 @@ class AirQualityTelemetryModule : private concurrency::OSThread,
bool firstTime = true;
meshtastic_MeshPacket *lastMeasurementPacket;
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
// uint32_t sendToPhoneIntervalMs = 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0;
};
#endif
#endif
@@ -138,6 +138,10 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
measurement->variant.air_quality_metrics.has_particles_100um = true;
measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26);
LOG_DEBUG("Got %s readings: pM1p0_standard=%u, pM2p5_standard=%u, pM10p0_standard=%u", sensorName,
measurement->variant.air_quality_metrics.pm10_standard, measurement->variant.air_quality_metrics.pm25_standard,
measurement->variant.air_quality_metrics.pm100_standard);
return true;
}
@@ -0,0 +1,926 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cScd4x.h>)
#include "../detect/reClockI2C.h"
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "SCD4XSensor.h"
#define SCD4X_NO_ERROR 0
SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {}
bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
{
LOG_INFO("Init sensor: %s", sensorName);
_bus = bus;
_address = dev->address.address;
#ifdef SCD4X_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SCD4X_I2C_CLOCK_SPEED */
scd4x.begin(*_bus, _address);
// From SCD4X library
delay(30);
// Stop periodic measurement
if (!stopMeasurement()) {
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
// Get sensor variant
scd4x.getSensorVariant(sensorVariant);
if (sensorVariant == SCD4X_SENSOR_VARIANT_SCD41) {
LOG_INFO("%s: Found SCD41", sensorName);
if (!powerUp()) {
LOG_ERROR("%s: Error trying to execute powerUp()", sensorName);
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
}
if (!getASC(ascActive)) {
LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName);
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
// Start measurement in selected power mode (low power by default)
if (!startMeasurement()) {
LOG_ERROR("%s: Couldn't start measurement", sensorName);
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
if (state == SCD4X_MEASUREMENT) {
status = 1;
} else {
status = 0;
}
initI2CSensor();
return true;
}
bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement)
{
if (state != SCD4X_MEASUREMENT) {
LOG_ERROR("%s: Not in measurement mode", sensorName);
return false;
}
uint16_t co2, error;
float temperature, humidity;
#ifdef SCD4X_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SCD4X_I2C_CLOCK_SPEED */
bool dataReady;
error = scd4x.getDataReadyStatus(dataReady);
if (!dataReady) {
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
LOG_ERROR("SCD4X: Data is not ready");
return false;
}
error = scd4x.readMeasurement(co2, temperature, humidity);
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum%.2f", sensorName, co2, temperature, humidity);
if (error != SCD4X_NO_ERROR) {
LOG_DEBUG("%s: Error while getting measurements: %u", sensorName, error);
if (co2 == 0) {
LOG_ERROR("%s: Skipping invalid measurement.", sensorName);
}
return false;
} else {
measurement->variant.air_quality_metrics.has_co2_temperature = true;
measurement->variant.air_quality_metrics.has_co2_humidity = true;
measurement->variant.air_quality_metrics.has_co2 = true;
measurement->variant.air_quality_metrics.co2_temperature = temperature;
measurement->variant.air_quality_metrics.co2_humidity = humidity;
measurement->variant.air_quality_metrics.co2 = co2;
return true;
}
}
/**
* @brief Perform a forced recalibration (FRC) of the CO concentration.
*
* From Sensirion SCD4X I2C Library
*
* 1. Operate the SCD4x in the operation mode later used for normal sensor
* operation (e.g. periodic measurement) for at least 3 minutes in an
* environment with a homogenous and constant CO2 concentration. The sensor
* must be operated at the voltage desired for the application when
* performing the FRC sequence. 2. Issue the stop_periodic_measurement
* command. 3. Issue the perform_forced_recalibration command.
* @note This function should not change the clock
*/
bool SCD4XSensor::performFRC(uint32_t targetCO2)
{
uint16_t error, frcCorr;
LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName);
if (!stopMeasurement()) {
return false;
}
LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2);
error = scd4x.performForcedRecalibration((uint16_t)targetCO2, frcCorr);
// SCD4X Sensirion datasheet
delay(400);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName);
return false;
}
if (frcCorr == 0xFFFF) {
LOG_ERROR("%s: Error while performing forced recalibration.", sensorName);
return false;
}
LOG_INFO("%s: FRC Correction successful. Correction output: %u", sensorName, (uint16_t)(frcCorr - 0x8000));
return true;
}
/**
* @brief Start measurement mode
* @note This function should not change the clock
*/
bool SCD4XSensor::startMeasurement()
{
uint16_t error;
if (state == SCD4X_MEASUREMENT) {
LOG_DEBUG("%s: Already in measurement mode", sensorName);
return true;
}
if (lowPower) {
error = scd4x.startLowPowerPeriodicMeasurement();
} else {
error = scd4x.startPeriodicMeasurement();
}
if (error == SCD4X_NO_ERROR) {
LOG_INFO("%s: Started measurement mode", sensorName);
if (lowPower) {
LOG_INFO("%s: Low power mode", sensorName);
} else {
LOG_INFO("%s: Normal power mode", sensorName);
}
state = SCD4X_MEASUREMENT;
return true;
} else {
LOG_ERROR("%s: Couldn't start measurement mode", sensorName);
return false;
}
}
/**
* @brief Stop measurement mode
* @note This function should not change the clock
*/
bool SCD4XSensor::stopMeasurement()
{
uint16_t error;
error = scd4x.stopPeriodicMeasurement();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to set idle mode on SCD4X.", sensorName);
return false;
}
state = SCD4X_IDLE;
co2MeasureStarted = 0;
return true;
}
/**
* @brief Set power mode
* Pass true to set low power mode
* @note This function should not change the clock
*/
bool SCD4XSensor::setPowerMode(bool _lowPower)
{
lowPower = _lowPower;
if (!stopMeasurement()) {
return false;
}
if (lowPower) {
LOG_DEBUG("%s: Set low power mode", sensorName);
} else {
LOG_DEBUG("%s: Set normal power mode", sensorName);
}
return true;
}
/**
* @brief Check the current mode (ASC or FRC)
* From Sensirion SCD4X I2C Library
* @note This function should not change the clock
*/
bool SCD4XSensor::getASC(uint16_t &_ascActive)
{
uint16_t error;
LOG_INFO("%s: Getting ASC", sensorName);
if (!stopMeasurement()) {
return false;
}
error = scd4x.getAutomaticSelfCalibrationEnabled(_ascActive);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to send command.", sensorName);
return false;
}
if (_ascActive) {
LOG_INFO("%s: ASC is enabled", sensorName);
} else {
LOG_INFO("%s: FRC is enabled", sensorName);
}
return true;
}
/**
* @brief Enable or disable automatic self calibration (ASC).
*
* From Sensirion SCD4X I2C Library
*
* Sets the current state (enabled / disabled) of the ASC. By default, ASC
* is enabled.
* @note This function should not change the clock
*/
bool SCD4XSensor::setASC(bool ascEnabled)
{
uint16_t error;
if (ascEnabled) {
LOG_INFO("%s: Enabling ASC", sensorName);
} else {
LOG_INFO("%s: Disabling ASC", sensorName);
}
if (!stopMeasurement()) {
return false;
}
error = scd4x.setAutomaticSelfCalibrationEnabled((uint16_t)ascEnabled);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to send command.", sensorName);
return false;
}
error = scd4x.persistSettings();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to make settings persistent.", sensorName);
return false;
}
if (!getASC(ascActive)) {
LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName);
return false;
}
return true;
}
/**
* @brief Set the value of ASC baseline target in ppm.
*
* From Sensirion SCD4X I2C Library.
*
* Sets the value of the ASC baseline target, i.e. the CO concentration in
* ppm which the ASC algorithm will assume as lower-bound background to
* which the SCD4x is exposed to regularly within one ASC period of
* operation. To save the setting to the EEPROM, the persist_settings
* command must be issued subsequently. The factory default value is 400
* ppm.
* @note This function should not change the clock
*/
bool SCD4XSensor::setASCBaseline(uint32_t targetCO2)
{
// TODO - Remove?
// Available in library, but not described in datasheet.
uint16_t error;
LOG_INFO("%s: Setting ASC baseline to: %u", sensorName, targetCO2);
getASC(ascActive);
if (!ascActive) {
LOG_ERROR("%s: Can't set ASC baseline. ASC is not active", sensorName);
return false;
}
if (!stopMeasurement()) {
return false;
}
error = scd4x.setAutomaticSelfCalibrationTarget((uint16_t)targetCO2);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to send command.", sensorName);
return false;
}
error = scd4x.persistSettings();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to make settings persistent.", sensorName);
return false;
}
LOG_INFO("%s: Setting ASC baseline successful", sensorName);
return true;
}
/**
* @brief Set the temperature compensation reference.
*
* From Sensirion SCD4X I2C Library.
*
* Setting the temperature offset of the SCD4x inside the customer device
* allows the user to optimize the RH and T output signal.
* By default, the temperature offset is set to 4 °C. To save
* the setting to the EEPROM, the persist_settings command may be issued.
* Equation (1) details how the characteristic temperature offset can be
* calculated using the current temperature output of the sensor (TSCD4x), a
* reference temperature value (TReference), and the previous temperature
* offset (Toffset_pervious) obtained using the get_temperature_offset_raw
* command:
*
* Toffset_actual = TSCD4x - TReference + Toffset_pervious.
*
* Recommended temperature offset values are between 0 °C and 20 °C. The
* temperature offset does not impact the accuracy of the CO2 output.
* @note This function should not change the clock
*/
bool SCD4XSensor::setTemperature(float tempReference)
{
uint16_t error;
float prevTempOffset;
float updatedTempOffset;
float tempOffset;
bool dataReady;
uint16_t co2;
float temperature;
float humidity;
LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference);
error = scd4x.getDataReadyStatus(dataReady);
if (!dataReady) {
LOG_ERROR("%s: Data is not ready", sensorName);
return false;
}
error = scd4x.readMeasurement(co2, temperature, humidity);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error);
return false;
}
LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature);
if (!stopMeasurement()) {
return false;
}
error = scd4x.getTemperatureOffset(prevTempOffset);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to get temperature offset. Error code: %u", sensorName, error);
return false;
}
LOG_INFO("%s: Current sensor temperature offset: %.2f", sensorName, prevTempOffset);
tempOffset = temperature - tempReference + prevTempOffset;
LOG_INFO("%s: Setting temperature offset: %.2f", sensorName, tempOffset);
error = scd4x.setTemperatureOffset(tempOffset);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error);
return false;
}
error = scd4x.persistSettings();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
return false;
}
scd4x.getTemperatureOffset(updatedTempOffset);
LOG_INFO("%s: Updated sensor temperature offset: %.2f", sensorName, updatedTempOffset);
return true;
}
/**
* @brief Get the sensor altitude.
*
* From Sensirion SCD4X I2C Library.
*
* Altitude in meters above sea level can be set after device installation.
* Valid value between 0 and 3000m. This overrides pressure offset.
* @note This function should not change the clock
*/
bool SCD4XSensor::getAltitude(uint16_t &altitude)
{
uint16_t error;
LOG_INFO("%s: Requesting sensor altitude", sensorName);
if (!stopMeasurement()) {
return false;
}
error = scd4x.getSensorAltitude(altitude);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error);
return false;
}
LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude);
return true;
}
/**
* @brief Get the ambient pressure around the sensor.
*
* From Sensirion SCD4X I2C Library.
*
* Gets the ambient pressure in Pa.
* @note This function should not change the clock
*/
bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure)
{
uint16_t error;
LOG_INFO("%s: Requesting sensor ambient pressure", sensorName);
error = scd4x.getAmbientPressure(ambientPressure);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error);
return false;
}
LOG_INFO("%s: Sensor ambient pressure: %u", sensorName, ambientPressure);
return true;
}
/**
* @brief Set the sensor altitude.
*
* From Sensirion SCD4X I2C Library.
*
* Altitude in meters above sea level can be set after device installation.
* Valid value between 0 and 3000m. This overrides pressure offset.
* @note This function should not change the clock
*/
bool SCD4XSensor::setAltitude(uint32_t altitude)
{
uint16_t error;
if (!stopMeasurement()) {
return false;
}
LOG_INFO("%s: setting altitude at %um", sensorName, altitude);
error = scd4x.setSensorAltitude(altitude);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error);
return false;
}
// NOTE: this gives an error if issued. Sensirion's library
// doesn't indicate it's needed.
// error = scd4x.persistSettings();
// if (error != SCD4X_NO_ERROR) {
// LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
// return false;
// }
LOG_INFO("%s: altitude set", sensorName);
return true;
}
/**
* @brief Set the ambient pressure around the sensor.
*
* From Sensirion SCD4X I2C Library.
*
* The set_ambient_pressure command can be sent during periodic measurements
* to enable continuous pressure compensation. Note that setting an ambient
* pressure overrides any pressure compensation based on a previously set
* sensor altitude. Use of this command is highly recommended for
* applications experiencing significant ambient pressure changes to ensure
* sensor accuracy. Valid input values are between 70000 - 120000 Pa. The
* default value is 101300 Pa.
* @note This function should not change the clock
*/
bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure)
{
uint16_t error;
LOG_INFO("%s: setting ambient pressure at %u Pa", sensorName, ambientPressure);
error = scd4x.setAmbientPressure(ambientPressure);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error);
return false;
}
// Sensirion doesn't indicate if this is necessary. We send it anyway
error = scd4x.persistSettings();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
return false;
}
LOG_INFO("%s: ambient pressure set set", sensorName);
return true;
}
/**
* @brief Perform factory reset to erase the settings stored in the EEPROM.
*
* From Sensirion SCD4X I2C Library.
*
* The perform_factory_reset command resets all configuration settings
* stored in the EEPROM and erases the FRC and ASC algorithm history.
* @note This function should not change the clock
*/
bool SCD4XSensor::factoryReset()
{
uint16_t error;
LOG_INFO("%s: Requesting factory reset", sensorName);
if (!stopMeasurement()) {
return false;
}
error = scd4x.performFactoryReset();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Unable to do factory reset. Error code: %u", sensorName, error);
return false;
}
LOG_INFO("%s: Factory reset successful", sensorName);
return true;
}
/**
* @brief Put the sensor into sleep mode from idle mode.
*
* From Sensirion SCD4X I2C Library.
*
* Put the sensor from idle to sleep to reduce power consumption. Can be
* used to power down when operating the sensor in power-cycled single shot
* mode.
* @note This command is only available in idle mode. Only for SCD41.
*/
bool SCD4XSensor::powerDown()
{
LOG_INFO("%s: Trying to send sensor to sleep", sensorName);
if (sensorVariant != SCD4X_SENSOR_VARIANT_SCD41) {
LOG_WARN("SCD4X: Can't send sensor to sleep. Incorrect variant. Ignoring");
return true;
}
#ifdef SCD4X_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SCD4X_I2C_CLOCK_SPEED */
if (!stopMeasurement()) {
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
if (scd4x.powerDown() != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Error trying to execute sleep()", sensorName);
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
state = SCD4X_OFF;
return true;
}
/**
* @brief Wake up sensor from sleep mode to idle mode (powerUp)
*
* From Sensirion SCD4X I2C Library.
*
* Wake up the sensor from sleep mode into idle mode. Note that the SCD4x
* does not acknowledge the wake_up command. The sensor's idle state after
* wake up can be verified by reading out the serial number.
* @note This command is only available for SCD41.
* @note This function can't change clock (used in init)
*/
bool SCD4XSensor::powerUp()
{
LOG_INFO("%s: Waking up", sensorName);
if (scd4x.wakeUp() != SCD4X_NO_ERROR) {
LOG_ERROR("%s: Error trying to execute wakeUp()", sensorName);
return false;
}
state = SCD4X_IDLE;
return true;
}
/**
* @brief Check if sensor is in measurement mode
*/
bool SCD4XSensor::isActive()
{
return state == SCD4X_MEASUREMENT;
}
/**
* @brief Start measurement mode
* @note Not used in admin comands, getMetrics or init, can change clock.
*/
uint32_t SCD4XSensor::wakeUp()
{
#ifdef SCD4X_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return 0;
#endif /* CAN_RECLOCK_I2C */
#endif /* SCD4X_I2C_CLOCK_SPEED */
if (startMeasurement()) {
co2MeasureStarted = getTime();
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return SCD4X_WARMUP_MS;
}
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return 0;
}
/**
* @brief Stop measurement mode
* @note Not used in admin comands, getMetrics or init, can change clock.
*/
void SCD4XSensor::sleep()
{
#ifdef SCD4X_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return;
#endif /* CAN_RECLOCK_I2C */
#endif /* SCD4X_I2C_CLOCK_SPEED */
stopMeasurement();
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
}
/**
* @brief Can sleep function
*
* Power consumption is very low on lowPower mode, modify this function if
* you still want to override this behaviour. Otherwise, sleep is disabled
* routinely in low power mode
*/
bool SCD4XSensor::canSleep()
{
return lowPower ? false : true;
}
int32_t SCD4XSensor::wakeUpTimeMs()
{
return SCD4X_WARMUP_MS;
}
int32_t SCD4XSensor::pendingForReadyMs()
{
uint32_t now;
now = getTime();
uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted) * 1000;
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceCO2MeasureStarted);
if (sinceCO2MeasureStarted < SCD4X_WARMUP_MS) {
LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
return SCD4X_WARMUP_MS - sinceCO2MeasureStarted;
}
return 0;
}
AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response)
{
AdminMessageHandleResult result;
#ifdef SCD4X_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return AdminMessageHandleResult::NOT_HANDLED;
#endif /* CAN_RECLOCK_I2C */
#endif /* SCD4X_I2C_CLOCK_SPEED */
// TODO: potentially add selftest command?
switch (request->which_payload_variant) {
case meshtastic_AdminMessage_sensor_config_tag:
// Check for ASC-FRC request first
if (!request->sensor_config.has_scd4x_config) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
if (request->sensor_config.scd4x_config.has_factory_reset) {
LOG_DEBUG("%s: Requested factory reset", sensorName);
if (!this->factoryReset()) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
} else {
if (request->sensor_config.scd4x_config.has_set_asc) {
getASC(ascActive);
bool currentASC = ascActive;
if (request->sensor_config.scd4x_config.set_asc == false) {
LOG_DEBUG("%s: Request for FRC", sensorName);
if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
if (this->setASC(request->sensor_config.scd4x_config.set_asc)) {
if (!this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc)) {
result = AdminMessageHandleResult::NOT_HANDLED;
// Set it back to ASC if failed
setASC(currentASC);
break;
};
} else {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
} else {
// FRC requested but no target CO2 provided
LOG_ERROR("%s: target CO2 not provided", sensorName);
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
} else {
LOG_DEBUG("%s: Request for ASC", sensorName);
if (this->setASC(request->sensor_config.scd4x_config.set_asc)) {
if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
LOG_DEBUG("%s: Request has target CO2", sensorName);
this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc);
// NOTE - in this situation, if we set ASC, but baseline set fails, we stay on ASC
} else {
LOG_DEBUG("%s: Request doesn't have target CO2", sensorName);
}
} else {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
}
}
// Check for temperature offset
// NOTE: this requires to have a sensor working on stable environment
// And to make it between readings
if (request->sensor_config.scd4x_config.has_set_temperature) {
if (!this->setTemperature(request->sensor_config.scd4x_config.set_temperature)) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
}
// Check for altitude or pressure offset
if (request->sensor_config.scd4x_config.has_set_altitude) {
if (!this->setAltitude(request->sensor_config.scd4x_config.set_altitude)) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
} else if (request->sensor_config.scd4x_config.has_set_ambient_pressure) {
if (!this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure)) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
}
// Check for low power mode
// NOTE: to switch from one mode to another do:
// setPowerMode -> startMeasurement
if (request->sensor_config.scd4x_config.has_set_power_mode) {
if (!this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode)) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
}
}
result = AdminMessageHandleResult::HANDLED;
break;
default:
result = AdminMessageHandleResult::NOT_HANDLED;
}
// Start measurement mode
this->startMeasurement();
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return result;
}
#endif
@@ -0,0 +1,63 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cScd4x.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "RTC.h"
#include "TelemetrySensor.h"
#include <SensirionI2cScd4x.h>
// Max speed 400kHz
#define SCD4X_I2C_CLOCK_SPEED 100000
#define SCD4X_WARMUP_MS 5000
class SCD4XSensor : public TelemetrySensor
{
private:
SensirionI2cScd4x scd4x;
TwoWire *_bus{};
uint8_t _address{};
bool performFRC(uint32_t targetCO2);
bool setASCBaseline(uint32_t targetCO2);
bool getASC(uint16_t &ascEnabled);
bool setASC(bool ascEnabled);
bool setTemperature(float tempReference);
bool getAltitude(uint16_t &altitude);
bool setAltitude(uint32_t altitude);
bool getAmbientPressure(uint32_t &ambientPressure);
bool setAmbientPressure(uint32_t ambientPressure);
bool factoryReset();
bool setPowerMode(bool _lowPower);
bool startMeasurement();
bool stopMeasurement();
uint16_t ascActive = 1;
// low power measurement mode (on sensirion side). Disables sleep mode
// Improvement and testing needed for timings
bool lowPower = true;
uint32_t co2MeasureStarted = 0;
public:
SCD4XSensor();
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
enum SCD4XState { SCD4X_OFF, SCD4X_IDLE, SCD4X_MEASUREMENT };
SCD4XState state = SCD4X_OFF;
SCD4xSensorVariant sensorVariant{};
virtual bool isActive() override;
virtual void sleep() override; // Stops measurement (measurement -> idle)
virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement)
bool powerDown(); // Powers down sensor (idle -> power-off)
bool powerUp(); // Powers the sensor (power-off -> idle)
virtual bool canSleep() override;
virtual int32_t wakeUpTimeMs() override;
virtual int32_t pendingForReadyMs() override;
AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response) override;
};
#endif
+24 -13
View File
@@ -572,7 +572,7 @@ bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
// Check if it is time to do a cleaning
uint32_t now;
int32_t passed;
int32_t passed = 0;
now = getValidTime(RTCQuality::RTCQualityDevice);
// If time is not RTCQualityNone, it will return non-zero
@@ -665,16 +665,16 @@ bool SEN5XSensor::readValues()
sen5xmeasurement.vocIndex = !isnan(int_vocIndex) ? int_vocIndex / 10.0f : FLT_MAX;
sen5xmeasurement.noxIndex = !isnan(int_noxIndex) ? int_noxIndex / 10.0f : FLT_MAX;
LOG_DEBUG("Got: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5,
sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0);
LOG_DEBUG("Got %s readings: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sensorName, sen5xmeasurement.pM1p0,
sen5xmeasurement.pM2p5, sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0);
if (model != SEN50) {
LOG_DEBUG("Got: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sen5xmeasurement.humidity, sen5xmeasurement.temperature,
sen5xmeasurement.vocIndex);
LOG_DEBUG("Got %s readings: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sensorName, sen5xmeasurement.humidity,
sen5xmeasurement.temperature, sen5xmeasurement.vocIndex);
}
if (model == SEN55) {
LOG_DEBUG("Got: noxIndex=%.2f", sen5xmeasurement.noxIndex);
LOG_DEBUG("Got %s readings: noxIndex=%.2f", sensorName, sen5xmeasurement.noxIndex);
}
return true;
@@ -727,9 +727,9 @@ bool SEN5XSensor::readPNValues(bool cumulative)
sen5xmeasurement.pN1p0 -= sen5xmeasurement.pN0p5;
}
LOG_DEBUG("Got: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sen5xmeasurement.pN0p5,
sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0, sen5xmeasurement.pN10p0,
sen5xmeasurement.tSize);
LOG_DEBUG("Got %s readings: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sensorName,
sen5xmeasurement.pN0p5, sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0,
sen5xmeasurement.pN10p0, sen5xmeasurement.tSize);
return true;
}
@@ -919,6 +919,11 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement)
void SEN5XSensor::setMode(bool setOneShot)
{
oneShotMode = setOneShot;
if (oneShotMode) {
LOG_INFO("%s setting mode to one shot mode", sensorName);
} else {
LOG_INFO("%s setting mode to continuous mode", sensorName);
}
}
AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
@@ -934,16 +939,22 @@ AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPa
break;
}
// TODO - Add admin command to set temperature offset
// Check for one-shot/continuous mode request
if (request->sensor_config.sen5x_config.has_set_one_shot_mode) {
this->setMode(request->sensor_config.sen5x_config.set_one_shot_mode);
}
// TODO - Add admin command to set temperature offset?
// Check for temperature offset
// if (request->sensor_config.sen5x_config.has_set_temperature) {
// this->setTemperature(request->sensor_config.sen5x_config.set_temperature);
// }
// TODO - Add admin command to trigger fan cleaning?
// Check for one-shot/continuous mode request
if (request->sensor_config.sen5x_config.has_set_one_shot_mode) {
this->setMode(request->sensor_config.sen5x_config.set_one_shot_mode);
}
// if (request->sensor_config.sen5x_config.has_fan_cleaning && request->sensor_config.sen5x_config.fan_cleaning) {
// this->startCleaning();
// }
result = AdminMessageHandleResult::HANDLED;
break;
@@ -0,0 +1,198 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cSfa3x.h>)
#include "../detect/reClockI2C.h"
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "SFA30Sensor.h"
SFA30Sensor::SFA30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SFA30, "SFA30"){};
bool SFA30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
{
LOG_INFO("Init sensor: %s", sensorName);
_bus = bus;
_address = dev->address.address;
#ifdef SFA30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SFA30_I2C_CLOCK_SPEED */
sfa30.begin(*_bus, _address);
delay(20);
if (this->isError(sfa30.deviceReset())) {
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
state = State::IDLE;
if (this->isError(sfa30.startContinuousMeasurement())) {
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
LOG_INFO("%s starting measurement", sensorName);
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
status = 1;
state = State::ACTIVE;
measureStarted = getTime();
LOG_INFO("%s Enabled", sensorName);
initI2CSensor();
return true;
};
bool SFA30Sensor::isError(uint16_t response)
{
if (response == SFA30_NO_ERROR)
return false;
// TODO - Check error to char conversion
LOG_ERROR("%s: %u", sensorName, response);
return true;
}
void SFA30Sensor::sleep()
{
#ifdef SFA30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return;
#endif /* CAN_RECLOCK_I2C */
#endif /* SFA30_I2C_CLOCK_SPEED */
// Note - not recommended for this sensor on a periodic basis
if (this->isError(sfa30.stopMeasurement())) {
LOG_ERROR("%s: can't stop measurement", sensorName);
};
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
LOG_INFO("%s: stop measurement", sensorName);
state = State::IDLE;
measureStarted = 0;
}
uint32_t SFA30Sensor::wakeUp()
{
#ifdef SFA30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SFA30_I2C_CLOCK_SPEED */
LOG_INFO("Waking up %s", sensorName);
if (this->isError(sfa30.startContinuousMeasurement())) {
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return 0;
}
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
state = State::ACTIVE;
measureStarted = getTime();
return SFA30_WARMUP_MS;
}
int32_t SFA30Sensor::wakeUpTimeMs()
{
return SFA30_WARMUP_MS;
}
bool SFA30Sensor::canSleep()
{
// Sleep is disabled in this sensor because readings are not tested with periodic sleep
// with such low power consumption, prefered to keep it active
return false;
}
bool SFA30Sensor::isActive()
{
return state == State::ACTIVE;
}
int32_t SFA30Sensor::pendingForReadyMs()
{
uint32_t now;
now = getTime();
uint32_t sinceHchoMeasureStarted = (now - measureStarted) * 1000;
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceHchoMeasureStarted);
if (sinceHchoMeasureStarted < SFA30_WARMUP_MS) {
LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
return SFA30_WARMUP_MS - sinceHchoMeasureStarted;
}
return 0;
}
bool SFA30Sensor::getMetrics(meshtastic_Telemetry *measurement)
{
float hcho = 0.0;
float humidity = 0.0;
float temperature = 0.0;
#ifdef SFA30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SFA30_I2C_CLOCK_SPEED */
if (this->isError(sfa30.readMeasuredValues(hcho, humidity, temperature))) {
LOG_WARN("%s: No values", sensorName);
return false;
}
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
measurement->variant.air_quality_metrics.has_form_temperature = true;
measurement->variant.air_quality_metrics.has_form_humidity = true;
measurement->variant.air_quality_metrics.has_form_formaldehyde = true;
measurement->variant.air_quality_metrics.form_temperature = temperature;
measurement->variant.air_quality_metrics.form_humidity = humidity;
measurement->variant.air_quality_metrics.form_formaldehyde = hcho;
LOG_DEBUG("Got %s readings: hcho=%.2f, hcho_temp=%.2f, hcho_hum=%.2f", sensorName, hcho, temperature, humidity);
return true;
}
#endif
@@ -0,0 +1,39 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cSfa3x.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "RTC.h"
#include "TelemetrySensor.h"
#include <SensirionI2cSfa3x.h>
#define SFA30_I2C_CLOCK_SPEED 100000
#define SFA30_WARMUP_MS 10000
#define SFA30_NO_ERROR 0
class SFA30Sensor : public TelemetrySensor
{
private:
enum class State { IDLE, ACTIVE };
State state = State::IDLE;
uint32_t measureStarted = 0;
SensirionI2cSfa3x sfa30;
TwoWire *_bus{};
uint8_t _address{};
bool isError(uint16_t response);
public:
SFA30Sensor();
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
virtual bool isActive() override;
virtual void sleep() override;
virtual uint32_t wakeUp() override;
virtual bool canSleep() override;
virtual int32_t wakeUpTimeMs() override;
virtual int32_t pendingForReadyMs() override;
};
#endif
+1 -5
View File
@@ -757,11 +757,7 @@ void NimbleBluetooth::deinit()
isDeInit = true;
#ifdef BLE_LED
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
digitalWrite(BLE_LED, LED_STATE_OFF);
#endif
#ifndef NIMBLE_TWO
NimBLEDevice::deinit();
+1 -1
View File
@@ -158,7 +158,7 @@
#endif
#ifdef PIN_LED1
#define LED_PIN PIN_LED1 // LED1 on nrf52840-DK
#define LED_POWER PIN_LED1 // LED1 on nrf52840-DK
#endif
#ifdef PIN_BUTTON1

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