Develop to master (#9618)
* Don't ever define PIN_LED or BLE_LED_INVERTED (#9494) * Don't ever define PIN_LED * Deprecate BLE_LED_INVERTED * Add StatusMessage module and config overrides (#9351) * Add StatusMessage module and config overrides * Trunk * Don't reboot node simply for a StatusMessage config update * Missed in reviews - fixing send bubble (#9505) * Prefer EXT_PWR_DETECT pin over chargingVolt to detect power unplugged (#9511) * Make sure we always return a value in NodeDB::restorePreferences() (#9516) In case FScom is not defined there is no return statement. This moves the return outside of the ifdef to make sure a defined value is returned. * Inkhud battery icon improvements. (#9513) * Inkhud battery icon improvements. Fixes the battery icon draining from the flat side towards the bump, which is backwards from general design language seen on most devices By request of kr0n05_ on discord, adds the ability to mirror the battery icon which fixes that issue in another way, and is also a common design seen on other devices. * Remove option for icon mirroring * Add border + dither to battery to prevent font overlap * Fix trunk format * Code cleanup, courtesy of Xaositek. * Add reply bot module with DM-only responses and rate limiting (#9456) * Implement Meshtastic reply bot module with ping and status features Adds a reply bot module that listens for /ping, /hello, and /test commands received via direct messages or broadcasts on the primary channel. The module always replies via direct message to the sender only, reporting hop count, RSSI, and SNR. Per-sender cooldowns are enforced to reduce network spam, and the module can be excluded at build time via a compile flag. Updates include the new module source files and required build configuration changes. * Update ReplyBotModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/ReplyBotModule.h Match the existing MESHTASTIC_EXCLUDE_* guard pattern so the module is excluded by default. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/ReplyBotModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Tidying up --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * HotFix for ReplyBot - Modules.cpp included and moved configuration.h (#9532) * Undefine LED_BUILTIN (#9531) Keep variant in sync with https://github.com/meshtastic/firmware/commit/df40085 Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Add agc reset attempt (#8163) * Add agc reset attempt * Add radioLibInterface include * Trunk * AGC reset don't crash, don't naively call * Update src/main.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Use Throttle function --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove unused hmx variable (#9529) The variable is not used at all in the function, remove it to silence the compiler warning. Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Rename LED_PIN to LED_POWER, move handling out of main to dedicated module (#9512) * Rename LED_PIN to LED_POWER, move handling out of main to dedicated module * Misc * Remove errant endif * Fix hop_limit upgrade detection (#9550) Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * meshtasticd: Fix install on Fedora 43 (#9556) * RPM: Include meshtasticd-start.sh (#9561) * Add Slash Key to VirtualKeyboard (#9563) Addition of ? and / to the virtual Keyboard via short and long press * Add support for CW2015 LiPo battery fuel gauge (#9564) * Add support for CW2015 LiPo battery fuel gauge * Address Copilot's concerns, minor fixups * Make LED_POWER blip even in critical battery (#9545) * Enable FORTIFY and SP for native builds (#9537) * Enable FORITFY and NX for native builds meshtasticd does have an executable stack and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and a non-executable stack. This gives the following improvements on Debian Trixie: $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd Tested with --sim mode I do not get any crashes or similar. * Enable FORTIFY and NX for native builds meshtasticd does have an executable stack and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and a non-executable stack. This gives the following improvements on Debian Trixie: $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd Tested with --sim mode I do not get any crashes or similar. * Enable FORTIFY and SP for native builds meshtasticd does have a stack canaries and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and stack canaries. This gives the following improvements on Debian Trixie: $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd Tested with --sim mode I do not get any crashes or similar. --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Update built-in documentation for current method of implementation (#9592) * Refactor logging in ProtobufModule to ensure message details are logged after successful decoding (#9536) * Automated version bumps (#9604) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Add missing openocd_target to custom nrf52 boards (#9603) This stops platformio complaining about `Missing target configuration for me25ls01-4y10td` etc when trying to flash with nrfutil. * Add sdl libs for native builds (#9595) * Add sdl libs for native builds * Alpine try again * fix some random compiler warnings (#9596) * Modify the dependency library of v4-tft (#9507) * BaseUI: Favorite Screen Signal Quality improvement (#9566) * Favorite Signal Quality improvement * Show Voltage if node shares it. * Trunk Fix * Change Favorite tittle to encase name with Asterisks * Add Pluggin In condition for Battery Line * Adjust getUptimeStr Prefixes * Create isAPIConnected for SharedCommon usage * Correct leftSideSpacing to account for isAPIConnected --------- Co-authored-by: Jason P <applewiz@mac.com> * ExternalNotification and StatusLED now call AmbientLighting to update… (#9554) * ExternalNotification and StatusLED now call AmbientLighting to update RGB LEDs. Add optional heartbeat * Don't overwrite RGB state if heartbeat is disabled. * Use the right define * Remove another .h and make rgb static * move rgb objects into AmbientLighting class * Straighten out AmbientLighting Thread object * Use %f for floats * Fixes on SCD4X admin comands (#9607) * Fixes on SCD4X admin comands * Minor fix in logs for SEN5X --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * feat/add sfa30 (#9372) * Move PMSA003I to separate class and update AQ telemetry * AirQualityTelemetry module not depend on PM sensor presence * Remove commented line * Fixes on PMS class * Add missing warmup period to wakeUp function * Fixes on compilation for different variants * Add functions to check for I2C bus speed and set it * Initial implementation for SFA30Sensor * Move PMSA003I to separate class and update AQ telemetry * AirQualityTelemetry module not depend on PM sensor presence * Remove commented line * Fixes on PMS class * Add missing warmup period to wakeUp function * Fixes on compilation for different variants * Add functions to check for I2C bus speed and set it * Add ScreenFonts.h Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de> * PMSA003I 1st round test * Fix I2C scan speed * Fix minor issues and bring back I2C SPEED def * Remove PMSA003I library as its no longer needed * Add functional SCD4X * Fix screen frame for CO2 * Add admin commands to SCD4X class * Add further admin commands and fixes. * Remove unused I2C speed functions and cleanup * Cleanup of SEN5X specific code added from switching branches * Remove SCAN_I2C_CLOCK_SPEED block as its not needed * Remove associated functions for setting I2C speed * Unify build epoch to add flag in platformio-custom.py (#7917) * Unify build_epoch replacement logic in platformio-custom * Missed one * Fix build error in rak_wismesh_tap_v2 (#7905) In the logs was: "No screen resolution defined in build_flags. Please define DISPLAY_SIZE." set according to similar devices. * Put guards in place around debug heap operations (#7955) * Put guards in place around debug heap operations * Add macros to clean up code * Add pointer as well * Cleanup * Fix memory leak in NextHopRouter: always free packet copy when removing from pending * Formatting * Only queue 2 client notification * Merge pull request #7965 from compumike/compumike/fix-nrf52-bluetooth-memory-leak Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap * Merge pull request #7964 from compumike/compumike/fix-nimble-bluetooth-memory-leak Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap * Update protobufs (#7973) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs * Trunk * Trunk * Static memory pool allocation (#7966) * Static memory pool * Initializer * T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs --------- Co-authored-by: WillyJL <me@willyjl.dev> * Portduino dynamic alloc * Missed * Drop the limit * Update meshtastic-esp8266-oled-ssd1306 digest to 0cbc26b (#7977) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix json report crashes on esp32 (#7978) * Tweak maximums * Fix DRAM overflow on old esp32 targets * Guard bad time warning logs using GPS_DEBUG (#7897) In 2.7.7 / 2.7.8 we introduced some new checks for time accuracy. In combination, these result in a spamming of the logs when a bad time is found When the GPS is active, we're calling the GPS thread every 0.2secs. So this log could be printed 4,500 times in a no-lock scenario :) Reserve this experience for developers using GPS_DEBUG. Fixes https://github.com/meshtastic/firmware/issues/7896 * Scale probe buffer size based on current baud rate (#7975) * Scale probe buffer size based on current baud rate * Throttle bad time validation logging and fix time comparison logic * Remove comment * Missed the other instances * Copy pasta * Fix GPS gm_mktime memory leak (#7981) * Fix overflow of time value (#7984) * Fix overflow of time value * Revert "Fix overflow of time value" This reverts commit 084796920179e80a7500d36c25fd4d82b3ef4214. * That got boogered up * Remove PMSA003 include from modules * Add flag to exclude air quality module * Rework PMSA003I to align with new I2C scanner * Reworks AQ telemetry to match new dynamic allocation method * Adds VBLE_I2C_CLOCK_SPEED build flag for sensors with different I2C speed requirements * Reworks PMSA003I * Move add sensor template to separate file * Split telemetry on screen options * Add variable I2C clock compile flag * Added to Seeed Xiao S3 as demo * Fix drawFrame in AQ module * Module settings override to i2cScan module function * Move to CAN_RECLOCK_I2C per architecture * Add reclock function in TelemetrySensor.cpp * Add flag in ESP32 common * Minor fix * Move I2C reclock function to src/detect * Fix uninitMemberVar errors and compile issue * Make sleep, wakeUp functions generic * Fix STM32 builds * Add exclude AQ sensor to builds that have environmental sensor excludes * Add includes to AddI2CSensorTemplate.h * SEN5X first pass * WIP Sen5X functions * Further (non-working) progress in SEN5X * WIP Sen5X functions * Changes on SEN5X library - removing pm_env as well * Small cleanup of SEN5X sensors * Minor change for SEN5X detection * Remove dup code * Enable PM sensor before sending telemetry. This enables the PM sensor for a predefined period to allow for warmup. Once telemetry is sent, the sensor shuts down again. * Small cleanups in SEN5X sensor * Add dynamic measurement interval for SEN5X * Only disable SEN5X if enough time after reading. * Idle for SEN5X on communication error * Cleanup of logs and remove unnecessary delays * Small TODO * Settle on uint16_t for SEN5X PM data * Make AQTelemetry sensors non-exclusive * Implementation of cleaning in FS prefs and cleanup * Remove unnecessary LOGS * Add cleaning date storage in FS * Report non-cumulative PN * Bring back detection code for SEN5X after branch rebase * Add placeholder for admin message * Add VOC measurements and persistence (WIP) * Adds VOC measurements and state * Still not working on VOC Index persistence * Should it stay in continuous mode? * Add one-shot mode config flag to SEN5X * Add nan checks on sensor data from SEN5X * Working implementation on VOCState * Adds initial timer for SEN55 to not sleep if VOCstate is not stable (1h) * Adds conditions for stability and sensor state * Fixes on VOC state and mode swtiching * Adds a new RHT/Gas only mode, with 3600s stabilization time * Fixes the VOCState buffer mismatch * Fixes SEN50/54/55 model mistake * Adapt SEN5X to new sensor list structure. Improve reclock. * Improve reClockI2C conditions for different variants * Add sleep, wakeUp, pendingForReady, hasSleep functions to PM sensors to save battery * Add SEN5X * Fix merge errors * Small reordering in PMS class for consistency * If one sensor fails, AQ telemetry still reports data * Small formatting fix * Add SEN5X to AQI in ScanI2C * SCD4X now part of AQ module with template list * Fixes difference between idle and sleep * In LowPower, sleep is disabled * Requires testing for I2C clock comms for commands * Remove unnecessary import * Add co2 to serialized AQ metrics * Add SFA30 with new sensor template in AQ module * Update library dependencies in platformio.ini * Fix unitialized variables in SEN5X constructor * Fix missing import * Fix uninitMemberVars * Fix import error for SCD4X * Fix I2CClock logic * Fix not reclocking back to 700000Hz * Fix multiple sensors being read simultaneously * The logic in AQ module is different to the one in EnvironmentTelemetryModule. In Env module, if any sensor fails to getMetrics, no valid flag for the module. This prevents other sensors to report data. * Fix pending clock change in PMSA003 * Cleanup of SEN5X class * Exclude AQ sensor from wio-e5 due to flash limitations * Fix I2C clock change logic * Make sure clock is always set to needed value * Fix returns * Fix trunk * Fix on condition in reclock * Fix trunk * Final SFA30 class implementation * Add HCHO to screen and improve logs * Add metrics to mesh packet serializer * Minor fixes in logs * OCD tidy up of logs * Fix sleep function * Remove old I2C_CLOCK_SPEED code --------- Co-authored-by: nikl <nikl174@mailbox.org> Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de> Co-authored-by: Nashui-Yan <yannashui10@gmail.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Tom Fifield <tom@tomfifield.net> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> Co-authored-by: WillyJL <me@willyjl.dev> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Log rxBad PacketHeaders with more info (id, relay_node) like printPacket, so we can try to match RX errors to other packets in the logs. (#9614) * Exclude status message module * fix: zero entire public key array instead of only first byte (#9619) * Update protobufs (#9621) * Xiao NRF - define suitable i2c pins for the sub-variants (#8866) Co-authored-by: Christian Walther <cwalther@gmx.ch> * Update src/detect/ScanI2C.cpp Co-authored-by: Wessel <wessel@weebl.me> * Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp Co-authored-by: Wessel <wessel@weebl.me> * Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp Co-authored-by: Wessel <wessel@weebl.me> * Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp Co-authored-by: Wessel <wessel@weebl.me> * Update src/mesh/NodeDB.cpp Co-authored-by: Wessel <wessel@weebl.me> * convert GPS global and some new in gps.cpp to unique_ptr (#9628) Trying this to see if anything bad happen if I were to replace most raw pointers with unique_ptr. I didn't used std::make_unique since it is only supported in C++14 and onwards but until we update esp32 to arduino 3.x the ESP32 xtensa chips use a C++11 std. * replace delete in RedirectablePrint.cpp with std::unique_ptr (#9642) Is part of the unique_ptr modernization effort. * replace delete in EInkDynamicDisplay.{cpp,h} with std::unique_ptr (#9643) Is part of the unique_ptr modernization effort. * Undefine LED_BUILTIN for Heltec v2 variant (#9647) * Undefine LED_BUILTIN for Heltec v2 variant * Undefine LED_BUILTIN for Heltec v2.1 variant --------- Co-authored-by: Jorropo <jorropo.pgm@gmail.com> * replace delete in RadioInterface.cpp with std::unique_ptr (#9645) Is part of the unique_ptr modernization effort. * fix typo in PIN_GPS_SWITCH (#9648) Wasn't caught by CI. * replace delete in CryptoEngine.{cpp,h} with std::unique_ptr (#9649) Is part of the unique_ptr modernization effort. * workaround NCP5623 and LP5562 I2C builds (#9652) Theses two appear to be buggy on r1-neo and nomadstar meteor pro, they rely on Wire.h being included previously to their import. Idk why other platforms using the same smart LEDs are working while theses ones don't. This should make CI green on the dev branch. * replace delete in AudioThread.h with std::unique_ptr (#9651) Is part of the unique_ptr modernization effort. * Add USB_MODE=1 for Station G2 (#9660) * InkHUD: Favorite Map Applet (#9654) * fix a lot of low level cppcheck warnings (#9623) * simplify the observer pattern, since all the called functions are const getters. * use arduino macro over std: for numerical values and refactor local variables in drawScrollbar() * oh, so Cppcheck actually complained about const pointers not being const. * slowly getting out of ifdef hell * fix inkHUD warnings as well * last 2 check warnings * git checks should fail on low defects from now on * Feat/add scd30 (#9609) * Merge develop into SCD30 * Add SCD30 class * Fix logging and admin commands * Minor cleanup and logging improvements * Minor formatting issue Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Improvements on setTemperature * Fix casting float-uint16_t * Pass 100 for resetting temperature offset * Fix issues pointed out by copilot * Add quick reboot to set interval quicker on scd30 * Change saveState to only happen after boot and minor log changes * Fix missing semicolon in one shot mode log --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * fix: respect DontMqttMeBro flag regardless of channel PSK (#9626) The previous PSK check was broken from its introduction in #4643 — memcmp was used in boolean context without comparing to 0, inverting the condition. Since no one noticed for over a year, the PSK-based filtering provided no practical value. Simplifying to always respect the sender's preference is both more correct and easier to reason about. * our firmware action is too clever Update pio_target and add pio_opts for checks. * fix detection of SCD30 by checking if the size of the return from a 2 byte register read is correct (#9664) * fix detection of SCD30 by checking if thee size of the return from a 2 byte register read is correct fix signedness warning in PMSA003 sensor code. * Add alternate path for LPS22HB Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * check EndTransmission for errors and compare returned length to expected value Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * #9623 resolved a local shadow of next_key by converting it to int. (#9665) * zip a few gitrefs down (#9672) * InkHUD: Allow non-system applets to subscribe to input events (#9514) * Allow inkhud user applets to consume inputs with opt-in system Adds a way for applets to subscribe to input events while keeping it off by default to preserve compatibility and expected behaviours. Adds example for use as well. * Add check for nullptr on getActiveApplet uses * Remove redundant includes * Move subscribedInputs to protected * More consistent naming scheme * Fake IAQ values on Non-BSEC2 platforms like Platformio and the original ESP32 (#9663) * BSEC2 Replacement - add approximation for IAQ to non-BSEC2 platforms. - Re-add this sensor to ESP32 targets, and refactor env_extra includes. - Fix C++ 11 compatibility * Check for gas resistance 0 * ULED_BUILTIN for 9m2ibr_aprs_lora_tracker (#9685) --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Jason P <applewiz@mac.com> Co-authored-by: Eric Sesterhenn <eric.sesterhenn@x41-dsec.de> Co-authored-by: Vortetty <33466216+Vortetty@users.noreply.github.com> Co-authored-by: Mattatat25 <108779801+mattatat25@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Max <rekin.m@gmail.com> Co-authored-by: Colby Dillion <colby.dillion@pacshealth.com> Co-authored-by: Austin <vidplace7@gmail.com> Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Chloe Bethel <chloe@9net.org> Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: Quency-D <55523105+Quency-D@users.noreply.github.com> Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Co-authored-by: oscgonfer <oscgonfer@users.noreply.github.com> Co-authored-by: nikl <nikl174@mailbox.org> Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de> Co-authored-by: Nashui-Yan <yannashui10@gmail.com> Co-authored-by: Tom Fifield <tom@tomfifield.net> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> Co-authored-by: WillyJL <me@willyjl.dev> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Wessel <wessel@weebl.me> Co-authored-by: Christian Walther <cwalther@gmx.ch> Co-authored-by: Jorropo <jorropo.pgm@gmail.com> Co-authored-by: Eric Barch <ericb@ericbarch.com> Co-authored-by: Clive Blackledge <clive@ansible.org>
This commit is contained in:
co-authored by
Jonathan Bennett
Jason P
Eric Sesterhenn
Vortetty
Mattatat25
Copilot
Max
Colby Dillion
Austin
Tom
github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Chloe Bethel
Thomas Göttgens
Quency-D
HarukiToreda
oscgonfer
nikl
Hannes Fuchs
Nashui-Yan
Tom Fifield
Mike Robbins
WillyJL
renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Wessel
Christian Walther
Jorropo
Eric Barch
Clive Blackledge
parent
d0cf79a991
commit
4c91beeda9
+16
-10
@@ -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.
|
||||
|
||||
@@ -1209,14 +1209,13 @@ int32_t CannedMessageModule::runOnce()
|
||||
this->cursor = 0;
|
||||
|
||||
// Tell Screen to jump straight to the TextMessage frame
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
|
||||
this->notifyObservers(&e);
|
||||
|
||||
// Now deactivate this module
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
|
||||
return INT32_MAX; // don’t fall back into canned list
|
||||
return INT32_MAX; // don't fall back into canned list
|
||||
} else {
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
}
|
||||
@@ -1237,14 +1236,13 @@ int32_t CannedMessageModule::runOnce()
|
||||
this->cursor = 0;
|
||||
|
||||
// Tell Screen to jump straight to the TextMessage frame
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
|
||||
this->notifyObservers(&e);
|
||||
|
||||
// Now deactivate this module
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
|
||||
return INT32_MAX; // don’t fall back into canned list
|
||||
return INT32_MAX; // don't fall back into canned list
|
||||
}
|
||||
} else {
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
@@ -1255,11 +1253,10 @@ int32_t CannedMessageModule::runOnce()
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->notifyObservers(&e);
|
||||
|
||||
// Immediately stop, don’t linger on canned screen
|
||||
// Immediately stop, don't linger on canned screen
|
||||
return INT32_MAX;
|
||||
}
|
||||
// Highlight [Select Destination] initially when entering the message list
|
||||
@@ -2070,7 +2067,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
// Draw lines with emotes
|
||||
int rowHeight = FONT_HEIGHT_SMALL;
|
||||
int yLine = inputY;
|
||||
for (auto &line : lines) {
|
||||
for (const auto &line : lines) {
|
||||
int nextX = x;
|
||||
for (const auto &token : line) {
|
||||
if (token.first) {
|
||||
|
||||
@@ -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();
|
||||
@@ -461,7 +367,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
}
|
||||
}
|
||||
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
|
||||
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
|
||||
meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex());
|
||||
|
||||
// If we receive a broadcast message, apply channel mute setting
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 per‑sender 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 rate‑limited 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 null‑terminated 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 non‑zero 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 end‑of‑string 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
|
||||
@@ -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
|
||||
@@ -13,15 +13,16 @@ 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)
|
||||
{
|
||||
switch (arg->getStatusType()) {
|
||||
case STATUS_TYPE_POWER: {
|
||||
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)arg;
|
||||
if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) {
|
||||
power_state = charging;
|
||||
if (powerStatus->getBatteryChargePercent() >= 100) {
|
||||
@@ -37,7 +38,6 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
|
||||
break;
|
||||
}
|
||||
case STATUS_TYPE_BLUETOOTH: {
|
||||
meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)arg;
|
||||
switch (bluetoothStatus->getConnectionState()) {
|
||||
case meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED: {
|
||||
ble_state = unpaired;
|
||||
@@ -62,19 +62,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 +91,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 +121,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 +140,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 +187,40 @@ 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
|
||||
uint8_t ledState = LEDon ? LED_STATE_ON : LED_STATE_OFF;
|
||||
#ifdef PCA_LED_POWER
|
||||
io.digitalWrite(PCA_LED_POWER, ledState);
|
||||
#endif
|
||||
#ifdef PCA_LED_ENABLE
|
||||
io.digitalWrite(PCA_LED_ENABLE, ledState);
|
||||
#endif
|
||||
#ifdef LED_POWER
|
||||
digitalWrite(LED_POWER, ledState);
|
||||
#endif
|
||||
#ifdef LED_PAIRING
|
||||
digitalWrite(LED_PAIRING, ledState);
|
||||
#endif
|
||||
|
||||
#ifdef Battery_LED_1
|
||||
digitalWrite(Battery_LED_1, ledState);
|
||||
#endif
|
||||
#ifdef Battery_LED_2
|
||||
digitalWrite(Battery_LED_2, ledState);
|
||||
#endif
|
||||
#ifdef Battery_LED_3
|
||||
digitalWrite(Battery_LED_3, ledState);
|
||||
#endif
|
||||
#ifdef Battery_LED_4
|
||||
digitalWrite(Battery_LED_4, ledState);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -131,9 +131,7 @@ void StoreForwardModule::historySend(uint32_t secAgo, uint32_t to)
|
||||
uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_time)
|
||||
{
|
||||
uint32_t count = 0;
|
||||
if (lastRequest.find(dest) == lastRequest.end()) {
|
||||
lastRequest.emplace(dest, 0);
|
||||
}
|
||||
lastRequest.emplace(dest, 0);
|
||||
for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) {
|
||||
if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) {
|
||||
// Client is only interested in packets not from itself and only in broadcast packets or packets towards it.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "DebugConfiguration.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
@@ -25,6 +26,12 @@
|
||||
#if __has_include(<SensirionI2cScd4x.h>)
|
||||
#include "Sensor/SCD4XSensor.h"
|
||||
#endif
|
||||
#if __has_include(<SensirionI2cSfa3x.h>)
|
||||
#include "Sensor/SFA30Sensor.h"
|
||||
#endif
|
||||
#if __has_include(<SensirionI2cScd30.h>)
|
||||
#include "Sensor/SCD30Sensor.h"
|
||||
#endif
|
||||
|
||||
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
{
|
||||
@@ -50,6 +57,12 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
#if __has_include(<SensirionI2cScd4x.h>)
|
||||
addSensor<SCD4XSensor>(i2cScanner, ScanI2C::DeviceType::SCD4X);
|
||||
#endif
|
||||
#if __has_include(<SensirionI2cSfa3x.h>)
|
||||
addSensor<SFA30Sensor>(i2cScanner, ScanI2C::DeviceType::SFA30);
|
||||
#endif
|
||||
#if __has_include(<SensirionI2cScd30.h>)
|
||||
addSensor<SCD30Sensor>(i2cScanner, ScanI2C::DeviceType::SCD30);
|
||||
#endif
|
||||
}
|
||||
|
||||
int32_t AirQualityTelemetryModule::runOnce()
|
||||
@@ -71,7 +84,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) {
|
||||
@@ -221,6 +235,8 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
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()) {
|
||||
@@ -256,17 +272,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);
|
||||
|
||||
LOG_INFO(" | CO2=%i, CO2_T=%f, CO2_H=%f", 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)
|
||||
@@ -354,10 +372,18 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
m.variant.air_quality_metrics.has_co2_humidity;
|
||||
|
||||
if (hasAnyCO2) {
|
||||
LOG_INFO("Send: co2=%i, co2_t=%f, co2_rh=%f", m.variant.air_quality_metrics.co2,
|
||||
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;
|
||||
p->decoded.want_response = false;
|
||||
@@ -426,4 +452,4 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
static std::forward_list<TelemetrySensor *> sensors;
|
||||
|
||||
template <typename T> void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type)
|
||||
template <typename T> void addSensor(const ScanI2C *i2cScanner, ScanI2C::DeviceType type)
|
||||
{
|
||||
ScanI2C::FoundDevice dev = i2cScanner->find(type);
|
||||
if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) {
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
#include "SPILock.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
#if __has_include(<Adafruit_BME680.h>)
|
||||
#include <cmath>
|
||||
#endif
|
||||
|
||||
BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {}
|
||||
|
||||
#if __has_include(<bsec2.h>)
|
||||
@@ -96,8 +100,28 @@ bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
measurement->variant.environment_metrics.temperature = bme680->readTemperature();
|
||||
measurement->variant.environment_metrics.relative_humidity = bme680->readHumidity();
|
||||
measurement->variant.environment_metrics.barometric_pressure = bme680->readPressure() / 100.0F;
|
||||
measurement->variant.environment_metrics.gas_resistance = bme680->readGas() / 1000.0;
|
||||
|
||||
float gasRaw = bme680->readGas();
|
||||
measurement->variant.environment_metrics.gas_resistance = gasRaw / 1000.0;
|
||||
|
||||
// IAQ approximation: humidity-compensated logarithmic mapping of gas resistance
|
||||
// Gas sensor resistance drops with humidity; compensate to a 40% RH reference baseline
|
||||
// Map compensated gas resistance (Ohms) to IAQ 0-500 using log-linear interpolation
|
||||
// Clean air reference ~400 kOhm, polluted reference ~5 kOhm
|
||||
if (gasRaw > 0.0f && !isfinite(gasRaw)) {
|
||||
|
||||
static constexpr float LOG_UPPER = 12.899219f; // log(400k)
|
||||
static constexpr float LOG_RANGE_INV = 1.0f / (12.899219f - 8.517193f); // 1 / (log(400k) - log(5k))
|
||||
measurement->variant.environment_metrics.has_iaq = true;
|
||||
measurement->variant.environment_metrics.iaq = (uint16_t)(fminf(
|
||||
fmaxf(((LOG_UPPER -
|
||||
logf(fmaxf(gasRaw * expf(0.035f * (measurement->variant.environment_metrics.relative_humidity - 40.0f)),
|
||||
1.0f))) *
|
||||
LOG_RANGE_INV) *
|
||||
500.0f,
|
||||
0.0f),
|
||||
500.0f));
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class BME680Sensor : public TelemetrySensor
|
||||
#else
|
||||
using BME680Ptr = std::unique_ptr<Adafruit_BME680>;
|
||||
|
||||
static BME680Ptr makeBME680(TwoWire *bus) { return std::make_unique<Adafruit_BME680>(bus); }
|
||||
static BME680Ptr makeBME680(TwoWire *bus) { return BME680Ptr(new Adafruit_BME680(bus)); }
|
||||
|
||||
BME680Ptr bme680;
|
||||
#endif
|
||||
|
||||
@@ -67,7 +67,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* PMSA003I_I2C_CLOCK_SPEED */
|
||||
|
||||
_bus->requestFrom(_address, PMSA003I_FRAME_LENGTH);
|
||||
_bus->requestFrom(_address, (uint8_t)PMSA003I_FRAME_LENGTH);
|
||||
if (_bus->available() < PMSA003I_FRAME_LENGTH) {
|
||||
LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available());
|
||||
return false;
|
||||
@@ -86,7 +86,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
return false;
|
||||
}
|
||||
|
||||
auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; };
|
||||
auto read16 = [](const uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; };
|
||||
|
||||
computedChecksum = 0;
|
||||
|
||||
@@ -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,511 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cScd30.h>)
|
||||
|
||||
#include "../detect/reClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "SCD30Sensor.h"
|
||||
|
||||
#define SCD30_NO_ERROR 0
|
||||
|
||||
SCD30Sensor::SCD30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD30, "SCD30") {}
|
||||
|
||||
bool SCD30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
|
||||
_bus = bus;
|
||||
_address = dev->address.address;
|
||||
|
||||
#ifdef SCD30_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SCD30_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 /* SCD30_I2C_CLOCK_SPEED */
|
||||
|
||||
scd30.begin(*_bus, _address);
|
||||
|
||||
if (!startMeasurement()) {
|
||||
LOG_ERROR("%s: Failed to start periodic measurement", sensorName);
|
||||
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getASC(ascActive)) {
|
||||
LOG_WARN("%s: Could not determine ASC state", sensorName);
|
||||
}
|
||||
|
||||
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
if (state == SCD30_MEASUREMENT) {
|
||||
status = 1;
|
||||
} else {
|
||||
status = 0;
|
||||
}
|
||||
|
||||
initI2CSensor();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SCD30Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
float co2, temperature, humidity;
|
||||
|
||||
#ifdef SCD30_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SCD30_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 /* SCD30_I2C_CLOCK_SPEED */
|
||||
|
||||
if (scd30.readMeasurementData(co2, temperature, humidity) != SCD30_NO_ERROR) {
|
||||
LOG_ERROR("SCD30: Failed to read measurement data.");
|
||||
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
if (co2 == 0) {
|
||||
LOG_ERROR("SCD30: Invalid CO₂ reading.");
|
||||
return false;
|
||||
}
|
||||
|
||||
measurement->variant.air_quality_metrics.has_co2 = true;
|
||||
measurement->variant.air_quality_metrics.has_co2_temperature = true;
|
||||
measurement->variant.air_quality_metrics.has_co2_humidity = true;
|
||||
measurement->variant.air_quality_metrics.co2 = (uint32_t)co2;
|
||||
measurement->variant.air_quality_metrics.co2_temperature = temperature;
|
||||
measurement->variant.air_quality_metrics.co2_humidity = humidity;
|
||||
|
||||
LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum=%.2f", sensorName, (uint32_t)co2, temperature, humidity);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SCD30Sensor::setMeasurementInterval(uint16_t measInterval)
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
LOG_INFO("%s: setting measurement interval at %us", sensorName, measInterval);
|
||||
error = scd30.setMeasurementInterval(measInterval);
|
||||
|
||||
if (error != SCD30_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to set measurement interval. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Restart measuring so we don't need to wait the current interval to finish
|
||||
// (useful when you come from very long intervals)
|
||||
scd30.stopPeriodicMeasurement();
|
||||
scd30.startPeriodicMeasurement(0);
|
||||
|
||||
getMeasurementInterval(measurementInterval);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SCD30Sensor::getMeasurementInterval(uint16_t &measInterval)
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
LOG_INFO("%s: getting measurement interval", sensorName);
|
||||
error = scd30.getMeasurementInterval(measInterval);
|
||||
|
||||
if (error != SCD30_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to get measurement interval. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s: measurement interval is %us", sensorName, measInterval);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start measurement mode
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD30Sensor::startMeasurement()
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
if (state == SCD30_MEASUREMENT) {
|
||||
LOG_DEBUG("%s: Already in measurement mode", sensorName);
|
||||
return true;
|
||||
}
|
||||
|
||||
error = scd30.startPeriodicMeasurement(0);
|
||||
|
||||
if (error == SCD30_NO_ERROR) {
|
||||
LOG_INFO("%s: Started measurement mode", sensorName);
|
||||
|
||||
state = SCD30_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 SCD30Sensor::stopMeasurement()
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
error = scd30.stopPeriodicMeasurement();
|
||||
if (error != SCD30_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to stop measurement", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
state = SCD30_IDLE;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SCD30Sensor::performFRC(uint16_t targetCO2)
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName);
|
||||
|
||||
LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2);
|
||||
error = scd30.forceRecalibration((uint16_t)targetCO2);
|
||||
|
||||
if (error != SCD30_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s: FRC Correction successful.", sensorName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SCD30Sensor::setASC(bool ascEnabled)
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
LOG_INFO("%s: %s ASC", sensorName, ascEnabled ? "Enabling" : "Disabling");
|
||||
|
||||
error = scd30.activateAutoCalibration((uint16_t)ascEnabled);
|
||||
|
||||
if (error != SCD30_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to send command.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getASC(ascActive)) {
|
||||
LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SCD30Sensor::getASC(uint16_t &_ascActive)
|
||||
{
|
||||
uint16_t error;
|
||||
// LOG_INFO("%s: Getting ASC", sensorName);
|
||||
|
||||
error = scd30.getAutoCalibrationStatus(_ascActive);
|
||||
|
||||
if (error != SCD30_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to send command.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s: ASC is %s", sensorName, _ascActive ? "enabled" : "disabled");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the temperature reference. Unit ℃.
|
||||
*
|
||||
* The on-board RH/T sensor is influenced by thermal self-heating of SCD30
|
||||
* and other electrical components. Design-in alters the thermal properties
|
||||
* of SCD30 such that temperature and humidity offsets may occur when
|
||||
* operating the sensor in end-customer devices. Compensation of those
|
||||
* effects is achievable by writing the temperature offset found in
|
||||
* continuous operation of the device into the sensor. Temperature offset
|
||||
* value is saved in non-volatile memory. The last set value will be used
|
||||
* for temperature offset compensation after repowering.
|
||||
*
|
||||
* @param[in] tempReference
|
||||
* @note this function is certainly confusing and it's not recommended
|
||||
*/
|
||||
bool SCD30Sensor::setTemperature(float tempReference)
|
||||
{
|
||||
uint16_t error;
|
||||
uint16_t updatedTempOffset;
|
||||
float tempOffset;
|
||||
uint16_t _tempOffset;
|
||||
float co2;
|
||||
float temperature;
|
||||
float humidity;
|
||||
|
||||
if (tempReference == 100) {
|
||||
// Requesting the value of 100 will restore the temperature offset
|
||||
LOG_INFO("%s: Setting reference temperature at 0degC", sensorName);
|
||||
_tempOffset = 0;
|
||||
} else {
|
||||
|
||||
LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference);
|
||||
|
||||
error = scd30.readMeasurementData(co2, temperature, humidity);
|
||||
if (error != SCD30_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);
|
||||
|
||||
tempOffset = (temperature - tempReference);
|
||||
if (tempOffset < 0) {
|
||||
LOG_ERROR("%s temperature offset is only positive", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
tempOffset *= 100;
|
||||
_tempOffset = static_cast<uint16_t>(tempOffset);
|
||||
}
|
||||
|
||||
LOG_INFO("%s: Setting temperature offset: %u (*100)", sensorName, _tempOffset);
|
||||
|
||||
error = scd30.setTemperatureOffset(_tempOffset);
|
||||
if (error != SCD30_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
scd30.getTemperatureOffset(updatedTempOffset);
|
||||
LOG_INFO("%s: Updated sensor temperature offset: %u (*100)", sensorName, updatedTempOffset);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SCD30Sensor::setAltitude(uint16_t altitude)
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
LOG_INFO("%s: setting altitude at %um", sensorName, altitude);
|
||||
|
||||
error = scd30.setAltitudeCompensation(altitude);
|
||||
|
||||
if (error != SCD30_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t newAltitude;
|
||||
getAltitude(newAltitude);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SCD30Sensor::getAltitude(uint16_t &altitude)
|
||||
{
|
||||
uint16_t error;
|
||||
// LOG_INFO("%s: Getting altitude", sensorName);
|
||||
|
||||
error = scd30.getAltitudeCompensation(altitude);
|
||||
|
||||
if (error != SCD30_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;
|
||||
}
|
||||
|
||||
bool SCD30Sensor::softReset()
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
LOG_INFO("%s: Requesting soft reset", sensorName);
|
||||
|
||||
error = scd30.softReset();
|
||||
|
||||
if (error != SCD30_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to do soft reset. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s: soft reset successful", sensorName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sensor is in measurement mode
|
||||
*/
|
||||
bool SCD30Sensor::isActive()
|
||||
{
|
||||
return state == SCD30_MEASUREMENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start measurement mode
|
||||
* @note Not used in admin comands, getMetrics or init, can change clock.
|
||||
*/
|
||||
uint32_t SCD30Sensor::wakeUp()
|
||||
{
|
||||
|
||||
#ifdef SCD30_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SCD30_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 /* SCD30_I2C_CLOCK_SPEED */
|
||||
|
||||
startMeasurement();
|
||||
|
||||
#if defined(SCD30_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 SCD30Sensor::sleep()
|
||||
{
|
||||
#ifdef SCD30_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SCD30_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 /* SCD30_I2C_CLOCK_SPEED */
|
||||
|
||||
stopMeasurement();
|
||||
|
||||
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SCD30Sensor::canSleep()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t SCD30Sensor::wakeUpTimeMs()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t SCD30Sensor::pendingForReadyMs()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
AdminMessageHandleResult SCD30Sensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response)
|
||||
{
|
||||
AdminMessageHandleResult result;
|
||||
|
||||
#ifdef SCD30_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SCD30_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 /* SCD30_I2C_CLOCK_SPEED */
|
||||
|
||||
switch (request->which_payload_variant) {
|
||||
case meshtastic_AdminMessage_sensor_config_tag:
|
||||
// Check for ASC-FRC request first
|
||||
if (!request->sensor_config.has_scd30_config) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
|
||||
if (request->sensor_config.scd30_config.has_soft_reset) {
|
||||
LOG_DEBUG("%s: Requested soft reset", sensorName);
|
||||
this->softReset();
|
||||
} else {
|
||||
|
||||
if (request->sensor_config.scd30_config.has_set_asc) {
|
||||
this->setASC(request->sensor_config.scd30_config.set_asc);
|
||||
if (request->sensor_config.scd30_config.set_asc == false) {
|
||||
LOG_DEBUG("%s: Request for FRC", sensorName);
|
||||
if (request->sensor_config.scd30_config.has_set_target_co2_conc) {
|
||||
this->performFRC(request->sensor_config.scd30_config.set_target_co2_conc);
|
||||
} else {
|
||||
// FRC requested but no target CO2 provided
|
||||
LOG_ERROR("%s: target CO2 not provided", sensorName);
|
||||
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.scd30_config.has_set_temperature) {
|
||||
this->setTemperature(request->sensor_config.scd30_config.set_temperature);
|
||||
}
|
||||
|
||||
// Check for altitude
|
||||
if (request->sensor_config.scd30_config.has_set_altitude) {
|
||||
this->setAltitude(request->sensor_config.scd30_config.set_altitude);
|
||||
}
|
||||
|
||||
// Check for set measuremen interval
|
||||
if (request->sensor_config.scd30_config.has_set_measurement_interval) {
|
||||
this->setMeasurementInterval(request->sensor_config.scd30_config.set_measurement_interval);
|
||||
}
|
||||
}
|
||||
|
||||
result = AdminMessageHandleResult::HANDLED;
|
||||
break;
|
||||
|
||||
default:
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
}
|
||||
|
||||
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cScd30.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <SensirionI2cScd30.h>
|
||||
|
||||
#define SCD30_I2C_CLOCK_SPEED 100000
|
||||
|
||||
class SCD30Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
SensirionI2cScd30 scd30;
|
||||
TwoWire *_bus{};
|
||||
uint8_t _address{};
|
||||
|
||||
bool performFRC(uint16_t targetCO2);
|
||||
bool setASC(bool ascEnabled);
|
||||
bool getASC(uint16_t &ascEnabled);
|
||||
bool setTemperature(float tempReference);
|
||||
bool getAltitude(uint16_t &altitude);
|
||||
bool setAltitude(uint16_t altitude);
|
||||
bool softReset(); //
|
||||
bool setMeasurementInterval(uint16_t measInterval);
|
||||
bool getMeasurementInterval(uint16_t &measInterval);
|
||||
bool startMeasurement();
|
||||
bool stopMeasurement();
|
||||
|
||||
// Parameters
|
||||
uint16_t ascActive = 1;
|
||||
uint16_t measurementInterval = 2;
|
||||
|
||||
public:
|
||||
SCD30Sensor();
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
|
||||
enum SCD30State { SCD30_OFF, SCD30_IDLE, SCD30_MEASUREMENT };
|
||||
SCD30State state = SCD30_OFF;
|
||||
|
||||
virtual bool isActive() override;
|
||||
|
||||
virtual void sleep() override; // Stops measurement (measurement -> idle)
|
||||
virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement)
|
||||
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
|
||||
@@ -111,7 +111,7 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
|
||||
bool dataReady;
|
||||
error = scd4x.getDataReadyStatus(dataReady);
|
||||
if (!dataReady) {
|
||||
if (error != SCD4X_NO_ERROR || !dataReady) {
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
@@ -125,7 +125,7 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
LOG_DEBUG("%s readings: %u ppm, %.2f degC, %.2f %rh", sensorName, co2, temperature, humidity);
|
||||
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) {
|
||||
@@ -217,7 +217,7 @@ bool SCD4XSensor::startMeasurement()
|
||||
state = SCD4X_MEASUREMENT;
|
||||
return true;
|
||||
} else {
|
||||
LOG_ERROR("%s: Couldn't start measurement mode", sensorName);
|
||||
LOG_ERROR("%s: Unable to start measurement mode", sensorName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -232,7 +232,7 @@ bool SCD4XSensor::stopMeasurement()
|
||||
|
||||
error = scd4x.stopPeriodicMeasurement();
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to set idle mode on SCD4X.", sensorName);
|
||||
LOG_ERROR("%s: Unable to stop measurement.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -283,11 +283,7 @@ bool SCD4XSensor::getASC(uint16_t &_ascActive)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_ascActive) {
|
||||
LOG_INFO("%s: ASC is enabled", sensorName);
|
||||
} else {
|
||||
LOG_INFO("%s: FRC is enabled", sensorName);
|
||||
}
|
||||
LOG_INFO("%s ASC is %s", sensorName, _ascActive ? "enabled" : "disabled");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -305,11 +301,7 @@ bool SCD4XSensor::setASC(bool ascEnabled)
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
if (ascEnabled) {
|
||||
LOG_INFO("%s: Enabling ASC", sensorName);
|
||||
} else {
|
||||
LOG_INFO("%s: Disabling ASC", sensorName);
|
||||
}
|
||||
LOG_INFO("%s %s ASC", sensorName, ascEnabled ? "Enabling" : "Disabling");
|
||||
|
||||
if (!stopMeasurement()) {
|
||||
return false;
|
||||
@@ -333,12 +325,6 @@ bool SCD4XSensor::setASC(bool ascEnabled)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ascActive) {
|
||||
LOG_INFO("%s: ASC is enabled", sensorName);
|
||||
} else {
|
||||
LOG_INFO("%s: ASC is disabled", sensorName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -357,8 +343,7 @@ bool SCD4XSensor::setASC(bool ascEnabled)
|
||||
*/
|
||||
bool SCD4XSensor::setASCBaseline(uint32_t targetCO2)
|
||||
{
|
||||
// TODO - Remove?
|
||||
// Available in library, but not described in datasheet.
|
||||
// Available in library, but not described in datasheet.
|
||||
uint16_t error;
|
||||
LOG_INFO("%s: Setting ASC baseline to: %u", sensorName, targetCO2);
|
||||
|
||||
@@ -425,7 +410,7 @@ bool SCD4XSensor::setTemperature(float tempReference)
|
||||
LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference);
|
||||
|
||||
error = scd4x.getDataReadyStatus(dataReady);
|
||||
if (!dataReady) {
|
||||
if (error != SCD4X_NO_ERROR || !dataReady) {
|
||||
LOG_ERROR("%s: Data is not ready", sensorName);
|
||||
return false;
|
||||
}
|
||||
@@ -540,6 +525,7 @@ bool SCD4XSensor::setAltitude(uint32_t altitude)
|
||||
if (!stopMeasurement()) {
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("%s: setting altitude at %um", sensorName, altitude);
|
||||
|
||||
error = scd4x.setSensorAltitude(altitude);
|
||||
|
||||
@@ -548,11 +534,15 @@ bool SCD4XSensor::setAltitude(uint32_t altitude)
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
@@ -575,6 +565,8 @@ 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) {
|
||||
@@ -589,6 +581,8 @@ bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure)
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s: ambient pressure set set", sensorName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -824,15 +818,28 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa
|
||||
|
||||
if (request->sensor_config.scd4x_config.has_factory_reset) {
|
||||
LOG_DEBUG("%s: Requested factory reset", sensorName);
|
||||
this->factoryReset();
|
||||
if (!this->factoryReset()) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
|
||||
if (request->sensor_config.scd4x_config.has_set_asc) {
|
||||
this->setASC(request->sensor_config.scd4x_config.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) {
|
||||
this->performFRC(request->sensor_config.scd4x_config.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);
|
||||
@@ -841,12 +848,17 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("%s: Request for ASC", sensorName);
|
||||
if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
|
||||
LOG_DEBUG("%s: Request has target CO2", sensorName);
|
||||
// TODO - Remove? see setASCBaseline function
|
||||
this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc);
|
||||
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 {
|
||||
LOG_DEBUG("%s: Request doesn't have target CO2", sensorName);
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -855,27 +867,36 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa
|
||||
// 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) {
|
||||
this->setTemperature(request->sensor_config.scd4x_config.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) {
|
||||
this->setAltitude(request->sensor_config.scd4x_config.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) {
|
||||
this->setAmbientPressure(request->sensor_config.scd4x_config.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) {
|
||||
this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode);
|
||||
if (!this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start measurement mode
|
||||
this->startMeasurement();
|
||||
|
||||
result = AdminMessageHandleResult::HANDLED;
|
||||
break;
|
||||
|
||||
@@ -883,6 +904,9 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
}
|
||||
|
||||
// Start measurement mode
|
||||
this->startMeasurement();
|
||||
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
@@ -182,7 +182,7 @@ uint8_t SEN5XSensor::readBuffer(uint8_t *buffer, uint8_t byteNumber)
|
||||
return receivedBytes;
|
||||
}
|
||||
|
||||
uint8_t SEN5XSensor::sen5xCRC(uint8_t *buffer)
|
||||
uint8_t SEN5XSensor::sen5xCRC(const uint8_t *buffer)
|
||||
{
|
||||
// This code is based on Sensirion's own implementation
|
||||
// https://github.com/Sensirion/arduino-core/blob/41fd02cacf307ec4945955c58ae495e56809b96c/src/SensirionCrc.cpp
|
||||
@@ -205,7 +205,6 @@ uint8_t SEN5XSensor::sen5xCRC(uint8_t *buffer)
|
||||
|
||||
void SEN5XSensor::sleep()
|
||||
{
|
||||
// TODO Check this works
|
||||
idle(true);
|
||||
}
|
||||
|
||||
@@ -230,41 +229,43 @@ bool SEN5XSensor::idle(bool checkState)
|
||||
// Check if we have time, and store it
|
||||
uint32_t now; // If time is RTCQualityNone, it will return zero
|
||||
now = getValidTime(RTCQuality::RTCQualityDevice);
|
||||
// Check if state is valid (non-zero)
|
||||
if (now) {
|
||||
// Check if state is valid (non-zero)
|
||||
vocTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
if (vocStateStable() && vocValid) {
|
||||
saveState();
|
||||
} else {
|
||||
LOG_INFO("SEN5X: Not stopping measurement, vocState is not stable yet!");
|
||||
if (!(vocStateStable() && vocValid)) {
|
||||
LOG_INFO("%s: Not stopping measurement, vocState is not stable yet!", sensorName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Save state and prefs (on all models)
|
||||
saveState();
|
||||
}
|
||||
|
||||
if (!oneShotMode) {
|
||||
LOG_INFO("SEN5X: Not stopping measurement, continuous mode!");
|
||||
LOG_INFO("%s: Not stopping measurement, continuous mode!", sensorName);
|
||||
return true;
|
||||
} else {
|
||||
LOG_INFO("%s: One shot mode enabled", sensorName);
|
||||
}
|
||||
|
||||
// Switch to low-power based on the model
|
||||
if (model == SEN50) {
|
||||
if (!sendCommand(SEN5X_STOP_MEASUREMENT)) {
|
||||
LOG_ERROR("SEN5X: Error stopping measurement");
|
||||
LOG_ERROR("%s: Error stopping measurement", sensorName);
|
||||
return false;
|
||||
}
|
||||
state = SEN5X_IDLE;
|
||||
LOG_INFO("SEN5X: Stop measurement mode");
|
||||
LOG_INFO("%s: Stop measurement mode", sensorName);
|
||||
} else {
|
||||
if (!sendCommand(SEN5X_START_MEASUREMENT_RHT_GAS)) {
|
||||
LOG_ERROR("SEN5X: Error switching to RHT/Gas measurement");
|
||||
LOG_ERROR("%s: Error switching to RHT/Gas measurement", sensorName);
|
||||
return false;
|
||||
}
|
||||
state = SEN5X_RHTGAS_ONLY;
|
||||
LOG_INFO("SEN5X: Switch to RHT/Gas only measurement mode");
|
||||
LOG_INFO("%s: Switch to RHT/Gas only measurement mode", sensorName);
|
||||
}
|
||||
|
||||
delay(200); // From Sensirion Datasheet
|
||||
@@ -289,10 +290,10 @@ bool SEN5XSensor::vocStateValid()
|
||||
{
|
||||
if (!vocState[0] && !vocState[1] && !vocState[2] && !vocState[3] && !vocState[4] && !vocState[5] && !vocState[6] &&
|
||||
!vocState[7]) {
|
||||
LOG_DEBUG("SEN5X: VOC state is all 0, invalid");
|
||||
LOG_DEBUG("%s: VOC state is all 0, invalid", sensorName);
|
||||
return false;
|
||||
} else {
|
||||
LOG_DEBUG("SEN5X: VOC state is valid");
|
||||
LOG_DEBUG("%s: VOC state is valid", sensorName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -618,7 +619,7 @@ bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
}
|
||||
} else {
|
||||
// TODO - Should this actually ignore? We could end up never cleaning...
|
||||
LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved state. Trying again later");
|
||||
LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved cleaning and VOC state");
|
||||
}
|
||||
|
||||
idle(false);
|
||||
@@ -665,16 +666,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 +728,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 +920,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 +940,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;
|
||||
@@ -954,4 +966,4 @@ AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPa
|
||||
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -114,7 +114,7 @@ See: https://sensirion.com/resource/application_note/low_power_mode/sen5x
|
||||
bool sendCommand(uint16_t command);
|
||||
bool sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber = 0);
|
||||
uint8_t readBuffer(uint8_t *buffer, uint8_t byteNumber); // Return number of bytes received
|
||||
uint8_t sen5xCRC(uint8_t *buffer);
|
||||
uint8_t sen5xCRC(const uint8_t *buffer);
|
||||
bool startCleaning();
|
||||
uint8_t getMeasurements();
|
||||
// bool readRawValues();
|
||||
|
||||
@@ -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
|
||||
@@ -266,7 +266,7 @@ void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtasti
|
||||
}
|
||||
}
|
||||
|
||||
void TraceRouteModule::updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r)
|
||||
void TraceRouteModule::updateNextHops(const meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r)
|
||||
{
|
||||
// E.g. if the route is A->B->C->D and we are B, we can set C as next-hop for C and D
|
||||
// Similarly, if we are C, we can set D as next-hop for D
|
||||
|
||||
@@ -62,7 +62,7 @@ class TraceRouteModule : public ProtobufModule<meshtastic_RouteDiscovery>,
|
||||
void appendMyIDandSNR(meshtastic_RouteDiscovery *r, float snr, bool isTowardsDestination, bool SNRonly);
|
||||
|
||||
// Update next-hops in the routing table based on the returned route
|
||||
void updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r);
|
||||
void updateNextHops(const meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r);
|
||||
|
||||
// Helper to update next-hop for a single node
|
||||
void maybeSetNextHop(NodeNum target, uint8_t nextHopByte);
|
||||
|
||||
Reference in New Issue
Block a user