mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-18 09:21:43 +10:00
Merge branch 'develop' into pioarduino
This commit is contained in:
+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.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "mesh/compression/unishox2.h"
|
||||
#include "meshUtils.h"
|
||||
#include "meshtastic/atak.pb.h"
|
||||
|
||||
AtakPluginModule *atakPluginModule;
|
||||
@@ -70,16 +71,17 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
auto compressed = cloneTAKPacketData(t);
|
||||
compressed.is_compressed = true;
|
||||
if (t->has_contact) {
|
||||
auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign,
|
||||
sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
auto length = unishox2_compress_lines(
|
||||
t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
|
||||
compressed.contact.callsign, sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Compressed callsign: %d bytes", length);
|
||||
length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
|
||||
compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1,
|
||||
USX_PSET_DFLT, NULL);
|
||||
length = unishox2_compress_lines(
|
||||
t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
|
||||
compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet");
|
||||
return;
|
||||
@@ -87,9 +89,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
LOG_DEBUG("Compressed device_callsign: %d bytes", length);
|
||||
}
|
||||
if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) {
|
||||
auto length = unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
|
||||
compressed.payload_variant.chat.message,
|
||||
sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
|
||||
auto length = unishox2_compress_lines(
|
||||
t->payload_variant.chat.message,
|
||||
pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
|
||||
compressed.payload_variant.chat.message, sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
|
||||
NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet");
|
||||
return;
|
||||
@@ -98,9 +102,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
|
||||
if (t->payload_variant.chat.has_to) {
|
||||
compressed.payload_variant.chat.has_to = true;
|
||||
length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
|
||||
compressed.payload_variant.chat.to,
|
||||
sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
|
||||
length = unishox2_compress_lines(
|
||||
t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
|
||||
compressed.payload_variant.chat.to, sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet");
|
||||
return;
|
||||
@@ -110,9 +114,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
|
||||
if (t->payload_variant.chat.has_to_callsign) {
|
||||
compressed.payload_variant.chat.has_to_callsign = true;
|
||||
length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
|
||||
compressed.payload_variant.chat.to_callsign,
|
||||
sizeof(compressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
length = unishox2_compress_lines(
|
||||
t->payload_variant.chat.to_callsign,
|
||||
pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
|
||||
compressed.payload_variant.chat.to_callsign, sizeof(compressed.payload_variant.chat.to_callsign) - 1,
|
||||
USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet");
|
||||
return;
|
||||
@@ -134,18 +140,18 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
auto uncompressed = cloneTAKPacketData(t);
|
||||
uncompressed.is_compressed = false;
|
||||
if (t->has_contact) {
|
||||
auto length =
|
||||
unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign,
|
||||
sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
auto length = unishox2_decompress_lines(
|
||||
t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
|
||||
uncompressed.contact.callsign, sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow contact.callsign. Bailing out");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Decompressed callsign: %d bytes", length);
|
||||
|
||||
length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
|
||||
uncompressed.contact.device_callsign,
|
||||
sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
length = unishox2_decompress_lines(
|
||||
t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
|
||||
uncompressed.contact.device_callsign, sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow contact.device_callsign. Bailing out");
|
||||
return;
|
||||
@@ -153,9 +159,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
LOG_DEBUG("Decompressed device_callsign: %d bytes", length);
|
||||
}
|
||||
if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) {
|
||||
auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
|
||||
uncompressed.payload_variant.chat.message,
|
||||
sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
|
||||
auto length = unishox2_decompress_lines(
|
||||
t->payload_variant.chat.message,
|
||||
pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
|
||||
uncompressed.payload_variant.chat.message, sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
|
||||
NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow chat.message. Bailing out");
|
||||
return;
|
||||
@@ -164,9 +172,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
|
||||
if (t->payload_variant.chat.has_to) {
|
||||
uncompressed.payload_variant.chat.has_to = true;
|
||||
length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
|
||||
uncompressed.payload_variant.chat.to,
|
||||
sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
|
||||
length = unishox2_decompress_lines(
|
||||
t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
|
||||
uncompressed.payload_variant.chat.to, sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow chat.to. Bailing out");
|
||||
return;
|
||||
@@ -176,10 +184,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
|
||||
if (t->payload_variant.chat.has_to_callsign) {
|
||||
uncompressed.payload_variant.chat.has_to_callsign = true;
|
||||
length =
|
||||
unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
|
||||
uncompressed.payload_variant.chat.to_callsign,
|
||||
sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
length = unishox2_decompress_lines(
|
||||
t->payload_variant.chat.to_callsign,
|
||||
pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
|
||||
uncompressed.payload_variant.chat.to_callsign, sizeof(uncompressed.payload_variant.chat.to_callsign) - 1,
|
||||
USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow chat.to_callsign. Bailing out");
|
||||
return;
|
||||
|
||||
@@ -130,8 +130,7 @@ CannedMessageModule::CannedMessageModule()
|
||||
: SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage")
|
||||
{
|
||||
this->loadProtoForModule();
|
||||
if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE &&
|
||||
!CANNED_MESSAGE_MODULE_ENABLE) {
|
||||
if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE) {
|
||||
LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled");
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED;
|
||||
disable();
|
||||
@@ -1210,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;
|
||||
}
|
||||
@@ -1238,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;
|
||||
@@ -1256,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
|
||||
@@ -2071,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) {
|
||||
|
||||
@@ -27,10 +27,6 @@ enum CannedMessageModuleIconType { shift, backspace, space, enter };
|
||||
#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50
|
||||
#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800
|
||||
|
||||
#ifndef CANNED_MESSAGE_MODULE_ENABLE
|
||||
#define CANNED_MESSAGE_MODULE_ENABLE 0
|
||||
#endif
|
||||
|
||||
// ============================
|
||||
// Data Structures
|
||||
// ============================
|
||||
|
||||
@@ -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();
|
||||
@@ -442,6 +348,7 @@ ExternalNotificationModule::ExternalNotificationModule()
|
||||
|
||||
ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// Trigger external notification if enabled and not muted; isSilenced is from temporary mute toggles
|
||||
if (moduleConfig.external_notification.enabled && !isSilenced) {
|
||||
#ifdef T_WATCH_S3
|
||||
drv.setWaveform(0, 75);
|
||||
@@ -456,99 +363,57 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
for (size_t i = 0; i < p.payload.size; i++) {
|
||||
if (p.payload.bytes[i] == ASCII_BELL) {
|
||||
containsBell = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// If we receive a direct message and the receipent is us, apply DM mute setting
|
||||
// Else we just handle it as not muted.
|
||||
const bool directToUs = !isBroadcast(mp.to) && isToUs(&mp);
|
||||
bool is_muted = directToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0))
|
||||
: (ch.settings.has_module_settings && ch.settings.module_settings.is_muted);
|
||||
const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp);
|
||||
bool is_muted = isDmToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0))
|
||||
: (ch.settings.has_module_settings && ch.settings.module_settings.is_muted);
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell) {
|
||||
if (containsBell) {
|
||||
LOG_INFO("externalNotificationModule - Notification Bell");
|
||||
const bool buzzerModeIsDirectOnly =
|
||||
(config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY);
|
||||
|
||||
if (containsBell || !is_muted) {
|
||||
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message ||
|
||||
moduleConfig.external_notification.alert_bell_vibra ||
|
||||
moduleConfig.external_notification.alert_message_vibra ||
|
||||
((moduleConfig.external_notification.alert_bell_buzzer ||
|
||||
moduleConfig.external_notification.alert_message_buzzer) &&
|
||||
canBuzz())) {
|
||||
nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout
|
||||
? (moduleConfig.external_notification.nag_timeout * 1000)
|
||||
: moduleConfig.external_notification.output_ms);
|
||||
LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff);
|
||||
isNagging = true;
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module or Bell");
|
||||
setExternalState(0, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell_vibra) {
|
||||
if (containsBell) {
|
||||
LOG_INFO("externalNotificationModule - Notification Bell (Vibra)");
|
||||
isNagging = true;
|
||||
if (moduleConfig.external_notification.alert_bell_vibra ||
|
||||
moduleConfig.external_notification.alert_message_vibra) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module or Bell (Vibra)");
|
||||
setExternalState(1, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
}
|
||||
|
||||
if ((moduleConfig.external_notification.alert_bell_buzzer ||
|
||||
moduleConfig.external_notification.alert_message_buzzer) &&
|
||||
canBuzz()) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module or Bell (Buzzer)");
|
||||
if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) {
|
||||
LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY");
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) {
|
||||
if (containsBell) {
|
||||
LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)");
|
||||
isNagging = true;
|
||||
if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
setExternalState(2, true);
|
||||
} else {
|
||||
#ifdef HAS_I2S
|
||||
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
} else
|
||||
#endif
|
||||
if (moduleConfig.external_notification.use_pwm) {
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
}
|
||||
}
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_message && !is_muted) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module");
|
||||
isNagging = true;
|
||||
setExternalState(0, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_message_vibra && !is_muted) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module (Vibra)");
|
||||
isNagging = true;
|
||||
setExternalState(1, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_message_buzzer && !is_muted) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module (Buzzer)");
|
||||
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
|
||||
(!isBroadcast(mp.to) && isToUs(&mp))) {
|
||||
// Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us
|
||||
isNagging = true;
|
||||
// Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us
|
||||
#ifdef T_LORA_PAGER
|
||||
if (canBuzz()) {
|
||||
drv.setWaveform(0, 16); // Long buzzer 100%
|
||||
drv.setWaveform(1, 0); // Pause
|
||||
drv.setWaveform(2, 16);
|
||||
@@ -558,11 +423,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
drv.setWaveform(6, 16);
|
||||
drv.setWaveform(7, 0);
|
||||
drv.go();
|
||||
}
|
||||
#endif
|
||||
if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
setExternalState(2, true);
|
||||
} else {
|
||||
#ifdef HAS_I2S
|
||||
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
@@ -570,18 +431,13 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
#endif
|
||||
if (moduleConfig.external_notification.use_pwm) {
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
} else {
|
||||
setExternalState(2, true);
|
||||
}
|
||||
}
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
} else {
|
||||
// Don't beep if buzzer mode is "direct messages only" and it is no direct message
|
||||
LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY");
|
||||
}
|
||||
}
|
||||
|
||||
setIntervalFromNow(0); // run once so we know if we should do something
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -123,7 +123,7 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
|
||||
// generate nonce
|
||||
updateState();
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;)
|
||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
|
||||
return false;
|
||||
}
|
||||
currentNonce = random();
|
||||
@@ -259,7 +259,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
p->priority = meshtastic_MeshPacket_Priority_HIGH;
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_USER;
|
||||
IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);)
|
||||
IF_SCREEN(screen->requestMenu(graphics::menuHandler::KeyVerificationFinalPrompt);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
|
||||
|
||||
+21
-77
@@ -1,24 +1,11 @@
|
||||
#include "configuration.h"
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
#include "buzz/BuzzerFeedbackThread.h"
|
||||
#include "input/ExpressLRSFiveWay.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "input/RotaryEncoderImpl.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/SerialKeyboardImpl.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "input/i2cButton.h"
|
||||
#include "modules/SystemCommandsModule.h"
|
||||
#if HAS_TRACKBALL
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#endif
|
||||
|
||||
#include "modules/StatusLEDModule.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
#include "input/cardKbI2cImpl.h"
|
||||
#endif
|
||||
#include "input/kbMatrixImpl.h"
|
||||
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
#include "ReplyBotModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
#include "KeyVerificationModule.h"
|
||||
@@ -59,8 +46,6 @@
|
||||
#include "modules/WaypointModule.h"
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
#include "input/LinuxInputImpl.h"
|
||||
#include "input/SeesawRotary.h"
|
||||
#include "modules/Telemetry/HostMetrics.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
#include "modules/StoreForwardModule.h"
|
||||
@@ -71,11 +56,15 @@
|
||||
#endif
|
||||
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
#include "main.h"
|
||||
#include "modules/Telemetry/AirQualityTelemetry.h"
|
||||
#include "modules/Telemetry/EnvironmentTelemetry.h"
|
||||
#include "modules/Telemetry/HealthTelemetry.h"
|
||||
#include "modules/Telemetry/Sensor/TelemetrySensor.h"
|
||||
#endif
|
||||
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
#include "main.h"
|
||||
#include "modules/Telemetry/AirQualityTelemetry.h"
|
||||
#include "modules/Telemetry/Sensor/TelemetrySensor.h"
|
||||
#endif
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY
|
||||
#include "modules/Telemetry/PowerTelemetry.h"
|
||||
#endif
|
||||
@@ -108,7 +97,13 @@
|
||||
#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"
|
||||
#endif
|
||||
/**
|
||||
* Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else)
|
||||
*/
|
||||
@@ -121,10 +116,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
|
||||
@@ -165,6 +160,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
|
||||
@@ -179,63 +177,6 @@ void setupModules()
|
||||
#endif
|
||||
// Example: Put your module here
|
||||
// new ReplyModule();
|
||||
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
#if defined(T_LORA_PAGER)
|
||||
// use a special FSM based rotary encoder version for T-LoRa Pager
|
||||
rotaryEncoderImpl = new RotaryEncoderImpl();
|
||||
if (!rotaryEncoderImpl->init()) {
|
||||
delete rotaryEncoderImpl;
|
||||
rotaryEncoderImpl = nullptr;
|
||||
}
|
||||
#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2)
|
||||
upDownInterruptImpl1 = new UpDownInterruptImpl1();
|
||||
if (!upDownInterruptImpl1->init()) {
|
||||
delete upDownInterruptImpl1;
|
||||
upDownInterruptImpl1 = nullptr;
|
||||
}
|
||||
#else
|
||||
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
|
||||
if (!rotaryEncoderInterruptImpl1->init()) {
|
||||
delete rotaryEncoderInterruptImpl1;
|
||||
rotaryEncoderInterruptImpl1 = nullptr;
|
||||
}
|
||||
#endif
|
||||
cardKbI2cImpl = new CardKbI2cImpl();
|
||||
cardKbI2cImpl->init();
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
i2cButton = new i2cButtonThread("i2cButtonThread");
|
||||
#endif
|
||||
#ifdef INPUTBROKER_MATRIX_TYPE
|
||||
kbMatrixImpl = new KbMatrixImpl();
|
||||
kbMatrixImpl->init();
|
||||
#endif // INPUTBROKER_MATRIX_TYPE
|
||||
#ifdef INPUTBROKER_SERIAL_TYPE
|
||||
aSerialKeyboardImpl = new SerialKeyboardImpl();
|
||||
aSerialKeyboardImpl->init();
|
||||
#endif // INPUTBROKER_MATRIX_TYPE
|
||||
}
|
||||
#endif // HAS_BUTTON
|
||||
#if ARCH_PORTDUINO
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") {
|
||||
seesawRotary = new SeesawRotary("SeesawRotary");
|
||||
if (!seesawRotary->init()) {
|
||||
delete seesawRotary;
|
||||
seesawRotary = nullptr;
|
||||
}
|
||||
aLinuxInputImpl = new LinuxInputImpl();
|
||||
aLinuxInputImpl->init();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
trackballInterruptImpl1 = new TrackballInterruptImpl1();
|
||||
trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS);
|
||||
}
|
||||
#endif
|
||||
#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE
|
||||
expressLRSFiveWayInput = new ExpressLRSFiveWay();
|
||||
#endif
|
||||
#if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
cannedMessageModule = new CannedMessageModule();
|
||||
@@ -304,6 +245,9 @@ void setupModules()
|
||||
#if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS
|
||||
if (moduleConfig.has_range_test && moduleConfig.range_test.enabled)
|
||||
new RangeTestModule();
|
||||
#endif
|
||||
#if defined(HAS_HARDWARE_WATCHDOG)
|
||||
watchdogThread = new WatchdogThread();
|
||||
#endif
|
||||
// NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending extra
|
||||
// acks
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeStatus.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include <Throttle.h>
|
||||
@@ -129,14 +131,18 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
|
||||
LOG_DEBUG("Skip send NodeInfo > 40%% ch. util");
|
||||
return NULL;
|
||||
}
|
||||
// If we sent our NodeInfo less than 5 min. ago, don't send it again as it may be still underway.
|
||||
if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 5 * 60 * 1000)) {
|
||||
LOG_DEBUG("Skip send NodeInfo since we sent it <5min ago");
|
||||
|
||||
// Use graduated scaling based on active mesh size (10 minute base, scales with congestion coefficient)
|
||||
uint32_t timeoutMs = Default::getConfiguredOrDefaultMsScaled(0, 10 * 60, nodeStatus->getNumOnline());
|
||||
uint32_t lastNodeInfo = transmitHistory ? transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP) : 0;
|
||||
if (!shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, timeoutMs)) {
|
||||
LOG_DEBUG("Skip send NodeInfo since we sent it <%us ago", timeoutMs / 1000);
|
||||
ignoreRequest = true; // Mark it as ignored for MeshModule
|
||||
return NULL;
|
||||
} else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) {
|
||||
} else if (shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, 60 * 1000)) {
|
||||
// For interactive/urgent requests (e.g., user-triggered or implicit requests), use a shorter 60s timeout
|
||||
LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago");
|
||||
ignoreRequest = true; // Mark it as ignored for MeshModule
|
||||
ignoreRequest = true;
|
||||
return NULL;
|
||||
} else {
|
||||
ignoreRequest = false; // Don't ignore requests anymore
|
||||
@@ -144,7 +150,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;
|
||||
}
|
||||
|
||||
@@ -155,7 +161,8 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
|
||||
strcpy(u.id, nodeDB->getNodeId().c_str());
|
||||
|
||||
LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name);
|
||||
lastSentToMesh = millis();
|
||||
if (transmitHistory)
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||
return allocDataProtobuf(u);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ class NodeInfoModule : public ProtobufModule<meshtastic_User>, private concurren
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh
|
||||
bool shorterTimeout = false;
|
||||
bool suppressReplyForCurrentRequest = false;
|
||||
std::map<NodeNum, uint32_t> lastNodeInfoSeen;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "airtime.h"
|
||||
#include "configuration.h"
|
||||
@@ -27,6 +28,15 @@ PositionModule::PositionModule()
|
||||
isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others
|
||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||
|
||||
// Seed throttle timer from persisted transmit history so we don't re-broadcast immediately after reboot
|
||||
if (transmitHistory) {
|
||||
uint32_t restored = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
|
||||
if (restored != 0) {
|
||||
lastGpsSend = restored;
|
||||
LOG_INFO("Position: restored lastGpsSend from transmit history");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
|
||||
config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) {
|
||||
setIntervalFromNow(setStartDelay());
|
||||
@@ -438,6 +448,8 @@ int32_t PositionModule::runOnce()
|
||||
lastGpsLatitude = node->position.latitude_i;
|
||||
lastGpsLongitude = node->position.longitude_i;
|
||||
|
||||
if (transmitHistory)
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
|
||||
sendOurPosition();
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) {
|
||||
sendLostAndFoundText();
|
||||
|
||||
@@ -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
|
||||
@@ -67,6 +67,8 @@ uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp)
|
||||
#if !(EVENTMODE) // This falls through to the default.
|
||||
return hopsUsed; // If the request used more hops than the limit, use the same amount of hops
|
||||
#endif
|
||||
} else if (mp.hop_start == 0) {
|
||||
return 0; // The requesting node wanted 0 hops, so the response also uses a direct/local path.
|
||||
} else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) {
|
||||
return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different
|
||||
}
|
||||
|
||||
@@ -63,29 +63,26 @@
|
||||
SerialModule *serialModule;
|
||||
SerialModuleRadio *serialModuleRadio;
|
||||
|
||||
#if defined(TTGO_T_ECHO) || defined(TTGO_T_ECHO_PLUS) || defined(CANARYONE) || defined(MESHLINK) || \
|
||||
defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M5) || \
|
||||
defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE)
|
||||
|
||||
SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
}
|
||||
static Print *serialPrint = &Serial;
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172) || defined(EBYTE_E77_MBL)
|
||||
SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
}
|
||||
static Print *serialPrint = &Serial1;
|
||||
#else
|
||||
SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Serial")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
}
|
||||
static Print *serialPrint = &Serial2;
|
||||
#ifndef SERIAL_PRINT_PORT
|
||||
#define SERIAL_PRINT_PORT 2
|
||||
#endif
|
||||
|
||||
#if SERIAL_PRINT_PORT == 0
|
||||
#define SERIAL_PRINT_OBJECT Serial
|
||||
#elif SERIAL_PRINT_PORT == 1
|
||||
#define SERIAL_PRINT_OBJECT Serial1
|
||||
#elif SERIAL_PRINT_PORT == 2
|
||||
#define SERIAL_PRINT_OBJECT Serial2
|
||||
#else
|
||||
#error "Unsupported SERIAL_PRINT_PORT value. Allowed values are 0, 1, or 2."
|
||||
#endif
|
||||
|
||||
SerialModule::SerialModule() : StreamAPI(&SERIAL_PRINT_OBJECT), concurrency::OSThread("Serial")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
}
|
||||
static Print *serialPrint = &SERIAL_PRINT_OBJECT;
|
||||
|
||||
char serialBytes[512];
|
||||
size_t serialPayloadSize;
|
||||
|
||||
@@ -205,9 +202,7 @@ int32_t SerialModule::runOnce()
|
||||
Serial.begin(baud);
|
||||
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
|
||||
}
|
||||
#elif !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \
|
||||
!defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \
|
||||
!defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE)
|
||||
#elif SERIAL_PRINT_PORT != 0
|
||||
|
||||
if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
|
||||
#ifdef ARCH_RP2040
|
||||
@@ -264,9 +259,7 @@ int32_t SerialModule::runOnce()
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \
|
||||
!defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \
|
||||
!defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE)
|
||||
#if SERIAL_PRINT_PORT != 0
|
||||
else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) {
|
||||
processWXSerial();
|
||||
|
||||
@@ -540,11 +533,7 @@ ParsedLine parseLine(const char *line)
|
||||
*/
|
||||
void SerialModule::processWXSerial()
|
||||
{
|
||||
#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \
|
||||
!defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \
|
||||
!defined(ELECROW_ThinkNode_M3) && \
|
||||
!defined(ELECROW_ThinkNode_M4) && \
|
||||
!defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE)
|
||||
#if SERIAL_PRINT_PORT != 0 && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
|
||||
static unsigned int lastAveraged = 0;
|
||||
static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded.
|
||||
|
||||
@@ -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,15 +140,38 @@ int32_t StatusLEDModule::runOnce()
|
||||
if (powerStatus && powerStatus->getBatteryChargePercent() >= 75)
|
||||
chargeIndicatorLED4 = LED_STATE_ON;
|
||||
}
|
||||
|
||||
#ifdef LED_CHARGE
|
||||
digitalWrite(LED_CHARGE, CHARGE_LED_state);
|
||||
#endif
|
||||
// digitalWrite(green_LED_PIN, LED_STATE_OFF);
|
||||
|
||||
#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
|
||||
@@ -150,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.
|
||||
@@ -513,7 +511,7 @@ bool StoreForwardModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp,
|
||||
LOG_DEBUG("StoreAndForward_RequestResponse_ROUTER_BUSY");
|
||||
// retry in messages_saved * packetTimeMax ms
|
||||
retry_delay = millis() + getNumAvailablePackets(this->busyTo, this->last_time) * packetTimeMax *
|
||||
(meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1);
|
||||
(p->rr == meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "DebugConfiguration.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
@@ -10,7 +11,7 @@
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "Sensor/AddI2CSensorTemplate.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "UnitConversions.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
@@ -19,8 +20,21 @@
|
||||
#include "sleep.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
static constexpr uint16_t TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY = 0x8004;
|
||||
|
||||
// Sensors
|
||||
#include "Sensor/AddI2CSensorTemplate.h"
|
||||
#include "Sensor/PMSA003ISensor.h"
|
||||
#include "Sensor/SEN5XSensor.h"
|
||||
#if __has_include(<SensirionI2cScd4x.h>)
|
||||
#include "Sensor/SCD4XSensor.h"
|
||||
#endif
|
||||
#if __has_include(<SensirionI2cSfa3x.h>)
|
||||
#include "Sensor/SFA30Sensor.h"
|
||||
#endif
|
||||
#if __has_include(<SensirionI2cScd30.h>)
|
||||
#include "Sensor/SCD30Sensor.h"
|
||||
#endif
|
||||
|
||||
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
{
|
||||
@@ -42,6 +56,16 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
|
||||
// order by priority of metrics/values (low top, high bottom)
|
||||
addSensor<PMSA003ISensor>(i2cScanner, ScanI2C::DeviceType::PMSA003I);
|
||||
addSensor<SEN5XSensor>(i2cScanner, ScanI2C::DeviceType::SEN5X);
|
||||
#if __has_include(<SensirionI2cScd4x.h>)
|
||||
addSensor<SCD4XSensor>(i2cScanner, ScanI2C::DeviceType::SCD4X);
|
||||
#endif
|
||||
#if __has_include(<SensirionI2cSfa3x.h>)
|
||||
addSensor<SFA30Sensor>(i2cScanner, ScanI2C::DeviceType::SFA30);
|
||||
#endif
|
||||
#if __has_include(<SensirionI2cScd30.h>)
|
||||
addSensor<SCD30Sensor>(i2cScanner, ScanI2C::DeviceType::SCD30);
|
||||
#endif
|
||||
}
|
||||
|
||||
int32_t AirQualityTelemetryModule::runOnce()
|
||||
@@ -63,7 +87,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) {
|
||||
@@ -85,21 +110,41 @@ int32_t AirQualityTelemetryModule::runOnce()
|
||||
}
|
||||
|
||||
// Wake up the sensors that need it
|
||||
LOG_INFO("Waking up sensors");
|
||||
LOG_INFO("Waking up sensors...");
|
||||
uint32_t lastTelemetry =
|
||||
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY) : 0;
|
||||
for (TelemetrySensor *sensor : sensors) {
|
||||
if (!sensor->isActive()) {
|
||||
return sensor->wakeUp();
|
||||
if (!sensor->canSleep()) {
|
||||
LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName);
|
||||
} else if (((lastTelemetry == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastTelemetry - sensor->wakeUpTimeMs(),
|
||||
Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
if (!sensor->isActive()) {
|
||||
LOG_DEBUG("Waking up: %s", sensor->sensorName);
|
||||
return sensor->wakeUp();
|
||||
} else {
|
||||
int32_t pendingForReadyMs = sensor->pendingForReadyMs();
|
||||
LOG_DEBUG("%s. Pending for ready %ums", sensor->sensorName, pendingForReadyMs);
|
||||
if (pendingForReadyMs) {
|
||||
return pendingForReadyMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (((lastSentToMesh == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
if (((lastTelemetry == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
sendTelemetry();
|
||||
lastSentToMesh = millis();
|
||||
if (transmitHistory)
|
||||
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY);
|
||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||
(service->isToPhoneQueueEmpty())) {
|
||||
// Just send to phone when it's not our time to send to mesh yet
|
||||
@@ -109,9 +154,18 @@ int32_t AirQualityTelemetryModule::runOnce()
|
||||
}
|
||||
|
||||
// Send to sleep sensors that consume power
|
||||
LOG_INFO("Sending sensors to sleep");
|
||||
LOG_DEBUG("Sending sensors to sleep");
|
||||
for (TelemetrySensor *sensor : sensors) {
|
||||
sensor->sleep();
|
||||
if (sensor->isActive() && sensor->canSleep()) {
|
||||
if (sensor->wakeUpTimeMs() <
|
||||
(int32_t)Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes)) {
|
||||
LOG_DEBUG("Disabling %s until next period", sensor->sensorName);
|
||||
sensor->sleep();
|
||||
} else {
|
||||
LOG_DEBUG("Sensor stays enabled due to warm up period");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return min(sendToPhoneIntervalMs, result);
|
||||
@@ -158,8 +212,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
const auto &m = telemetry.variant.air_quality_metrics;
|
||||
|
||||
// Check if any telemetry field has valid data
|
||||
bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental ||
|
||||
m.has_pm25_environmental || m.has_pm100_environmental;
|
||||
bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_co2;
|
||||
|
||||
if (!hasAny) {
|
||||
display->drawString(x, currentY, "No Telemetry");
|
||||
@@ -186,6 +239,10 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3");
|
||||
if (m.has_pm100_standard)
|
||||
entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3");
|
||||
if (m.has_co2)
|
||||
entries.push_back("CO2: " + String(m.co2) + "ppm");
|
||||
if (m.has_form_formaldehyde)
|
||||
entries.push_back("HCHO: " + String(m.form_formaldehyde) + "ppb");
|
||||
|
||||
// === Show first available metric on top-right of first line ===
|
||||
if (!entries.empty()) {
|
||||
@@ -221,13 +278,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);
|
||||
|
||||
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
|
||||
t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
|
||||
t->variant.air_quality_metrics.pm100_environmental);
|
||||
if (t->variant.air_quality_metrics.has_co2)
|
||||
LOG_INFO("CO2=%i, CO2_T=%.2f, CO2_H=%.2f", t->variant.air_quality_metrics.co2,
|
||||
t->variant.air_quality_metrics.co2_temperature, t->variant.air_quality_metrics.co2_humidity);
|
||||
|
||||
if (t->variant.air_quality_metrics.has_form_formaldehyde)
|
||||
LOG_INFO("HCHO=%.2f, HCHO_T=%.2f, HCHO_H=%.2f", t->variant.air_quality_metrics.form_formaldehyde,
|
||||
t->variant.air_quality_metrics.form_temperature, t->variant.air_quality_metrics.form_humidity);
|
||||
#endif
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
@@ -241,17 +304,20 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
|
||||
|
||||
bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
|
||||
{
|
||||
bool valid = true;
|
||||
// Note: this is different to the case in EnvironmentTelemetryModule
|
||||
// There, if any sensor fails to read - valid = false.
|
||||
bool valid = false;
|
||||
bool hasSensor = false;
|
||||
m->time = getTime();
|
||||
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
|
||||
m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero;
|
||||
|
||||
// TODO - Should we check for sensor state here?
|
||||
// If a sensor is sleeping, we should know and check to wake it up
|
||||
bool sensor_get = false;
|
||||
for (TelemetrySensor *sensor : sensors) {
|
||||
LOG_INFO("Reading AQ sensors");
|
||||
valid = valid && sensor->getMetrics(m);
|
||||
LOG_DEBUG("Reading %s", sensor->sensorName);
|
||||
// Note - this function doesn't get properly called if within a conditional
|
||||
sensor_get = sensor->getMetrics(m);
|
||||
valid = valid || sensor_get;
|
||||
hasSensor = true;
|
||||
}
|
||||
|
||||
@@ -261,6 +327,10 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
|
||||
meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
|
||||
{
|
||||
if (currentRequest) {
|
||||
if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) {
|
||||
ignoreRequest = true;
|
||||
return NULL;
|
||||
}
|
||||
auto req = *currentRequest;
|
||||
const auto &p = req.decoded;
|
||||
meshtastic_Telemetry scratch;
|
||||
@@ -291,12 +361,38 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||
m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
|
||||
m.time = getTime();
|
||||
|
||||
if (getAirQualityTelemetry(&m)) {
|
||||
LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \
|
||||
pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u",
|
||||
m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard,
|
||||
m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental,
|
||||
m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental);
|
||||
|
||||
bool hasAnyPM =
|
||||
m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard ||
|
||||
m.variant.air_quality_metrics.has_pm100_standard || m.variant.air_quality_metrics.has_pm10_environmental ||
|
||||
m.variant.air_quality_metrics.has_pm25_environmental || m.variant.air_quality_metrics.has_pm100_environmental;
|
||||
|
||||
if (hasAnyPM) {
|
||||
LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", m.variant.air_quality_metrics.pm10_standard,
|
||||
m.variant.air_quality_metrics.pm25_standard, m.variant.air_quality_metrics.pm100_standard);
|
||||
if (m.variant.air_quality_metrics.has_pm10_environmental)
|
||||
LOG_INFO("pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u",
|
||||
m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental,
|
||||
m.variant.air_quality_metrics.pm100_environmental);
|
||||
}
|
||||
|
||||
bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature ||
|
||||
m.variant.air_quality_metrics.has_co2_humidity;
|
||||
|
||||
if (hasAnyCO2) {
|
||||
LOG_INFO("Send: co2=%i, co2_t=%.2f, co2_rh=%.2f", m.variant.air_quality_metrics.co2,
|
||||
m.variant.air_quality_metrics.co2_temperature, m.variant.air_quality_metrics.co2_humidity);
|
||||
}
|
||||
|
||||
bool hasAnyHCHO = m.variant.air_quality_metrics.has_form_formaldehyde ||
|
||||
m.variant.air_quality_metrics.has_form_temperature || m.variant.air_quality_metrics.has_form_humidity;
|
||||
|
||||
if (hasAnyHCHO) {
|
||||
LOG_INFO("Send: hcho=%.2f, hcho_t=%.2f, hcho_rh=%.2f", m.variant.air_quality_metrics.form_formaldehyde,
|
||||
m.variant.air_quality_metrics.form_temperature, m.variant.air_quality_metrics.form_humidity);
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(m);
|
||||
p->to = dest;
|
||||
@@ -331,6 +427,20 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
LOG_DEBUG("Start next execution in 5s, then sleep");
|
||||
setIntervalFromNow(FIVE_SECONDS_MS);
|
||||
}
|
||||
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
|
||||
meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
|
||||
notification->level = meshtastic_LogRecord_Level_INFO;
|
||||
notification->time = getValidTime(RTCQualityFromNet);
|
||||
sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment",
|
||||
Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs) /
|
||||
1000U);
|
||||
service->sendClientNotification(notification);
|
||||
sleepOnNextExecution = true;
|
||||
LOG_DEBUG("Start next execution in 5s, then sleep");
|
||||
setIntervalFromNow(FIVE_SECONDS_MS);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -352,4 +462,4 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BaseTelemetryModule.h"
|
||||
|
||||
#ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE
|
||||
#define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0
|
||||
#endif
|
||||
@@ -17,6 +19,7 @@
|
||||
|
||||
class AirQualityTelemetryModule : private concurrency::OSThread,
|
||||
public ScanI2CConsumer,
|
||||
public BaseTelemetryModule,
|
||||
public ProtobufModule<meshtastic_Telemetry>
|
||||
{
|
||||
CallbackObserver<AirQualityTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
@@ -64,8 +67,8 @@ 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 lastSentToMesh = 0;
|
||||
// uint32_t sendToPhoneIntervalMs = 1000; // Send to phone every minute
|
||||
uint32_t lastSentToPhone = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "NodeDB.h"
|
||||
#include "configuration.h"
|
||||
|
||||
class BaseTelemetryModule
|
||||
{
|
||||
protected:
|
||||
bool isSensorOrRouterRole() const
|
||||
{
|
||||
return config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR ||
|
||||
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER;
|
||||
}
|
||||
};
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "RTC.h"
|
||||
#include "RadioLibInterface.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "memGet.h"
|
||||
@@ -15,21 +16,23 @@
|
||||
#include <meshUtils.h>
|
||||
|
||||
#define MAGIC_USB_BATTERY_LEVEL 101
|
||||
static constexpr uint16_t TX_HISTORY_KEY_DEVICE_TELEMETRY = 0x8001;
|
||||
|
||||
int32_t DeviceTelemetryModule::runOnce()
|
||||
{
|
||||
refreshUptime();
|
||||
bool isImpoliteRole =
|
||||
IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_ROUTER);
|
||||
if (((lastSentToMesh == 0) ||
|
||||
((uptimeLastMs - lastSentToMesh) >=
|
||||
Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_DEVICE_TELEMETRY) : 0;
|
||||
bool isImpoliteRole = isSensorOrRouterRole();
|
||||
if (((lastTelemetry == 0) ||
|
||||
((uptimeLastMs - lastTelemetry) >= Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval,
|
||||
default_telemetry_broadcast_interval_secs,
|
||||
numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() &&
|
||||
config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN &&
|
||||
moduleConfig.telemetry.device_telemetry_enabled) {
|
||||
sendTelemetry();
|
||||
lastSentToMesh = uptimeLastMs;
|
||||
if (transmitHistory)
|
||||
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_DEVICE_TELEMETRY);
|
||||
} else if (service->isToPhoneQueueEmpty()) {
|
||||
// Just send to phone when it's not our time to send to mesh yet
|
||||
// Only send while queue is empty (phone assumed connected)
|
||||
@@ -60,6 +63,10 @@ bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
meshtastic_MeshPacket *DeviceTelemetryModule::allocReply()
|
||||
{
|
||||
if (currentRequest) {
|
||||
if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) {
|
||||
ignoreRequest = true;
|
||||
return NULL;
|
||||
}
|
||||
auto req = *currentRequest;
|
||||
const auto &p = req.decoded;
|
||||
meshtastic_Telemetry scratch;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#pragma once
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "BaseTelemetryModule.h"
|
||||
#include "NodeDB.h"
|
||||
#include "ProtobufModule.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
|
||||
class DeviceTelemetryModule : private concurrency::OSThread,
|
||||
public BaseTelemetryModule,
|
||||
public ProtobufModule<meshtastic_Telemetry>
|
||||
{
|
||||
CallbackObserver<DeviceTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<DeviceTelemetryModule, const meshtastic::Status *>(this, &DeviceTelemetryModule::handleStatusUpdate);
|
||||
@@ -48,7 +51,6 @@ class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModu
|
||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||
uint32_t sendStatsToPhoneIntervalMs = 15 * SECONDS_IN_MINUTE * 1000; // Send stats to phone every 15 minutes
|
||||
uint32_t lastSentStatsToPhone = 0;
|
||||
uint32_t lastSentToMesh = 0;
|
||||
|
||||
void refreshUptime()
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "UnitConversions.h"
|
||||
#include "buzz.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
@@ -145,6 +146,8 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002;
|
||||
|
||||
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
{
|
||||
if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) {
|
||||
@@ -297,7 +300,8 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
||||
// this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the
|
||||
// sensormap here.
|
||||
#ifdef HAS_RAKPROT
|
||||
result = rak9154Sensor.runOnce();
|
||||
if (rak9154Sensor.hasSensor())
|
||||
result = rak9154Sensor.runOnce();
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
@@ -317,14 +321,17 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
||||
}
|
||||
}
|
||||
|
||||
if (((lastSentToMesh == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.environment_update_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
uint32_t lastTelemetry =
|
||||
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0;
|
||||
if (((lastTelemetry == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.environment_update_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
sendTelemetry();
|
||||
lastSentToMesh = millis();
|
||||
if (transmitHistory)
|
||||
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY);
|
||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||
(service->isToPhoneQueueEmpty())) {
|
||||
// Just send to phone when it's not our time to send to mesh yet
|
||||
@@ -529,38 +536,49 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac
|
||||
|
||||
bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m)
|
||||
{
|
||||
bool valid = true;
|
||||
bool valid = false;
|
||||
bool hasSensor = false;
|
||||
// getMetrics() doesn't always get evaluated because of
|
||||
// short-circuit evaluation rules in c++
|
||||
bool get_metrics;
|
||||
m->time = getTime();
|
||||
m->which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
m->variant.environment_metrics = meshtastic_EnvironmentMetrics_init_zero;
|
||||
|
||||
for (TelemetrySensor *sensor : sensors) {
|
||||
valid = valid && sensor->getMetrics(m);
|
||||
get_metrics = sensor->getMetrics(m); // avoid short-circuit evaluation rules
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
|
||||
#ifndef T1000X_SENSOR_EN
|
||||
if (ina219Sensor.hasSensor()) {
|
||||
valid = valid && ina219Sensor.getMetrics(m);
|
||||
get_metrics = ina219Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
if (ina260Sensor.hasSensor()) {
|
||||
valid = valid && ina260Sensor.getMetrics(m);
|
||||
get_metrics = ina260Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
if (ina3221Sensor.hasSensor()) {
|
||||
valid = valid && ina3221Sensor.getMetrics(m);
|
||||
get_metrics = ina3221Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
if (max17048Sensor.hasSensor()) {
|
||||
valid = valid && max17048Sensor.getMetrics(m);
|
||||
get_metrics = max17048Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
#endif
|
||||
#ifdef HAS_RAKPROT
|
||||
valid = valid && rak9154Sensor.getMetrics(m);
|
||||
hasSensor = true;
|
||||
if (rak9154Sensor.hasSensor()) {
|
||||
get_metrics = rak9154Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
#endif
|
||||
return valid && hasSensor;
|
||||
}
|
||||
@@ -568,6 +586,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
|
||||
meshtastic_MeshPacket *EnvironmentTelemetryModule::allocReply()
|
||||
{
|
||||
if (currentRequest) {
|
||||
if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) {
|
||||
ignoreRequest = true;
|
||||
return NULL;
|
||||
}
|
||||
auto req = *currentRequest;
|
||||
const auto &p = req.decoded;
|
||||
meshtastic_Telemetry scratch;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BaseTelemetryModule.h"
|
||||
|
||||
#ifndef ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE
|
||||
#define ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE 0
|
||||
#endif
|
||||
@@ -17,6 +19,7 @@
|
||||
|
||||
class EnvironmentTelemetryModule : private concurrency::OSThread,
|
||||
public ScanI2CConsumer,
|
||||
public BaseTelemetryModule,
|
||||
public ProtobufModule<meshtastic_Telemetry>
|
||||
{
|
||||
CallbackObserver<EnvironmentTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
@@ -65,7 +68,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread,
|
||||
bool firstTime = 1;
|
||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||
uint32_t lastSentToMesh = 0;
|
||||
uint32_t lastSentToPhone = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "UnitConversions.h"
|
||||
#include "main.h"
|
||||
#include "power.h"
|
||||
@@ -33,6 +34,8 @@ MLX90614Sensor mlx90614Sensor;
|
||||
#endif
|
||||
#include <Throttle.h>
|
||||
|
||||
static constexpr uint16_t TX_HISTORY_KEY_HEALTH_TELEMETRY = 0x8003;
|
||||
|
||||
int32_t HealthTelemetryModule::runOnce()
|
||||
{
|
||||
if (sleepOnNextExecution == true) {
|
||||
@@ -69,14 +72,16 @@ int32_t HealthTelemetryModule::runOnce()
|
||||
return disable();
|
||||
}
|
||||
|
||||
if (((lastSentToMesh == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.health_update_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_HEALTH_TELEMETRY) : 0;
|
||||
if (((lastTelemetry == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.health_update_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
sendTelemetry();
|
||||
lastSentToMesh = millis();
|
||||
if (transmitHistory)
|
||||
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_HEALTH_TELEMETRY);
|
||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||
(service->isToPhoneQueueEmpty())) {
|
||||
// Just send to phone when it's not our time to send to mesh yet
|
||||
@@ -168,18 +173,21 @@ bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
|
||||
bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m)
|
||||
{
|
||||
bool valid = true;
|
||||
bool valid = false;
|
||||
bool hasSensor = false;
|
||||
bool get_metrics;
|
||||
m->time = getTime();
|
||||
m->which_variant = meshtastic_Telemetry_health_metrics_tag;
|
||||
m->variant.health_metrics = meshtastic_HealthMetrics_init_zero;
|
||||
|
||||
if (max30102Sensor.hasSensor()) {
|
||||
valid = valid && max30102Sensor.getMetrics(m);
|
||||
get_metrics = max30102Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics; // avoid short-circuit evaluation rules
|
||||
hasSensor = true;
|
||||
}
|
||||
if (mlx90614Sensor.hasSensor()) {
|
||||
valid = valid && mlx90614Sensor.getMetrics(m);
|
||||
get_metrics = mlx90614Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
|
||||
@@ -189,6 +197,10 @@ bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m)
|
||||
meshtastic_MeshPacket *HealthTelemetryModule::allocReply()
|
||||
{
|
||||
if (currentRequest) {
|
||||
if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) {
|
||||
ignoreRequest = true;
|
||||
return NULL;
|
||||
}
|
||||
auto req = *currentRequest;
|
||||
const auto &p = req.decoded;
|
||||
meshtastic_Telemetry scratch;
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
|
||||
#pragma once
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "BaseTelemetryModule.h"
|
||||
#include "NodeDB.h"
|
||||
#include "ProtobufModule.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
|
||||
class HealthTelemetryModule : private concurrency::OSThread,
|
||||
public BaseTelemetryModule,
|
||||
public ProtobufModule<meshtastic_Telemetry>
|
||||
{
|
||||
CallbackObserver<HealthTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<HealthTelemetryModule, const meshtastic::Status *>(this, &HealthTelemetryModule::handleStatusUpdate);
|
||||
@@ -52,7 +55,6 @@ class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModu
|
||||
bool firstTime = 1;
|
||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||
uint32_t lastSentToMesh = 0;
|
||||
uint32_t lastSentToPhone = 0;
|
||||
uint32_t sensor_read_error_count = 0;
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "PowerTelemetry.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "main.h"
|
||||
#include "power.h"
|
||||
@@ -22,6 +23,8 @@
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
static constexpr uint16_t TX_HISTORY_KEY_POWER_TELEMETRY = 0x8005;
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert,
|
||||
@@ -88,10 +91,12 @@ int32_t PowerTelemetryModule::runOnce()
|
||||
if (!moduleConfig.telemetry.power_measurement_enabled)
|
||||
return disable();
|
||||
|
||||
if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, sendToMeshIntervalMs)) &&
|
||||
uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_POWER_TELEMETRY) : 0;
|
||||
if (((lastTelemetry == 0) || !Throttle::isWithinTimespanMs(lastTelemetry, sendToMeshIntervalMs)) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
sendTelemetry();
|
||||
lastSentToMesh = millis();
|
||||
if (transmitHistory)
|
||||
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_POWER_TELEMETRY);
|
||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||
(service->isToPhoneQueueEmpty())) {
|
||||
// Just send to phone when it's not our time to send to mesh yet
|
||||
@@ -217,6 +222,10 @@ bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m)
|
||||
meshtastic_MeshPacket *PowerTelemetryModule::allocReply()
|
||||
{
|
||||
if (currentRequest) {
|
||||
if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) {
|
||||
ignoreRequest = true;
|
||||
return NULL;
|
||||
}
|
||||
auto req = *currentRequest;
|
||||
const auto &p = req.decoded;
|
||||
meshtastic_Telemetry scratch;
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "BaseTelemetryModule.h"
|
||||
#include "NodeDB.h"
|
||||
#include "ProtobufModule.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
|
||||
class PowerTelemetryModule : private concurrency::OSThread,
|
||||
public BaseTelemetryModule,
|
||||
public ProtobufModule<meshtastic_Telemetry>
|
||||
{
|
||||
CallbackObserver<PowerTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<PowerTelemetryModule, const meshtastic::Status *>(this, &PowerTelemetryModule::handleStatusUpdate);
|
||||
@@ -51,7 +54,6 @@ class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModul
|
||||
bool firstTime = 1;
|
||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||
uint32_t lastSentToMesh = 0;
|
||||
uint32_t lastSentToPhone = 0;
|
||||
uint32_t sensor_read_error_count = 0;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,26 +21,29 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
_bus = bus;
|
||||
_address = dev->address.address;
|
||||
|
||||
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
|
||||
if (!currentClock) {
|
||||
LOG_WARN("PMSA003I can't be used at this clock speed");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#ifdef PMSA003I_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(PMSA003I_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 /* PMSA003I_I2C_CLOCK_SPEED */
|
||||
|
||||
_bus->beginTransmission(_address);
|
||||
if (_bus->endTransmission() != 0) {
|
||||
LOG_WARN("PMSA003I not found on I2C at 0x12");
|
||||
LOG_WARN("%s not found on I2C at 0x12", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus);
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
status = 1;
|
||||
LOG_INFO("PMSA003I Enabled");
|
||||
LOG_INFO("%s Enabled", sensorName);
|
||||
|
||||
initI2CSensor();
|
||||
return true;
|
||||
@@ -49,34 +52,41 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
if (!isActive()) {
|
||||
LOG_WARN("PMSA003I is not active");
|
||||
LOG_WARN("Can't get metrics. %s is not active", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
|
||||
#endif
|
||||
#ifdef PMSA003I_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(PMSA003I_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 /* 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("PMSA003I read failed: incomplete data (%d bytes)", _bus->available());
|
||||
LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available());
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus);
|
||||
#endif
|
||||
|
||||
for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) {
|
||||
buffer[i] = _bus->read();
|
||||
}
|
||||
|
||||
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
if (buffer[0] != 0x42 || buffer[1] != 0x4D) {
|
||||
LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]);
|
||||
LOG_WARN("%s frame header invalid: 0x%02X 0x%02X", sensorName, buffer[0], buffer[1]);
|
||||
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;
|
||||
|
||||
@@ -86,7 +96,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2);
|
||||
|
||||
if (computedChecksum != receivedChecksum) {
|
||||
LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum);
|
||||
LOG_WARN("%s checksum failed: computed 0x%04X, received 0x%04X", sensorName, computedChecksum, receivedChecksum);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -128,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;
|
||||
}
|
||||
|
||||
@@ -136,20 +150,58 @@ bool PMSA003ISensor::isActive()
|
||||
return state == State::ACTIVE;
|
||||
}
|
||||
|
||||
int32_t PMSA003ISensor::wakeUpTimeMs()
|
||||
{
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
return PMSA003I_WARMUP_MS;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t PMSA003ISensor::pendingForReadyMs()
|
||||
{
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
|
||||
uint32_t now;
|
||||
now = getTime();
|
||||
uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000;
|
||||
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePmMeasureStarted);
|
||||
|
||||
if (sincePmMeasureStarted < PMSA003I_WARMUP_MS) {
|
||||
LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
|
||||
return PMSA003I_WARMUP_MS - sincePmMeasureStarted;
|
||||
}
|
||||
return 0;
|
||||
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool PMSA003ISensor::canSleep()
|
||||
{
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
return true;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
void PMSA003ISensor::sleep()
|
||||
{
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
|
||||
state = State::IDLE;
|
||||
pmMeasureStarted = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t PMSA003ISensor::wakeUp()
|
||||
{
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
LOG_INFO("Waking up PMSA003I");
|
||||
LOG_INFO("Waking up %s", sensorName);
|
||||
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
|
||||
state = State::ACTIVE;
|
||||
pmMeasureStarted = getTime();
|
||||
|
||||
return PMSA003I_WARMUP_MS;
|
||||
#endif
|
||||
// No need to wait for warmup if already active
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "RTC.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
#define PMSA003I_I2C_CLOCK_SPEED 100000
|
||||
@@ -19,6 +20,9 @@ class PMSA003ISensor : public TelemetrySensor
|
||||
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;
|
||||
|
||||
private:
|
||||
enum class State { IDLE, ACTIVE };
|
||||
@@ -26,6 +30,7 @@ class PMSA003ISensor : public TelemetrySensor
|
||||
|
||||
uint16_t computedChecksum = 0;
|
||||
uint16_t receivedChecksum = 0;
|
||||
uint32_t pmMeasureStarted = 0;
|
||||
|
||||
uint8_t buffer[PMSA003I_FRAME_LENGTH]{};
|
||||
TwoWire *_bus{};
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "RAK12035Sensor.h"
|
||||
|
||||
// The RAK12035 library's sensor_sleep() sets WB_IO2 (GPIO 34) LOW, which controls
|
||||
// the 3.3V switched power rail (PIN_3V3_EN). This turns off power to ALL peripherals
|
||||
// including GPS. We need to restore power after the library turns it off.
|
||||
#ifdef PIN_3V3_EN
|
||||
#define RESTORE_3V3_POWER() digitalWrite(PIN_3V3_EN, HIGH)
|
||||
#else
|
||||
#define RESTORE_3V3_POWER()
|
||||
#endif
|
||||
|
||||
RAK12035Sensor::RAK12035Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RAK12035, "RAK12035") {}
|
||||
|
||||
bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
@@ -13,16 +22,15 @@ bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
delay(100);
|
||||
sensor.begin(dev->address.address);
|
||||
|
||||
// Get sensor firmware version
|
||||
uint8_t data = 0;
|
||||
sensor.get_sensor_version(&data);
|
||||
if (data != 0) {
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
LOG_INFO("RAK12035Sensor Init Succeed \nSensor1 Firmware version: %i, Sensor Name: %s", data, sensorName);
|
||||
LOG_INFO("RAK12035Sensor Init Succeed \nSensor Firmware version: %i, Sensor Name: %s", data, sensorName);
|
||||
status = true;
|
||||
sensor.sensor_sleep();
|
||||
RESTORE_3V3_POWER();
|
||||
} else {
|
||||
// If we reach here, it means the sensor did not initialize correctly.
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
LOG_ERROR("RAK12035Sensor Init Failed");
|
||||
status = false;
|
||||
@@ -38,39 +46,44 @@ bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
|
||||
void RAK12035Sensor::setup()
|
||||
{
|
||||
// Set the calibration values
|
||||
// Reading the saved calibration values from the sensor.
|
||||
// TODO:: Check for and run calibration check for up to 2 additional sensors if present.
|
||||
uint16_t zero_val = 0;
|
||||
uint16_t hundred_val = 0;
|
||||
uint16_t default_zero_val = 550;
|
||||
uint16_t default_hundred_val = 420;
|
||||
const uint16_t default_zero_val = 510;
|
||||
const uint16_t default_hundred_val = 390;
|
||||
|
||||
sensor.sensor_on();
|
||||
sensor.begin();
|
||||
delay(200);
|
||||
sensor.get_dry_cal(&zero_val);
|
||||
delay(200);
|
||||
sensor.get_wet_cal(&hundred_val);
|
||||
delay(200);
|
||||
if (zero_val == 0 || zero_val <= hundred_val) {
|
||||
LOG_INFO("Dry calibration value is %d", zero_val);
|
||||
LOG_INFO("Wet calibration value is %d", hundred_val);
|
||||
LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: "
|
||||
"https://github.com/RAKWireless/RAK12035_SoilMoisture.");
|
||||
LOG_INFO("For now, setting default calibration value for Dry Calibration: %d", default_zero_val);
|
||||
|
||||
bool calibrationReset = false;
|
||||
|
||||
if (zero_val == 0) {
|
||||
LOG_INFO("Dry calibration not set, using default: %d", default_zero_val);
|
||||
sensor.set_dry_cal(default_zero_val);
|
||||
sensor.get_dry_cal(&zero_val);
|
||||
LOG_INFO("Dry calibration reset complete. New value is %d", zero_val);
|
||||
delay(200);
|
||||
zero_val = default_zero_val;
|
||||
calibrationReset = true;
|
||||
}
|
||||
if (hundred_val == 0 || hundred_val >= zero_val) {
|
||||
LOG_INFO("Dry calibration value is %d", zero_val);
|
||||
LOG_INFO("Wet calibration value is %d", hundred_val);
|
||||
LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: "
|
||||
"https://github.com/RAKWireless/RAK12035_SoilMoisture.");
|
||||
LOG_INFO("For now, setting default calibration value for Wet Calibration: %d", default_hundred_val);
|
||||
LOG_INFO("Wet calibration not set, using default: %d", default_hundred_val);
|
||||
sensor.set_wet_cal(default_hundred_val);
|
||||
sensor.get_wet_cal(&hundred_val);
|
||||
LOG_INFO("Wet calibration reset complete. New value is %d", hundred_val);
|
||||
delay(200);
|
||||
hundred_val = default_hundred_val;
|
||||
calibrationReset = true;
|
||||
}
|
||||
if (calibrationReset) {
|
||||
LOG_INFO("Default calibration values applied. Consider running the calibration sketch for better accuracy: "
|
||||
"https://github.com/RAKWireless/RAK12035_SoilMoisture");
|
||||
}
|
||||
|
||||
LOG_INFO("Dry calibration value: %d, Wet calibration value: %d", zero_val, hundred_val);
|
||||
sensor.sensor_sleep();
|
||||
RESTORE_3V3_POWER();
|
||||
delay(200);
|
||||
LOG_INFO("Dry calibration value is %d", zero_val);
|
||||
LOG_INFO("Wet calibration value is %d", hundred_val);
|
||||
@@ -79,10 +92,6 @@ void RAK12035Sensor::setup()
|
||||
bool RAK12035Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
// TODO:: read and send metrics for up to 2 additional soil monitors if present.
|
||||
// -- how to do this.. this could get a little complex..
|
||||
// ie - 1> we combine them into an average and send that, 2> we send them as separate metrics
|
||||
// ^-- these scenarios would require different handling of the metrics in the receiving end and maybe a setting in the
|
||||
// device ui and an additional proto for that?
|
||||
measurement->variant.environment_metrics.has_soil_temperature = true;
|
||||
measurement->variant.environment_metrics.has_soil_moisture = true;
|
||||
|
||||
@@ -97,6 +106,7 @@ bool RAK12035Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
success &= sensor.get_sensor_temperature(&temp);
|
||||
delay(200);
|
||||
sensor.sensor_sleep();
|
||||
RESTORE_3V3_POWER();
|
||||
|
||||
if (success == false) {
|
||||
LOG_ERROR("Failed to read sensor data");
|
||||
|
||||
@@ -18,6 +18,7 @@ class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
|
||||
|
||||
public:
|
||||
RAK9154Sensor();
|
||||
bool hasSensor() { return true; } // Not an I2C sensor; always available when HAS_RAKPROT is defined
|
||||
virtual int32_t runOnce() override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual uint16_t getBusVoltageMv() override;
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,917 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cScd4x.h>)
|
||||
|
||||
#include "../detect/reClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "SCD4XSensor.h"
|
||||
|
||||
#define SCD4X_NO_ERROR 0
|
||||
|
||||
SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {}
|
||||
|
||||
bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
|
||||
_bus = bus;
|
||||
_address = dev->address.address;
|
||||
|
||||
#ifdef SCD4X_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return false;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* SCD4X_I2C_CLOCK_SPEED */
|
||||
|
||||
scd4x.begin(*_bus, _address);
|
||||
|
||||
// From SCD4X library
|
||||
delay(30);
|
||||
|
||||
// Stop periodic measurement
|
||||
if (!stopMeasurement()) {
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get sensor variant
|
||||
scd4x.getSensorVariant(sensorVariant);
|
||||
|
||||
if (sensorVariant == SCD4X_SENSOR_VARIANT_SCD41) {
|
||||
LOG_INFO("%s: Found SCD41", sensorName);
|
||||
if (!powerUp()) {
|
||||
LOG_ERROR("%s: Error trying to execute powerUp()", sensorName);
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!getASC(ascActive)) {
|
||||
LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName);
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start measurement in selected power mode (low power by default)
|
||||
if (!startMeasurement()) {
|
||||
LOG_ERROR("%s: Couldn't start measurement", sensorName);
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
if (state == SCD4X_MEASUREMENT) {
|
||||
status = 1;
|
||||
} else {
|
||||
status = 0;
|
||||
}
|
||||
|
||||
initI2CSensor();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
|
||||
if (state != SCD4X_MEASUREMENT) {
|
||||
LOG_ERROR("%s: Not in measurement mode", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t co2, error;
|
||||
float temperature, humidity;
|
||||
|
||||
#ifdef SCD4X_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return false;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* SCD4X_I2C_CLOCK_SPEED */
|
||||
|
||||
bool dataReady;
|
||||
error = scd4x.getDataReadyStatus(dataReady);
|
||||
if (error != SCD4X_NO_ERROR || !dataReady) {
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
LOG_ERROR("SCD4X: Data is not ready");
|
||||
return false;
|
||||
}
|
||||
|
||||
error = scd4x.readMeasurement(co2, temperature, humidity);
|
||||
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum%.2f", sensorName, co2, temperature, humidity);
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_DEBUG("%s: Error while getting measurements: %u", sensorName, error);
|
||||
if (co2 == 0) {
|
||||
LOG_ERROR("%s: Skipping invalid measurement.", sensorName);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
measurement->variant.air_quality_metrics.has_co2_temperature = true;
|
||||
measurement->variant.air_quality_metrics.has_co2_humidity = true;
|
||||
measurement->variant.air_quality_metrics.has_co2 = true;
|
||||
measurement->variant.air_quality_metrics.co2_temperature = temperature;
|
||||
measurement->variant.air_quality_metrics.co2_humidity = humidity;
|
||||
measurement->variant.air_quality_metrics.co2 = co2;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Perform a forced recalibration (FRC) of the CO₂ concentration.
|
||||
*
|
||||
* From Sensirion SCD4X I2C Library
|
||||
*
|
||||
* 1. Operate the SCD4x in the operation mode later used for normal sensor
|
||||
* operation (e.g. periodic measurement) for at least 3 minutes in an
|
||||
* environment with a homogenous and constant CO2 concentration. The sensor
|
||||
* must be operated at the voltage desired for the application when
|
||||
* performing the FRC sequence. 2. Issue the stop_periodic_measurement
|
||||
* command. 3. Issue the perform_forced_recalibration command.
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::performFRC(uint32_t targetCO2)
|
||||
{
|
||||
uint16_t error, frcCorr;
|
||||
|
||||
LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName);
|
||||
|
||||
if (!stopMeasurement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2);
|
||||
error = scd4x.performForcedRecalibration((uint16_t)targetCO2, frcCorr);
|
||||
|
||||
// SCD4X Sensirion datasheet
|
||||
delay(400);
|
||||
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frcCorr == 0xFFFF) {
|
||||
LOG_ERROR("%s: Error while performing forced recalibration.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s: FRC Correction successful. Correction output: %u", sensorName, (uint16_t)(frcCorr - 0x8000));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start measurement mode
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::startMeasurement()
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
if (state == SCD4X_MEASUREMENT) {
|
||||
LOG_DEBUG("%s: Already in measurement mode", sensorName);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lowPower) {
|
||||
error = scd4x.startLowPowerPeriodicMeasurement();
|
||||
} else {
|
||||
error = scd4x.startPeriodicMeasurement();
|
||||
}
|
||||
|
||||
if (error == SCD4X_NO_ERROR) {
|
||||
LOG_INFO("%s: Started measurement mode", sensorName);
|
||||
if (lowPower) {
|
||||
LOG_INFO("%s: Low power mode", sensorName);
|
||||
} else {
|
||||
LOG_INFO("%s: Normal power mode", sensorName);
|
||||
}
|
||||
|
||||
state = SCD4X_MEASUREMENT;
|
||||
return true;
|
||||
} else {
|
||||
LOG_ERROR("%s: Unable to start measurement mode", sensorName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop measurement mode
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::stopMeasurement()
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
error = scd4x.stopPeriodicMeasurement();
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to stop measurement.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
state = SCD4X_IDLE;
|
||||
co2MeasureStarted = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set power mode
|
||||
* Pass true to set low power mode
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::setPowerMode(bool _lowPower)
|
||||
{
|
||||
lowPower = _lowPower;
|
||||
|
||||
if (!stopMeasurement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lowPower) {
|
||||
LOG_DEBUG("%s: Set low power mode", sensorName);
|
||||
} else {
|
||||
LOG_DEBUG("%s: Set normal power mode", sensorName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check the current mode (ASC or FRC)
|
||||
* From Sensirion SCD4X I2C Library
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::getASC(uint16_t &_ascActive)
|
||||
{
|
||||
uint16_t error;
|
||||
LOG_INFO("%s: Getting ASC", sensorName);
|
||||
|
||||
if (!stopMeasurement()) {
|
||||
return false;
|
||||
}
|
||||
error = scd4x.getAutomaticSelfCalibrationEnabled(_ascActive);
|
||||
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to send command.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s ASC is %s", sensorName, _ascActive ? "enabled" : "disabled");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Enable or disable automatic self calibration (ASC).
|
||||
*
|
||||
* From Sensirion SCD4X I2C Library
|
||||
*
|
||||
* Sets the current state (enabled / disabled) of the ASC. By default, ASC
|
||||
* is enabled.
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::setASC(bool ascEnabled)
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
LOG_INFO("%s %s ASC", sensorName, ascEnabled ? "Enabling" : "Disabling");
|
||||
|
||||
if (!stopMeasurement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
error = scd4x.setAutomaticSelfCalibrationEnabled((uint16_t)ascEnabled);
|
||||
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to send command.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
error = scd4x.persistSettings();
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to make settings persistent.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getASC(ascActive)) {
|
||||
LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the value of ASC baseline target in ppm.
|
||||
*
|
||||
* From Sensirion SCD4X I2C Library.
|
||||
*
|
||||
* Sets the value of the ASC baseline target, i.e. the CO₂ concentration in
|
||||
* ppm which the ASC algorithm will assume as lower-bound background to
|
||||
* which the SCD4x is exposed to regularly within one ASC period of
|
||||
* operation. To save the setting to the EEPROM, the persist_settings
|
||||
* command must be issued subsequently. The factory default value is 400
|
||||
* ppm.
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::setASCBaseline(uint32_t targetCO2)
|
||||
{
|
||||
// Available in library, but not described in datasheet.
|
||||
uint16_t error;
|
||||
LOG_INFO("%s: Setting ASC baseline to: %u", sensorName, targetCO2);
|
||||
|
||||
getASC(ascActive);
|
||||
if (!ascActive) {
|
||||
LOG_ERROR("%s: Can't set ASC baseline. ASC is not active", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!stopMeasurement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
error = scd4x.setAutomaticSelfCalibrationTarget((uint16_t)targetCO2);
|
||||
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to send command.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
error = scd4x.persistSettings();
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to make settings persistent.", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s: Setting ASC baseline successful", sensorName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the temperature compensation reference.
|
||||
*
|
||||
* From Sensirion SCD4X I2C Library.
|
||||
*
|
||||
* Setting the temperature offset of the SCD4x inside the customer device
|
||||
* allows the user to optimize the RH and T output signal.
|
||||
* By default, the temperature offset is set to 4 °C. To save
|
||||
* the setting to the EEPROM, the persist_settings command may be issued.
|
||||
* Equation (1) details how the characteristic temperature offset can be
|
||||
* calculated using the current temperature output of the sensor (TSCD4x), a
|
||||
* reference temperature value (TReference), and the previous temperature
|
||||
* offset (Toffset_pervious) obtained using the get_temperature_offset_raw
|
||||
* command:
|
||||
*
|
||||
* Toffset_actual = TSCD4x - TReference + Toffset_pervious.
|
||||
*
|
||||
* Recommended temperature offset values are between 0 °C and 20 °C. The
|
||||
* temperature offset does not impact the accuracy of the CO2 output.
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::setTemperature(float tempReference)
|
||||
{
|
||||
uint16_t error;
|
||||
float prevTempOffset;
|
||||
float updatedTempOffset;
|
||||
float tempOffset;
|
||||
bool dataReady;
|
||||
uint16_t co2;
|
||||
float temperature;
|
||||
float humidity;
|
||||
|
||||
LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference);
|
||||
|
||||
error = scd4x.getDataReadyStatus(dataReady);
|
||||
if (error != SCD4X_NO_ERROR || !dataReady) {
|
||||
LOG_ERROR("%s: Data is not ready", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
error = scd4x.readMeasurement(co2, temperature, humidity);
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature);
|
||||
|
||||
if (!stopMeasurement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
error = scd4x.getTemperatureOffset(prevTempOffset);
|
||||
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to get temperature offset. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("%s: Current sensor temperature offset: %.2f", sensorName, prevTempOffset);
|
||||
|
||||
tempOffset = temperature - tempReference + prevTempOffset;
|
||||
|
||||
LOG_INFO("%s: Setting temperature offset: %.2f", sensorName, tempOffset);
|
||||
error = scd4x.setTemperatureOffset(tempOffset);
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
error = scd4x.persistSettings();
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
scd4x.getTemperatureOffset(updatedTempOffset);
|
||||
LOG_INFO("%s: Updated sensor temperature offset: %.2f", sensorName, updatedTempOffset);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the sensor altitude.
|
||||
*
|
||||
* From Sensirion SCD4X I2C Library.
|
||||
*
|
||||
* Altitude in meters above sea level can be set after device installation.
|
||||
* Valid value between 0 and 3000m. This overrides pressure offset.
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::getAltitude(uint16_t &altitude)
|
||||
{
|
||||
uint16_t error;
|
||||
LOG_INFO("%s: Requesting sensor altitude", sensorName);
|
||||
|
||||
if (!stopMeasurement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
error = scd4x.getSensorAltitude(altitude);
|
||||
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the ambient pressure around the sensor.
|
||||
*
|
||||
* From Sensirion SCD4X I2C Library.
|
||||
*
|
||||
* Gets the ambient pressure in Pa.
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure)
|
||||
{
|
||||
uint16_t error;
|
||||
LOG_INFO("%s: Requesting sensor ambient pressure", sensorName);
|
||||
|
||||
error = scd4x.getAmbientPressure(ambientPressure);
|
||||
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("%s: Sensor ambient pressure: %u", sensorName, ambientPressure);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the sensor altitude.
|
||||
*
|
||||
* From Sensirion SCD4X I2C Library.
|
||||
*
|
||||
* Altitude in meters above sea level can be set after device installation.
|
||||
* Valid value between 0 and 3000m. This overrides pressure offset.
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::setAltitude(uint32_t altitude)
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
if (!stopMeasurement()) {
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("%s: setting altitude at %um", sensorName, altitude);
|
||||
|
||||
error = scd4x.setSensorAltitude(altitude);
|
||||
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// NOTE: this gives an error if issued. Sensirion's library
|
||||
// doesn't indicate it's needed.
|
||||
// error = scd4x.persistSettings();
|
||||
// if (error != SCD4X_NO_ERROR) {
|
||||
// LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
LOG_INFO("%s: altitude set", sensorName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the ambient pressure around the sensor.
|
||||
*
|
||||
* From Sensirion SCD4X I2C Library.
|
||||
*
|
||||
* The set_ambient_pressure command can be sent during periodic measurements
|
||||
* to enable continuous pressure compensation. Note that setting an ambient
|
||||
* pressure overrides any pressure compensation based on a previously set
|
||||
* sensor altitude. Use of this command is highly recommended for
|
||||
* applications experiencing significant ambient pressure changes to ensure
|
||||
* sensor accuracy. Valid input values are between 70000 - 120000 Pa. The
|
||||
* default value is 101300 Pa.
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure)
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
LOG_INFO("%s: setting ambient pressure at %u Pa", sensorName, ambientPressure);
|
||||
|
||||
error = scd4x.setAmbientPressure(ambientPressure);
|
||||
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sensirion doesn't indicate if this is necessary. We send it anyway
|
||||
error = scd4x.persistSettings();
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s: ambient pressure set set", sensorName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Perform factory reset to erase the settings stored in the EEPROM.
|
||||
*
|
||||
* From Sensirion SCD4X I2C Library.
|
||||
*
|
||||
* The perform_factory_reset command resets all configuration settings
|
||||
* stored in the EEPROM and erases the FRC and ASC algorithm history.
|
||||
* @note This function should not change the clock
|
||||
*/
|
||||
bool SCD4XSensor::factoryReset()
|
||||
{
|
||||
uint16_t error;
|
||||
|
||||
LOG_INFO("%s: Requesting factory reset", sensorName);
|
||||
|
||||
if (!stopMeasurement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
error = scd4x.performFactoryReset();
|
||||
|
||||
if (error != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Unable to do factory reset. Error code: %u", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s: Factory reset successful", sensorName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Put the sensor into sleep mode from idle mode.
|
||||
*
|
||||
* From Sensirion SCD4X I2C Library.
|
||||
*
|
||||
* Put the sensor from idle to sleep to reduce power consumption. Can be
|
||||
* used to power down when operating the sensor in power-cycled single shot
|
||||
* mode.
|
||||
* @note This command is only available in idle mode. Only for SCD41.
|
||||
*/
|
||||
bool SCD4XSensor::powerDown()
|
||||
{
|
||||
LOG_INFO("%s: Trying to send sensor to sleep", sensorName);
|
||||
|
||||
if (sensorVariant != SCD4X_SENSOR_VARIANT_SCD41) {
|
||||
LOG_WARN("SCD4X: Can't send sensor to sleep. Incorrect variant. Ignoring");
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef SCD4X_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return false;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* SCD4X_I2C_CLOCK_SPEED */
|
||||
|
||||
if (!stopMeasurement()) {
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scd4x.powerDown() != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Error trying to execute sleep()", sensorName);
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
state = SCD4X_OFF;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wake up sensor from sleep mode to idle mode (powerUp)
|
||||
*
|
||||
* From Sensirion SCD4X I2C Library.
|
||||
*
|
||||
* Wake up the sensor from sleep mode into idle mode. Note that the SCD4x
|
||||
* does not acknowledge the wake_up command. The sensor's idle state after
|
||||
* wake up can be verified by reading out the serial number.
|
||||
* @note This command is only available for SCD41.
|
||||
* @note This function can't change clock (used in init)
|
||||
*/
|
||||
bool SCD4XSensor::powerUp()
|
||||
{
|
||||
LOG_INFO("%s: Waking up", sensorName);
|
||||
|
||||
if (scd4x.wakeUp() != SCD4X_NO_ERROR) {
|
||||
LOG_ERROR("%s: Error trying to execute wakeUp()", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
state = SCD4X_IDLE;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sensor is in measurement mode
|
||||
*/
|
||||
bool SCD4XSensor::isActive()
|
||||
{
|
||||
return state == SCD4X_MEASUREMENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start measurement mode
|
||||
* @note Not used in admin comands, getMetrics or init, can change clock.
|
||||
*/
|
||||
uint32_t SCD4XSensor::wakeUp()
|
||||
{
|
||||
|
||||
#ifdef SCD4X_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return 0;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* SCD4X_I2C_CLOCK_SPEED */
|
||||
|
||||
if (startMeasurement()) {
|
||||
co2MeasureStarted = getTime();
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return SCD4X_WARMUP_MS;
|
||||
}
|
||||
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop measurement mode
|
||||
* @note Not used in admin comands, getMetrics or init, can change clock.
|
||||
*/
|
||||
void SCD4XSensor::sleep()
|
||||
{
|
||||
#ifdef SCD4X_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* SCD4X_I2C_CLOCK_SPEED */
|
||||
|
||||
stopMeasurement();
|
||||
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Can sleep function
|
||||
*
|
||||
* Power consumption is very low on lowPower mode, modify this function if
|
||||
* you still want to override this behaviour. Otherwise, sleep is disabled
|
||||
* routinely in low power mode
|
||||
*/
|
||||
bool SCD4XSensor::canSleep()
|
||||
{
|
||||
return lowPower ? false : true;
|
||||
}
|
||||
|
||||
int32_t SCD4XSensor::wakeUpTimeMs()
|
||||
{
|
||||
return SCD4X_WARMUP_MS;
|
||||
}
|
||||
|
||||
int32_t SCD4XSensor::pendingForReadyMs()
|
||||
{
|
||||
uint32_t now;
|
||||
now = getTime();
|
||||
uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted) * 1000;
|
||||
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceCO2MeasureStarted);
|
||||
|
||||
if (sinceCO2MeasureStarted < SCD4X_WARMUP_MS) {
|
||||
LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
|
||||
return SCD4X_WARMUP_MS - sinceCO2MeasureStarted;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response)
|
||||
{
|
||||
AdminMessageHandleResult result;
|
||||
|
||||
#ifdef SCD4X_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return AdminMessageHandleResult::NOT_HANDLED;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* SCD4X_I2C_CLOCK_SPEED */
|
||||
|
||||
// TODO: potentially add selftest command?
|
||||
switch (request->which_payload_variant) {
|
||||
case meshtastic_AdminMessage_sensor_config_tag:
|
||||
// Check for ASC-FRC request first
|
||||
if (!request->sensor_config.has_scd4x_config) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
|
||||
if (request->sensor_config.scd4x_config.has_factory_reset) {
|
||||
LOG_DEBUG("%s: Requested factory reset", sensorName);
|
||||
if (!this->factoryReset()) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (request->sensor_config.scd4x_config.has_set_asc) {
|
||||
getASC(ascActive);
|
||||
bool currentASC = ascActive;
|
||||
if (request->sensor_config.scd4x_config.set_asc == false) {
|
||||
LOG_DEBUG("%s: Request for FRC", sensorName);
|
||||
if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
|
||||
if (this->setASC(request->sensor_config.scd4x_config.set_asc)) {
|
||||
if (!this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
// Set it back to ASC if failed
|
||||
setASC(currentASC);
|
||||
break;
|
||||
};
|
||||
} else {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// FRC requested but no target CO2 provided
|
||||
LOG_ERROR("%s: target CO2 not provided", sensorName);
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("%s: Request for ASC", sensorName);
|
||||
if (this->setASC(request->sensor_config.scd4x_config.set_asc)) {
|
||||
if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
|
||||
LOG_DEBUG("%s: Request has target CO2", sensorName);
|
||||
this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc);
|
||||
// NOTE - in this situation, if we set ASC, but baseline set fails, we stay on ASC
|
||||
} else {
|
||||
LOG_DEBUG("%s: Request doesn't have target CO2", sensorName);
|
||||
}
|
||||
} else {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for temperature offset
|
||||
// NOTE: this requires to have a sensor working on stable environment
|
||||
// And to make it between readings
|
||||
if (request->sensor_config.scd4x_config.has_set_temperature) {
|
||||
if (!this->setTemperature(request->sensor_config.scd4x_config.set_temperature)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for altitude or pressure offset
|
||||
if (request->sensor_config.scd4x_config.has_set_altitude) {
|
||||
if (!this->setAltitude(request->sensor_config.scd4x_config.set_altitude)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
} else if (request->sensor_config.scd4x_config.has_set_ambient_pressure) {
|
||||
if (!this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for low power mode
|
||||
// NOTE: to switch from one mode to another do:
|
||||
// setPowerMode -> startMeasurement
|
||||
if (request->sensor_config.scd4x_config.has_set_power_mode) {
|
||||
if (!this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = AdminMessageHandleResult::HANDLED;
|
||||
break;
|
||||
|
||||
default:
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
}
|
||||
|
||||
// Start measurement mode
|
||||
this->startMeasurement();
|
||||
|
||||
#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cScd4x.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "RTC.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <SensirionI2cScd4x.h>
|
||||
|
||||
// Max speed 400kHz
|
||||
#define SCD4X_I2C_CLOCK_SPEED 100000
|
||||
#define SCD4X_WARMUP_MS 5000
|
||||
|
||||
class SCD4XSensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
SensirionI2cScd4x scd4x;
|
||||
TwoWire *_bus{};
|
||||
uint8_t _address{};
|
||||
|
||||
bool performFRC(uint32_t targetCO2);
|
||||
bool setASCBaseline(uint32_t targetCO2);
|
||||
bool getASC(uint16_t &ascEnabled);
|
||||
bool setASC(bool ascEnabled);
|
||||
bool setTemperature(float tempReference);
|
||||
bool getAltitude(uint16_t &altitude);
|
||||
bool setAltitude(uint32_t altitude);
|
||||
bool getAmbientPressure(uint32_t &ambientPressure);
|
||||
bool setAmbientPressure(uint32_t ambientPressure);
|
||||
bool factoryReset();
|
||||
bool setPowerMode(bool _lowPower);
|
||||
bool startMeasurement();
|
||||
bool stopMeasurement();
|
||||
|
||||
uint16_t ascActive = 1;
|
||||
// low power measurement mode (on sensirion side). Disables sleep mode
|
||||
// Improvement and testing needed for timings
|
||||
bool lowPower = true;
|
||||
uint32_t co2MeasureStarted = 0;
|
||||
|
||||
public:
|
||||
SCD4XSensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
|
||||
enum SCD4XState { SCD4X_OFF, SCD4X_IDLE, SCD4X_MEASUREMENT };
|
||||
SCD4XState state = SCD4X_OFF;
|
||||
SCD4xSensorVariant sensorVariant{};
|
||||
|
||||
virtual bool isActive() override;
|
||||
|
||||
virtual void sleep() override; // Stops measurement (measurement -> idle)
|
||||
virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement)
|
||||
bool powerDown(); // Powers down sensor (idle -> power-off)
|
||||
bool powerUp(); // Powers the sensor (power-off -> idle)
|
||||
virtual bool canSleep() override;
|
||||
virtual int32_t wakeUpTimeMs() override;
|
||||
virtual int32_t pendingForReadyMs() override;
|
||||
AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,969 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
|
||||
#include "../detect/reClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "FSCommon.h"
|
||||
#include "SEN5XSensor.h"
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <float.h> // FLT_MAX
|
||||
#include <pb_decode.h>
|
||||
#include <pb_encode.h>
|
||||
|
||||
SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {}
|
||||
|
||||
bool SEN5XSensor::getVersion()
|
||||
{
|
||||
if (!sendCommand(SEN5X_GET_FIRMWARE_VERSION)) {
|
||||
LOG_ERROR("SEN5X: Error sending version command");
|
||||
return false;
|
||||
}
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
uint8_t versionBuffer[12];
|
||||
size_t charNumber = readBuffer(&versionBuffer[0], 3);
|
||||
if (charNumber == 0) {
|
||||
LOG_ERROR("SEN5X: Error getting data ready flag value");
|
||||
return false;
|
||||
}
|
||||
|
||||
firmwareVer = versionBuffer[0] + (versionBuffer[1] / 10);
|
||||
hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10);
|
||||
protocolVer = versionBuffer[5] + (versionBuffer[6] / 10);
|
||||
|
||||
LOG_INFO("SEN5X Firmware Version: %0.2f", firmwareVer);
|
||||
LOG_INFO("SEN5X Hardware Version: %0.2f", hardwareVer);
|
||||
LOG_INFO("SEN5X Protocol Version: %0.2f", protocolVer);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SEN5XSensor::findModel()
|
||||
{
|
||||
if (!sendCommand(SEN5X_GET_PRODUCT_NAME)) {
|
||||
LOG_ERROR("SEN5X: Error asking for product name");
|
||||
return false;
|
||||
}
|
||||
delay(50); // From Sensirion Datasheet
|
||||
|
||||
const uint8_t nameSize = 48;
|
||||
uint8_t name[nameSize];
|
||||
size_t charNumber = readBuffer(&name[0], nameSize);
|
||||
if (charNumber == 0) {
|
||||
LOG_ERROR("SEN5X: Error getting device name");
|
||||
return false;
|
||||
}
|
||||
|
||||
// We only check the last character that defines the model SEN5X
|
||||
switch (name[4]) {
|
||||
case 48:
|
||||
model = SEN50;
|
||||
LOG_INFO("SEN5X: found sensor model SEN50");
|
||||
break;
|
||||
case 52:
|
||||
model = SEN54;
|
||||
LOG_INFO("SEN5X: found sensor model SEN54");
|
||||
break;
|
||||
case 53:
|
||||
model = SEN55;
|
||||
LOG_INFO("SEN5X: found sensor model SEN55");
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SEN5XSensor::sendCommand(uint16_t command)
|
||||
{
|
||||
uint8_t nothing;
|
||||
return sendCommand(command, ¬hing, 0);
|
||||
}
|
||||
|
||||
bool SEN5XSensor::sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber)
|
||||
{
|
||||
// At least we need two bytes for the command
|
||||
uint8_t bufferSize = 2;
|
||||
|
||||
// Add space for CRC bytes (one every two bytes)
|
||||
if (byteNumber > 0)
|
||||
bufferSize += byteNumber + (byteNumber / 2);
|
||||
|
||||
uint8_t toSend[bufferSize];
|
||||
uint8_t i = 0;
|
||||
toSend[i++] = static_cast<uint8_t>((command & 0xFF00) >> 8);
|
||||
toSend[i++] = static_cast<uint8_t>((command & 0x00FF) >> 0);
|
||||
|
||||
// Prepare buffer with CRC every third byte
|
||||
uint8_t bi = 0;
|
||||
if (byteNumber > 0) {
|
||||
while (bi < byteNumber) {
|
||||
toSend[i++] = buffer[bi++];
|
||||
toSend[i++] = buffer[bi++];
|
||||
uint8_t calcCRC = sen5xCRC(&buffer[bi - 2]);
|
||||
toSend[i++] = calcCRC;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SEN5X_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SEN5X_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 /* SEN5X_I2C_CLOCK_SPEED */
|
||||
|
||||
// Transmit the data
|
||||
// LOG_DEBUG("Beginning connection to SEN5X: 0x%x. Size: %u", address, bufferSize);
|
||||
// Note: this delay is necessary to allow for long-buffers
|
||||
delay(20);
|
||||
_bus->beginTransmission(_address);
|
||||
size_t writtenBytes = _bus->write(toSend, bufferSize);
|
||||
uint8_t i2c_error = _bus->endTransmission();
|
||||
|
||||
#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
if (writtenBytes != bufferSize) {
|
||||
LOG_ERROR("SEN5X: Error writting on I2C bus");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (i2c_error != 0) {
|
||||
LOG_ERROR("SEN5X: Error on I2C communication: %x", i2c_error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t SEN5XSensor::readBuffer(uint8_t *buffer, uint8_t byteNumber)
|
||||
{
|
||||
#ifdef SEN5X_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SEN5X_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 /* SEN5X_I2C_CLOCK_SPEED */
|
||||
|
||||
size_t readBytes = _bus->requestFrom(_address, byteNumber);
|
||||
if (readBytes != byteNumber) {
|
||||
LOG_ERROR("SEN5X: Error reading I2C bus");
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t i = 0;
|
||||
uint8_t receivedBytes = 0;
|
||||
while (readBytes > 0) {
|
||||
buffer[i++] = _bus->read(); // Just as a reminder: i++ returns i and after that increments.
|
||||
buffer[i++] = _bus->read();
|
||||
uint8_t recvCRC = _bus->read();
|
||||
uint8_t calcCRC = sen5xCRC(&buffer[i - 2]);
|
||||
if (recvCRC != calcCRC) {
|
||||
LOG_ERROR("SEN5X: Checksum error while receiving msg");
|
||||
return 0;
|
||||
}
|
||||
readBytes -= 3;
|
||||
receivedBytes += 2;
|
||||
}
|
||||
#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
return receivedBytes;
|
||||
}
|
||||
|
||||
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
|
||||
uint8_t crc = 0xff;
|
||||
|
||||
for (uint8_t i = 0; i < 2; i++) {
|
||||
|
||||
crc ^= buffer[i];
|
||||
|
||||
for (uint8_t bit = 8; bit > 0; bit--) {
|
||||
if (crc & 0x80)
|
||||
crc = (crc << 1) ^ 0x31;
|
||||
else
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
void SEN5XSensor::sleep()
|
||||
{
|
||||
idle(true);
|
||||
}
|
||||
|
||||
bool SEN5XSensor::idle(bool checkState)
|
||||
{
|
||||
// From the datasheet:
|
||||
// By default, the VOC algorithm resets its state to initial
|
||||
// values each time a measurement is started,
|
||||
// even if the measurement was stopped only for a short
|
||||
// time. So, the VOC index output value needs a long time
|
||||
// until it is stable again. This can be avoided by
|
||||
// restoring the previously memorized algorithm state before
|
||||
// starting the measure mode
|
||||
|
||||
if (checkState) {
|
||||
// If the stabilisation period is not passed for SEN54 or SEN55, don't go to idle
|
||||
if (model != SEN50) {
|
||||
// Get VOC state before going to idle mode
|
||||
vocValid = false;
|
||||
if (vocStateFromSensor()) {
|
||||
vocValid = vocStateValid();
|
||||
// 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) {
|
||||
vocTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
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("%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("%s: Error stopping measurement", sensorName);
|
||||
return false;
|
||||
}
|
||||
state = SEN5X_IDLE;
|
||||
LOG_INFO("%s: Stop measurement mode", sensorName);
|
||||
} else {
|
||||
if (!sendCommand(SEN5X_START_MEASUREMENT_RHT_GAS)) {
|
||||
LOG_ERROR("%s: Error switching to RHT/Gas measurement", sensorName);
|
||||
return false;
|
||||
}
|
||||
state = SEN5X_RHTGAS_ONLY;
|
||||
LOG_INFO("%s: Switch to RHT/Gas only measurement mode", sensorName);
|
||||
}
|
||||
|
||||
delay(200); // From Sensirion Datasheet
|
||||
pmMeasureStarted = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SEN5XSensor::vocStateRecent(uint32_t now)
|
||||
{
|
||||
if (now) {
|
||||
uint32_t passed = now - vocTime; // in seconds
|
||||
|
||||
// Check if state is recent, less than 10 minutes (600 seconds)
|
||||
if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SEN5XSensor::vocStateValid()
|
||||
{
|
||||
if (!vocState[0] && !vocState[1] && !vocState[2] && !vocState[3] && !vocState[4] && !vocState[5] && !vocState[6] &&
|
||||
!vocState[7]) {
|
||||
LOG_DEBUG("%s: VOC state is all 0, invalid", sensorName);
|
||||
return false;
|
||||
} else {
|
||||
LOG_DEBUG("%s: VOC state is valid", sensorName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool SEN5XSensor::vocStateToSensor()
|
||||
{
|
||||
if (model == SEN50) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!vocStateValid()) {
|
||||
LOG_INFO("SEN5X: VOC state is invalid, not sending");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!sendCommand(SEN5X_STOP_MEASUREMENT)) {
|
||||
LOG_ERROR("SEN5X: Error stoping measurement");
|
||||
return false;
|
||||
}
|
||||
delay(200); // From Sensirion Datasheet
|
||||
|
||||
LOG_DEBUG("SEN5X: Sending VOC state to sensor");
|
||||
LOG_DEBUG("[%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2], vocState[3], vocState[4], vocState[5],
|
||||
vocState[6], vocState[7]);
|
||||
|
||||
// Note: send command already takes into account the CRC
|
||||
// buffer size increment needed
|
||||
if (!sendCommand(SEN5X_RW_VOCS_STATE, vocState, SEN5X_VOC_STATE_BUFFER_SIZE)) {
|
||||
LOG_ERROR("SEN5X: Error sending VOC's state command'");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SEN5XSensor::vocStateFromSensor()
|
||||
{
|
||||
if (model == SEN50) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_INFO("SEN5X: Getting VOC state from sensor");
|
||||
// Ask VOCs state from the sensor
|
||||
if (!sendCommand(SEN5X_RW_VOCS_STATE)) {
|
||||
LOG_ERROR("SEN5X: Error sending VOC's state command'");
|
||||
return false;
|
||||
}
|
||||
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
// Retrieve the data
|
||||
// Allocate buffer to account for CRC
|
||||
size_t receivedNumber = readBuffer(&vocState[0], SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2));
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
if (receivedNumber == 0) {
|
||||
LOG_DEBUG("SEN5X: Error getting VOC's state");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Print the state (if debug is on)
|
||||
LOG_DEBUG("SEN5X: VOC state retrieved from sensor: [%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2],
|
||||
vocState[3], vocState[4], vocState[5], vocState[6], vocState[7]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SEN5XSensor::loadState()
|
||||
{
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
auto file = FSCom.open(sen5XStateFileName, FILE_O_READ);
|
||||
bool okay = false;
|
||||
if (file) {
|
||||
LOG_INFO("%s state read from %s", sensorName, sen5XStateFileName);
|
||||
pb_istream_t stream = {&readcb, &file, meshtastic_SEN5XState_size};
|
||||
|
||||
if (!pb_decode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) {
|
||||
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
|
||||
} else {
|
||||
lastCleaning = sen5xstate.last_cleaning_time;
|
||||
lastCleaningValid = sen5xstate.last_cleaning_valid;
|
||||
oneShotMode = sen5xstate.one_shot_mode;
|
||||
|
||||
if (model != SEN50) {
|
||||
vocTime = sen5xstate.voc_state_time;
|
||||
vocValid = sen5xstate.voc_state_valid;
|
||||
// Unpack state
|
||||
vocState[7] = (uint8_t)(sen5xstate.voc_state_array >> 56);
|
||||
vocState[6] = (uint8_t)(sen5xstate.voc_state_array >> 48);
|
||||
vocState[5] = (uint8_t)(sen5xstate.voc_state_array >> 40);
|
||||
vocState[4] = (uint8_t)(sen5xstate.voc_state_array >> 32);
|
||||
vocState[3] = (uint8_t)(sen5xstate.voc_state_array >> 24);
|
||||
vocState[2] = (uint8_t)(sen5xstate.voc_state_array >> 16);
|
||||
vocState[1] = (uint8_t)(sen5xstate.voc_state_array >> 8);
|
||||
vocState[0] = (uint8_t)sen5xstate.voc_state_array;
|
||||
}
|
||||
|
||||
// LOG_DEBUG("Loaded lastCleaning %u", lastCleaning);
|
||||
// LOG_DEBUG("Loaded lastCleaningValid %u", lastCleaningValid);
|
||||
// LOG_DEBUG("Loaded oneShotMode %s", oneShotMode ? "true" : "false");
|
||||
// LOG_DEBUG("Loaded vocTime %u", vocTime);
|
||||
// LOG_DEBUG("Loaded [%u, %u, %u, %u, %u, %u, %u, %u]",
|
||||
// vocState[7], vocState[6], vocState[5], vocState[4], vocState[3], vocState[2], vocState[1], vocState[0]);
|
||||
// LOG_DEBUG("Loaded %svalid VOC state", vocValid ? "" : "in");
|
||||
|
||||
okay = true;
|
||||
}
|
||||
file.close();
|
||||
} else {
|
||||
LOG_INFO("No %s state found (File: %s)", sensorName, sen5XStateFileName);
|
||||
}
|
||||
spiLock->unlock();
|
||||
return okay;
|
||||
#else
|
||||
LOG_ERROR("SEN5X: ERROR - Filesystem not implemented");
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SEN5XSensor::saveState()
|
||||
{
|
||||
#ifdef FSCom
|
||||
auto file = SafeFile(sen5XStateFileName);
|
||||
|
||||
sen5xstate.last_cleaning_time = lastCleaning;
|
||||
sen5xstate.last_cleaning_valid = lastCleaningValid;
|
||||
sen5xstate.one_shot_mode = oneShotMode;
|
||||
|
||||
if (model != SEN50) {
|
||||
sen5xstate.has_voc_state_time = true;
|
||||
sen5xstate.has_voc_state_valid = true;
|
||||
sen5xstate.has_voc_state_array = true;
|
||||
|
||||
sen5xstate.voc_state_time = vocTime;
|
||||
sen5xstate.voc_state_valid = vocValid;
|
||||
// Unpack state (8 bytes)
|
||||
sen5xstate.voc_state_array = (((uint64_t)vocState[7]) << 56) | ((uint64_t)vocState[6] << 48) |
|
||||
((uint64_t)vocState[5] << 40) | ((uint64_t)vocState[4] << 32) |
|
||||
((uint64_t)vocState[3] << 24) | ((uint64_t)vocState[2] << 16) |
|
||||
((uint64_t)vocState[1] << 8) | ((uint64_t)vocState[0]);
|
||||
}
|
||||
|
||||
bool okay = false;
|
||||
|
||||
LOG_INFO("%s: state write to %s", sensorName, sen5XStateFileName);
|
||||
pb_ostream_t stream = {&writecb, static_cast<Print *>(&file), meshtastic_SEN5XState_size};
|
||||
|
||||
if (!pb_encode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) {
|
||||
LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream));
|
||||
} else {
|
||||
okay = true;
|
||||
}
|
||||
|
||||
okay &= file.close();
|
||||
|
||||
if (okay)
|
||||
LOG_INFO("%s: state write to %s successful", sensorName, sen5XStateFileName);
|
||||
|
||||
return okay;
|
||||
#else
|
||||
LOG_ERROR("%s: ERROR - Filesystem not implemented", sensorName);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SEN5XSensor::isActive()
|
||||
{
|
||||
return state == SEN5X_MEASUREMENT || state == SEN5X_MEASUREMENT_2;
|
||||
}
|
||||
|
||||
uint32_t SEN5XSensor::wakeUp()
|
||||
{
|
||||
|
||||
LOG_DEBUG("SEN5X: Waking up sensor");
|
||||
|
||||
if (!sendCommand(SEN5X_START_MEASUREMENT)) {
|
||||
LOG_ERROR("SEN5X: Error starting measurement");
|
||||
// TODO - what should this return?? Something actually on the default interval?
|
||||
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||
}
|
||||
delay(50); // From Sensirion Datasheet
|
||||
|
||||
// TODO - This is currently "problematic"
|
||||
// If time is updated in between reads, there is no way to
|
||||
// keep track of how long it has passed
|
||||
pmMeasureStarted = getTime();
|
||||
state = SEN5X_MEASUREMENT;
|
||||
if (state == SEN5X_MEASUREMENT)
|
||||
LOG_INFO("SEN5X: Started measurement mode");
|
||||
return SEN5X_WARMUP_MS_1;
|
||||
}
|
||||
|
||||
bool SEN5XSensor::vocStateStable()
|
||||
{
|
||||
uint32_t now;
|
||||
now = getTime();
|
||||
uint32_t sinceFirstMeasureStarted = (now - rhtGasMeasureStarted);
|
||||
LOG_DEBUG("sinceFirstMeasureStarted: %us", sinceFirstMeasureStarted);
|
||||
return sinceFirstMeasureStarted > SEN5X_VOC_STATE_WARMUP_S;
|
||||
}
|
||||
|
||||
bool SEN5XSensor::startCleaning()
|
||||
{
|
||||
// Note: we only should enter here if we have a valid RTC with at least
|
||||
// RTCQuality::RTCQualityDevice
|
||||
state = SEN5X_CLEANING;
|
||||
|
||||
// Note that cleaning command can only be run when the sensor is in measurement mode
|
||||
if (!sendCommand(SEN5X_START_MEASUREMENT)) {
|
||||
LOG_ERROR("SEN5X: Error starting measurment mode");
|
||||
return false;
|
||||
}
|
||||
delay(50); // From Sensirion Datasheet
|
||||
|
||||
if (!sendCommand(SEN5X_START_FAN_CLEANING)) {
|
||||
LOG_ERROR("SEN5X: Error starting fan cleaning");
|
||||
return false;
|
||||
}
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
// This message will be always printed so the user knows the device it's not hung
|
||||
LOG_INFO("SEN5X: Started fan cleaning it will take 10 seconds...");
|
||||
|
||||
uint16_t started = millis();
|
||||
while (millis() - started < 10500) {
|
||||
delay(500);
|
||||
}
|
||||
LOG_INFO("SEN5X: Cleaning done!!");
|
||||
|
||||
// Save timestamp in flash so we know when a week has passed
|
||||
uint32_t now;
|
||||
now = getValidTime(RTCQuality::RTCQualityDevice);
|
||||
// If time is not RTCQualityNone, it will return non-zero
|
||||
lastCleaning = now;
|
||||
lastCleaningValid = true;
|
||||
saveState();
|
||||
|
||||
idle();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
state = SEN5X_NOT_DETECTED;
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
|
||||
_bus = bus;
|
||||
_address = dev->address.address;
|
||||
|
||||
delay(50); // without this there is an error on the deviceReset function
|
||||
|
||||
if (!sendCommand(SEN5X_RESET)) {
|
||||
LOG_ERROR("SEN5X: Error reseting device");
|
||||
return false;
|
||||
}
|
||||
delay(200); // From Sensirion Datasheet
|
||||
|
||||
if (!findModel()) {
|
||||
LOG_ERROR("SEN5X: error finding sensor model");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the firmware version
|
||||
if (!getVersion())
|
||||
return false;
|
||||
if (firmwareVer < 2) {
|
||||
LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation");
|
||||
return false;
|
||||
}
|
||||
delay(200); // From Sensirion Datasheet
|
||||
|
||||
// Detection succeeded
|
||||
state = SEN5X_IDLE;
|
||||
status = 1;
|
||||
|
||||
// Load state
|
||||
loadState();
|
||||
|
||||
// Check if it is time to do a cleaning
|
||||
uint32_t now;
|
||||
int32_t passed = 0;
|
||||
now = getValidTime(RTCQuality::RTCQualityDevice);
|
||||
|
||||
// If time is not RTCQualityNone, it will return non-zero
|
||||
if (now) {
|
||||
if (lastCleaningValid) {
|
||||
|
||||
passed = now - lastCleaning; // in seconds
|
||||
|
||||
if (passed > ONE_WEEK_IN_SECONDS && (now > SEN5X_VOC_VALID_DATE)) {
|
||||
// If current date greater than 01/01/2018 (validity check)
|
||||
LOG_INFO("SEN5X: More than a week (%us) since last cleaning in epoch (%us). Trigger, cleaning...", passed,
|
||||
lastCleaning);
|
||||
startCleaning();
|
||||
} else {
|
||||
LOG_INFO("SEN5X: Cleaning not needed (%ds passed). Last cleaning date (in epoch): %us", passed, lastCleaning);
|
||||
}
|
||||
} else {
|
||||
// We assume the device has just been updated or it is new,
|
||||
// so no need to trigger a cleaning.
|
||||
// Just save the timestamp to do a cleaning one week from now.
|
||||
// Otherwise, we will never trigger cleaning in some cases
|
||||
lastCleaning = now;
|
||||
lastCleaningValid = true;
|
||||
LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %us", lastCleaning);
|
||||
saveState();
|
||||
}
|
||||
|
||||
if (model != SEN50) {
|
||||
if (!vocValid) {
|
||||
LOG_INFO("SEN5X: No valid VOC's state found");
|
||||
} else {
|
||||
// Check if state is recent
|
||||
if (vocStateRecent(now)) {
|
||||
// If current date greater than 01/01/2018 (validity check)
|
||||
// Send it to the sensor
|
||||
LOG_INFO("SEN5X: VOC state is valid and recent");
|
||||
vocStateToSensor();
|
||||
} else {
|
||||
LOG_INFO("SEN5X: VOC state is too old or date is invalid");
|
||||
LOG_DEBUG("SEN5X: vocTime %u, Passed %u, and now %u", vocTime, passed, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO - Should this actually ignore? We could end up never cleaning...
|
||||
LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved cleaning and VOC state");
|
||||
}
|
||||
|
||||
idle(false);
|
||||
rhtGasMeasureStarted = now;
|
||||
|
||||
initI2CSensor();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SEN5XSensor::readValues()
|
||||
{
|
||||
if (!sendCommand(SEN5X_READ_VALUES)) {
|
||||
LOG_ERROR("SEN5X: Error sending read command");
|
||||
return false;
|
||||
}
|
||||
LOG_DEBUG("SEN5X: Reading PM Values");
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
uint8_t dataBuffer[16];
|
||||
size_t receivedNumber = readBuffer(&dataBuffer[0], 24);
|
||||
if (receivedNumber == 0) {
|
||||
LOG_ERROR("SEN5X: Error getting values");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the integers
|
||||
uint16_t uint_pM1p0 = static_cast<uint16_t>((dataBuffer[0] << 8) | dataBuffer[1]);
|
||||
uint16_t uint_pM2p5 = static_cast<uint16_t>((dataBuffer[2] << 8) | dataBuffer[3]);
|
||||
uint16_t uint_pM4p0 = static_cast<uint16_t>((dataBuffer[4] << 8) | dataBuffer[5]);
|
||||
uint16_t uint_pM10p0 = static_cast<uint16_t>((dataBuffer[6] << 8) | dataBuffer[7]);
|
||||
|
||||
int16_t int_humidity = static_cast<int16_t>((dataBuffer[8] << 8) | dataBuffer[9]);
|
||||
int16_t int_temperature = static_cast<int16_t>((dataBuffer[10] << 8) | dataBuffer[11]);
|
||||
int16_t int_vocIndex = static_cast<int16_t>((dataBuffer[12] << 8) | dataBuffer[13]);
|
||||
int16_t int_noxIndex = static_cast<int16_t>((dataBuffer[14] << 8) | dataBuffer[15]);
|
||||
|
||||
// Convert values based on Sensirion Arduino lib
|
||||
sen5xmeasurement.pM1p0 = !isnan(uint_pM1p0) ? uint_pM1p0 / 10 : UINT16_MAX;
|
||||
sen5xmeasurement.pM2p5 = !isnan(uint_pM2p5) ? uint_pM2p5 / 10 : UINT16_MAX;
|
||||
sen5xmeasurement.pM4p0 = !isnan(uint_pM4p0) ? uint_pM4p0 / 10 : UINT16_MAX;
|
||||
sen5xmeasurement.pM10p0 = !isnan(uint_pM10p0) ? uint_pM10p0 / 10 : UINT16_MAX;
|
||||
sen5xmeasurement.humidity = !isnan(int_humidity) ? int_humidity / 100.0f : FLT_MAX;
|
||||
sen5xmeasurement.temperature = !isnan(int_temperature) ? int_temperature / 200.0f : FLT_MAX;
|
||||
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 %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 %s readings: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sensorName, sen5xmeasurement.humidity,
|
||||
sen5xmeasurement.temperature, sen5xmeasurement.vocIndex);
|
||||
}
|
||||
|
||||
if (model == SEN55) {
|
||||
LOG_DEBUG("Got %s readings: noxIndex=%.2f", sensorName, sen5xmeasurement.noxIndex);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SEN5XSensor::readPNValues(bool cumulative)
|
||||
{
|
||||
if (!sendCommand(SEN5X_READ_PM_VALUES)) {
|
||||
LOG_ERROR("SEN5X: Error sending read command");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("SEN5X: Reading PN Values");
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
uint8_t dataBuffer[20];
|
||||
size_t receivedNumber = readBuffer(&dataBuffer[0], 30);
|
||||
if (receivedNumber == 0) {
|
||||
LOG_ERROR("SEN5X: Error getting PN values");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the integers
|
||||
// uint16_t uint_pM1p0 = static_cast<uint16_t>((dataBuffer[0] << 8) | dataBuffer[1]);
|
||||
// uint16_t uint_pM2p5 = static_cast<uint16_t>((dataBuffer[2] << 8) | dataBuffer[3]);
|
||||
// uint16_t uint_pM4p0 = static_cast<uint16_t>((dataBuffer[4] << 8) | dataBuffer[5]);
|
||||
// uint16_t uint_pM10p0 = static_cast<uint16_t>((dataBuffer[6] << 8) | dataBuffer[7]);
|
||||
uint16_t uint_pN0p5 = static_cast<uint16_t>((dataBuffer[8] << 8) | dataBuffer[9]);
|
||||
uint16_t uint_pN1p0 = static_cast<uint16_t>((dataBuffer[10] << 8) | dataBuffer[11]);
|
||||
uint16_t uint_pN2p5 = static_cast<uint16_t>((dataBuffer[12] << 8) | dataBuffer[13]);
|
||||
uint16_t uint_pN4p0 = static_cast<uint16_t>((dataBuffer[14] << 8) | dataBuffer[15]);
|
||||
uint16_t uint_pN10p0 = static_cast<uint16_t>((dataBuffer[16] << 8) | dataBuffer[17]);
|
||||
uint16_t uint_tSize = static_cast<uint16_t>((dataBuffer[18] << 8) | dataBuffer[19]);
|
||||
|
||||
// Convert values based on Sensirion Arduino lib
|
||||
// Multiply by 100 for converting from #/cm3 to #/0.1l for PN values
|
||||
sen5xmeasurement.pN0p5 = !isnan(uint_pN0p5) ? uint_pN0p5 / 10 * 100 : UINT32_MAX;
|
||||
sen5xmeasurement.pN1p0 = !isnan(uint_pN1p0) ? uint_pN1p0 / 10 * 100 : UINT32_MAX;
|
||||
sen5xmeasurement.pN2p5 = !isnan(uint_pN2p5) ? uint_pN2p5 / 10 * 100 : UINT32_MAX;
|
||||
sen5xmeasurement.pN4p0 = !isnan(uint_pN4p0) ? uint_pN4p0 / 10 * 100 : UINT32_MAX;
|
||||
sen5xmeasurement.pN10p0 = !isnan(uint_pN10p0) ? uint_pN10p0 / 10 * 100 : UINT32_MAX;
|
||||
sen5xmeasurement.tSize = !isnan(uint_tSize) ? uint_tSize / 1000.0f : FLT_MAX;
|
||||
|
||||
// Remove accumuluative values:
|
||||
// https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85
|
||||
if (!cumulative) {
|
||||
sen5xmeasurement.pN10p0 -= sen5xmeasurement.pN4p0;
|
||||
sen5xmeasurement.pN4p0 -= sen5xmeasurement.pN2p5;
|
||||
sen5xmeasurement.pN2p5 -= sen5xmeasurement.pN1p0;
|
||||
sen5xmeasurement.pN1p0 -= sen5xmeasurement.pN0p5;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
uint8_t SEN5XSensor::getMeasurements()
|
||||
{
|
||||
uint32_t now;
|
||||
now = getTime();
|
||||
|
||||
// Try to get new data
|
||||
if (!sendCommand(SEN5X_READ_DATA_READY)) {
|
||||
LOG_ERROR("SEN5X: Error sending command data ready flag");
|
||||
return 2;
|
||||
}
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
uint8_t dataReadyBuffer[3];
|
||||
size_t charNumber = readBuffer(&dataReadyBuffer[0], 3);
|
||||
if (charNumber == 0) {
|
||||
LOG_ERROR("SEN5X: Error getting device version value");
|
||||
return 2;
|
||||
}
|
||||
|
||||
bool dataReady = dataReadyBuffer[1];
|
||||
uint32_t sinceLastDataPollMs = (now - lastDataPoll) * 1000;
|
||||
// Check if data is ready, and if since last time we requested is less than SEN5X_POLL_INTERVAL
|
||||
if (!dataReady && (sinceLastDataPollMs > SEN5X_POLL_INTERVAL)) {
|
||||
LOG_INFO("SEN5X: Data is not ready");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!readValues()) {
|
||||
LOG_ERROR("SEN5X: Error getting readings");
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (!readPNValues(false)) {
|
||||
LOG_ERROR("SEN5X: Error getting PN readings");
|
||||
return 2;
|
||||
}
|
||||
|
||||
lastDataPoll = now;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t SEN5XSensor::wakeUpTimeMs()
|
||||
{
|
||||
return SEN5X_WARMUP_MS_2;
|
||||
}
|
||||
|
||||
int32_t SEN5XSensor::pendingForReadyMs()
|
||||
{
|
||||
uint32_t now;
|
||||
now = getTime();
|
||||
uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000;
|
||||
LOG_DEBUG("SEN5X: Since measure started: %ums", sincePmMeasureStarted);
|
||||
|
||||
switch (state) {
|
||||
case SEN5X_MEASUREMENT: {
|
||||
|
||||
if (sincePmMeasureStarted < SEN5X_WARMUP_MS_1) {
|
||||
LOG_INFO("SEN5X: not enough time passed since starting measurement");
|
||||
return SEN5X_WARMUP_MS_1 - sincePmMeasureStarted;
|
||||
}
|
||||
|
||||
if (!pmMeasureStarted) {
|
||||
pmMeasureStarted = now;
|
||||
}
|
||||
|
||||
// Get PN values to check if we are above or below threshold
|
||||
readPNValues(true);
|
||||
lastDataPoll = now;
|
||||
|
||||
// If the reading is low (the tyhreshold is in #/cm3) and second warmUp hasn't passed we return to come back later
|
||||
if ((sen5xmeasurement.pN4p0 / 100) < SEN5X_PN4P0_CONC_THD && sincePmMeasureStarted < SEN5X_WARMUP_MS_2) {
|
||||
LOG_INFO("SEN5X: Concentration is low, we will ask again in the second warm up period");
|
||||
state = SEN5X_MEASUREMENT_2;
|
||||
// Report how many seconds are pending to cover the first warm up period
|
||||
return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case SEN5X_MEASUREMENT_2: {
|
||||
if (sincePmMeasureStarted < SEN5X_WARMUP_MS_2) {
|
||||
// Report how many seconds are pending to cover the first warm up period
|
||||
return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
default: {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
LOG_INFO("SEN5X: Attempting to get metrics");
|
||||
if (!isActive()) {
|
||||
LOG_INFO("SEN5X: not in measurement mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t response;
|
||||
response = getMeasurements();
|
||||
|
||||
if (response == 0) {
|
||||
if (sen5xmeasurement.pM1p0 != UINT16_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_pm10_standard = true;
|
||||
measurement->variant.air_quality_metrics.pm10_standard = sen5xmeasurement.pM1p0;
|
||||
}
|
||||
if (sen5xmeasurement.pM2p5 != UINT16_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_pm25_standard = true;
|
||||
measurement->variant.air_quality_metrics.pm25_standard = sen5xmeasurement.pM2p5;
|
||||
}
|
||||
if (sen5xmeasurement.pM4p0 != UINT16_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_pm40_standard = true;
|
||||
measurement->variant.air_quality_metrics.pm40_standard = sen5xmeasurement.pM4p0;
|
||||
}
|
||||
if (sen5xmeasurement.pM10p0 != UINT16_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_pm100_standard = true;
|
||||
measurement->variant.air_quality_metrics.pm100_standard = sen5xmeasurement.pM10p0;
|
||||
}
|
||||
if (sen5xmeasurement.pN0p5 != UINT32_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_particles_05um = true;
|
||||
measurement->variant.air_quality_metrics.particles_05um = sen5xmeasurement.pN0p5;
|
||||
}
|
||||
if (sen5xmeasurement.pN1p0 != UINT32_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_particles_10um = true;
|
||||
measurement->variant.air_quality_metrics.particles_10um = sen5xmeasurement.pN1p0;
|
||||
}
|
||||
if (sen5xmeasurement.pN2p5 != UINT32_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_particles_25um = true;
|
||||
measurement->variant.air_quality_metrics.particles_25um = sen5xmeasurement.pN2p5;
|
||||
}
|
||||
if (sen5xmeasurement.pN4p0 != UINT32_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_particles_40um = true;
|
||||
measurement->variant.air_quality_metrics.particles_40um = sen5xmeasurement.pN4p0;
|
||||
}
|
||||
if (sen5xmeasurement.pN10p0 != UINT32_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_particles_100um = true;
|
||||
measurement->variant.air_quality_metrics.particles_100um = sen5xmeasurement.pN10p0;
|
||||
}
|
||||
if (sen5xmeasurement.tSize != FLT_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_particles_tps = true;
|
||||
measurement->variant.air_quality_metrics.particles_tps = sen5xmeasurement.tSize;
|
||||
}
|
||||
|
||||
if (model != SEN50) {
|
||||
if (sen5xmeasurement.humidity != FLT_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_pm_humidity = true;
|
||||
measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity;
|
||||
}
|
||||
if (sen5xmeasurement.temperature != FLT_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_pm_temperature = true;
|
||||
measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature;
|
||||
}
|
||||
if (sen5xmeasurement.noxIndex != FLT_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_pm_voc_idx = true;
|
||||
measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (model == SEN55) {
|
||||
if (sen5xmeasurement.noxIndex != FLT_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_pm_nox_idx = true;
|
||||
measurement->variant.air_quality_metrics.pm_nox_idx = sen5xmeasurement.noxIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (response == 1) {
|
||||
// TODO return because data was not ready yet
|
||||
// Should this return false?
|
||||
idle();
|
||||
return false;
|
||||
} else if (response == 2) {
|
||||
// Return with error for non-existing data
|
||||
idle();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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,
|
||||
meshtastic_AdminMessage *response)
|
||||
{
|
||||
AdminMessageHandleResult result;
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
|
||||
switch (request->which_payload_variant) {
|
||||
case meshtastic_AdminMessage_sensor_config_tag:
|
||||
if (!request->sensor_config.has_sen5x_config) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
|
||||
// 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_fan_cleaning && request->sensor_config.sen5x_config.fan_cleaning) {
|
||||
// this->startCleaning();
|
||||
// }
|
||||
|
||||
result = AdminMessageHandleResult::HANDLED;
|
||||
break;
|
||||
|
||||
default:
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "RTC.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "Wire.h"
|
||||
|
||||
// Warm up times for SEN5X from the datasheet
|
||||
#ifndef SEN5X_WARMUP_MS_1
|
||||
#define SEN5X_WARMUP_MS_1 15000
|
||||
#endif
|
||||
|
||||
#ifndef SEN5X_WARMUP_MS_2
|
||||
#define SEN5X_WARMUP_MS_2 30000
|
||||
#endif
|
||||
|
||||
#ifndef SEN5X_POLL_INTERVAL
|
||||
#define SEN5X_POLL_INTERVAL 1000
|
||||
#endif
|
||||
|
||||
#ifndef SEN5X_I2C_CLOCK_SPEED
|
||||
#define SEN5X_I2C_CLOCK_SPEED 100000
|
||||
#endif
|
||||
|
||||
/*
|
||||
Time after which the sensor can go to sleep, as the warmup period has passed
|
||||
and the VOCs sensor will is allowed to stop (although needs to recover the state
|
||||
each time)
|
||||
*/
|
||||
#ifndef SEN5X_VOC_STATE_WARMUP_S
|
||||
/* Note for Testing 5' is enough
|
||||
Sensirion recommends 1h
|
||||
This can be bypassed completely if switching to low-power RHT/Gas mode and setting
|
||||
SEN5X_VOC_STATE_WARMUP_S 0
|
||||
*/
|
||||
#define SEN5X_VOC_STATE_WARMUP_S 3600
|
||||
#endif
|
||||
|
||||
#define ONE_WEEK_IN_SECONDS 604800
|
||||
|
||||
struct _SEN5XMeasurements {
|
||||
uint16_t pM1p0;
|
||||
uint16_t pM2p5;
|
||||
uint16_t pM4p0;
|
||||
uint16_t pM10p0;
|
||||
uint32_t pN0p5;
|
||||
uint32_t pN1p0;
|
||||
uint32_t pN2p5;
|
||||
uint32_t pN4p0;
|
||||
uint32_t pN10p0;
|
||||
float tSize;
|
||||
float humidity;
|
||||
float temperature;
|
||||
float vocIndex;
|
||||
float noxIndex;
|
||||
};
|
||||
|
||||
class SEN5XSensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
TwoWire *_bus{};
|
||||
uint8_t _address{};
|
||||
|
||||
bool getVersion();
|
||||
float firmwareVer = -1;
|
||||
float hardwareVer = -1;
|
||||
float protocolVer = -1;
|
||||
bool findModel();
|
||||
|
||||
// Commands
|
||||
#define SEN5X_RESET 0xD304
|
||||
#define SEN5X_GET_PRODUCT_NAME 0xD014
|
||||
#define SEN5X_GET_FIRMWARE_VERSION 0xD100
|
||||
#define SEN5X_START_MEASUREMENT 0x0021
|
||||
#define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037
|
||||
#define SEN5X_STOP_MEASUREMENT 0x0104
|
||||
#define SEN5X_READ_DATA_READY 0x0202
|
||||
#define SEN5X_START_FAN_CLEANING 0x5607
|
||||
#define SEN5X_RW_VOCS_STATE 0x6181
|
||||
|
||||
#define SEN5X_READ_VALUES 0x03C4
|
||||
#define SEN5X_READ_RAW_VALUES 0x03D2
|
||||
#define SEN5X_READ_PM_VALUES 0x0413
|
||||
|
||||
#define SEN5X_VOC_VALID_TIME 600
|
||||
#define SEN5X_VOC_VALID_DATE 1514764800
|
||||
|
||||
enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 };
|
||||
SEN5Xmodel model = SEN5X_UNKNOWN;
|
||||
|
||||
enum SEN5XState {
|
||||
SEN5X_OFF,
|
||||
SEN5X_IDLE,
|
||||
SEN5X_RHTGAS_ONLY,
|
||||
SEN5X_MEASUREMENT,
|
||||
SEN5X_MEASUREMENT_2,
|
||||
SEN5X_CLEANING,
|
||||
SEN5X_NOT_DETECTED
|
||||
};
|
||||
SEN5XState state = SEN5X_OFF;
|
||||
// Flag to work on one-shot (read and sleep), or continuous mode
|
||||
bool oneShotMode = true;
|
||||
void setMode(bool setOneShot);
|
||||
bool vocStateValid();
|
||||
/* Sensirion recommends taking a reading after 15 seconds,
|
||||
if the Particle number reading is over 100#/cm3 the reading is OK,
|
||||
but if it is lower wait until 30 seconds and take it again.
|
||||
See: https://sensirion.com/resource/application_note/low_power_mode/sen5x
|
||||
*/
|
||||
#define SEN5X_PN4P0_CONC_THD 100
|
||||
|
||||
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(const uint8_t *buffer);
|
||||
bool startCleaning();
|
||||
uint8_t getMeasurements();
|
||||
// bool readRawValues();
|
||||
bool readPNValues(bool cumulative);
|
||||
bool readValues();
|
||||
|
||||
uint32_t pmMeasureStarted = 0;
|
||||
uint32_t rhtGasMeasureStarted = 0;
|
||||
uint32_t lastDataPoll = 0;
|
||||
_SEN5XMeasurements sen5xmeasurement{};
|
||||
|
||||
bool idle(bool checkState = true);
|
||||
|
||||
protected:
|
||||
// Store status of the sensor in this file
|
||||
const char *sen5XStateFileName = "/prefs/sen5X.dat";
|
||||
meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero;
|
||||
|
||||
bool loadState();
|
||||
bool saveState();
|
||||
|
||||
// Cleaning State
|
||||
uint32_t lastCleaning = 0;
|
||||
bool lastCleaningValid = false;
|
||||
|
||||
// VOC State
|
||||
#define SEN5X_VOC_STATE_BUFFER_SIZE 8
|
||||
uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]{};
|
||||
uint32_t vocTime = 0;
|
||||
bool vocValid = false;
|
||||
|
||||
bool vocStateFromSensor();
|
||||
bool vocStateToSensor();
|
||||
bool vocStateStable();
|
||||
bool vocStateRecent(uint32_t now);
|
||||
|
||||
public:
|
||||
SEN5XSensor();
|
||||
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 { return true; }
|
||||
virtual int32_t wakeUpTimeMs() override;
|
||||
virtual int32_t pendingForReadyMs() override;
|
||||
|
||||
AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,198 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cSfa3x.h>)
|
||||
|
||||
#include "../detect/reClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "SFA30Sensor.h"
|
||||
|
||||
SFA30Sensor::SFA30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SFA30, "SFA30"){};
|
||||
|
||||
bool SFA30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
|
||||
_bus = bus;
|
||||
_address = dev->address.address;
|
||||
|
||||
#ifdef SFA30_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return false;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* SFA30_I2C_CLOCK_SPEED */
|
||||
|
||||
sfa30.begin(*_bus, _address);
|
||||
delay(20);
|
||||
|
||||
if (this->isError(sfa30.deviceReset())) {
|
||||
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
state = State::IDLE;
|
||||
if (this->isError(sfa30.startContinuousMeasurement())) {
|
||||
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s starting measurement", sensorName);
|
||||
|
||||
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
status = 1;
|
||||
state = State::ACTIVE;
|
||||
measureStarted = getTime();
|
||||
LOG_INFO("%s Enabled", sensorName);
|
||||
|
||||
initI2CSensor();
|
||||
return true;
|
||||
};
|
||||
|
||||
bool SFA30Sensor::isError(uint16_t response)
|
||||
{
|
||||
if (response == SFA30_NO_ERROR)
|
||||
return false;
|
||||
|
||||
// TODO - Check error to char conversion
|
||||
LOG_ERROR("%s: %u", sensorName, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SFA30Sensor::sleep()
|
||||
{
|
||||
#ifdef SFA30_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* SFA30_I2C_CLOCK_SPEED */
|
||||
|
||||
// Note - not recommended for this sensor on a periodic basis
|
||||
if (this->isError(sfa30.stopMeasurement())) {
|
||||
LOG_ERROR("%s: can't stop measurement", sensorName);
|
||||
};
|
||||
|
||||
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
LOG_INFO("%s: stop measurement", sensorName);
|
||||
state = State::IDLE;
|
||||
measureStarted = 0;
|
||||
}
|
||||
|
||||
uint32_t SFA30Sensor::wakeUp()
|
||||
{
|
||||
#ifdef SFA30_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return false;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* SFA30_I2C_CLOCK_SPEED */
|
||||
|
||||
LOG_INFO("Waking up %s", sensorName);
|
||||
if (this->isError(sfa30.startContinuousMeasurement())) {
|
||||
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
state = State::ACTIVE;
|
||||
measureStarted = getTime();
|
||||
return SFA30_WARMUP_MS;
|
||||
}
|
||||
|
||||
int32_t SFA30Sensor::wakeUpTimeMs()
|
||||
{
|
||||
return SFA30_WARMUP_MS;
|
||||
}
|
||||
|
||||
bool SFA30Sensor::canSleep()
|
||||
{
|
||||
// Sleep is disabled in this sensor because readings are not tested with periodic sleep
|
||||
// with such low power consumption, prefered to keep it active
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SFA30Sensor::isActive()
|
||||
{
|
||||
return state == State::ACTIVE;
|
||||
}
|
||||
|
||||
int32_t SFA30Sensor::pendingForReadyMs()
|
||||
{
|
||||
uint32_t now;
|
||||
now = getTime();
|
||||
uint32_t sinceHchoMeasureStarted = (now - measureStarted) * 1000;
|
||||
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceHchoMeasureStarted);
|
||||
|
||||
if (sinceHchoMeasureStarted < SFA30_WARMUP_MS) {
|
||||
LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
|
||||
return SFA30_WARMUP_MS - sinceHchoMeasureStarted;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SFA30Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
float hcho = 0.0;
|
||||
float humidity = 0.0;
|
||||
float temperature = 0.0;
|
||||
|
||||
#ifdef SFA30_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return false;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* SFA30_I2C_CLOCK_SPEED */
|
||||
|
||||
if (this->isError(sfa30.readMeasuredValues(hcho, humidity, temperature))) {
|
||||
LOG_WARN("%s: No values", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
measurement->variant.air_quality_metrics.has_form_temperature = true;
|
||||
measurement->variant.air_quality_metrics.has_form_humidity = true;
|
||||
measurement->variant.air_quality_metrics.has_form_formaldehyde = true;
|
||||
|
||||
measurement->variant.air_quality_metrics.form_temperature = temperature;
|
||||
measurement->variant.air_quality_metrics.form_humidity = humidity;
|
||||
measurement->variant.air_quality_metrics.form_formaldehyde = hcho;
|
||||
|
||||
LOG_DEBUG("Got %s readings: hcho=%.2f, hcho_temp=%.2f, hcho_hum=%.2f", sensorName, hcho, temperature, humidity);
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cSfa3x.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "RTC.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <SensirionI2cSfa3x.h>
|
||||
|
||||
#define SFA30_I2C_CLOCK_SPEED 100000
|
||||
#define SFA30_WARMUP_MS 10000
|
||||
#define SFA30_NO_ERROR 0
|
||||
|
||||
class SFA30Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
enum class State { IDLE, ACTIVE };
|
||||
State state = State::IDLE;
|
||||
uint32_t measureStarted = 0;
|
||||
|
||||
SensirionI2cSfa3x sfa30;
|
||||
TwoWire *_bus{};
|
||||
uint8_t _address{};
|
||||
bool isError(uint16_t response);
|
||||
|
||||
public:
|
||||
SFA30Sensor();
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
|
||||
virtual bool isActive() override;
|
||||
virtual void sleep() override;
|
||||
virtual uint32_t wakeUp() override;
|
||||
virtual bool canSleep() override;
|
||||
virtual int32_t wakeUpTimeMs() override;
|
||||
virtual int32_t pendingForReadyMs() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
|
||||
#pragma once
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
@@ -26,7 +26,6 @@ class TelemetrySensor
|
||||
this->status = 0;
|
||||
}
|
||||
|
||||
const char *sensorName;
|
||||
meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET;
|
||||
unsigned status;
|
||||
bool initialized = false;
|
||||
@@ -56,13 +55,18 @@ class TelemetrySensor
|
||||
return AdminMessageHandleResult::NOT_HANDLED;
|
||||
}
|
||||
|
||||
const char *sensorName;
|
||||
// TODO: delete after migration
|
||||
bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; }
|
||||
|
||||
// Functions to sleep / wakeup sensors that support it
|
||||
// These functions can save power consumption in cases like AQ
|
||||
virtual void sleep(){};
|
||||
virtual uint32_t wakeUp() { return 0; }
|
||||
// Return active by default, override per sensor
|
||||
virtual bool isActive() { return true; }
|
||||
virtual bool isActive() { return true; } // Return true by default, override per sensor
|
||||
virtual bool canSleep() { return false; } // Return false by default, override per sensor
|
||||
virtual int32_t wakeUpTimeMs() { return 0; }
|
||||
virtual int32_t pendingForReadyMs() { return 0; }
|
||||
|
||||
#if WIRE_INTERFACES_COUNT > 1
|
||||
// Set to true if Implementation only works first I2C port (Wire)
|
||||
|
||||
@@ -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