diff --git a/platformio.ini b/platformio.ini
index 5dcf61afdf..2f76c82363 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -152,6 +152,8 @@ lib_deps =
ClosedCube OPT3001@^1.1.2
emotibit/EmotiBit MLX90632@^1.0.8
dfrobot/DFRobot_RTU@^1.0.3
+ sparkfun/SparkFun MAX3010x Pulse and Proximity Sensor Library@^1.1.2
+ adafruit/Adafruit MLX90614 Library@^2.1.5
https://github.com/boschsensortec/Bosch-BSEC2-Library#v1.7.2502
boschsensortec/BME68x Sensor Library@^1.1.40407
diff --git a/src/configuration.h b/src/configuration.h
index 3cd93ec83d..975c9fc685 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -144,6 +144,8 @@ along with this program. If not, see .
#define MLX90632_ADDR 0x3A
#define DFROBOT_LARK_ADDR 0x42
#define NAU7802_ADDR 0x2A
+#define MAX30102_ADDR 0x57
+#define MLX90614_ADDR 0x5A
// -----------------------------------------------------------------------------
// ACCELEROMETER
diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h
index 3b49026ced..920af06c74 100644
--- a/src/detect/ScanI2C.h
+++ b/src/detect/ScanI2C.h
@@ -52,13 +52,15 @@ class ScanI2C
TSL2591,
OPT3001,
MLX90632,
+ MLX90614,
AHT10,
BMX160,
DFROBOT_LARK,
NAU7802,
FT6336U,
STK8BAXX,
- ICM20948
+ ICM20948,
+ MAX30102
} DeviceType;
// typedef uint8_t DeviceAddress;
diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp
index 43340765aa..74597fbc3c 100644
--- a/src/detect/ScanI2CTwoWire.cpp
+++ b/src/detect/ScanI2CTwoWire.cpp
@@ -356,7 +356,18 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break;
SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3 sensor found\n")
- SCAN_SIMPLE_CASE(RCWL9620_ADDR, RCWL9620, "RCWL9620 sensor found\n")
+ case RCWL9620_ADDR:
+ // get MAX30102 PARTID
+ registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1);
+ if (registerValue == 0x15) {
+ type = MAX30102;
+ LOG_INFO("MAX30102 Health sensor found\n");
+ break;
+ } else {
+ type = RCWL9620;
+ LOG_INFO("RCWL9620 sensor found\n");
+ }
+ break;
case LPS22HB_ADDR_ALT:
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB sensor found\n")
@@ -394,6 +405,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802 based scale found\n");
SCAN_SIMPLE_CASE(FT6336U_ADDR, FT6336U, "FT6336U touchscreen found\n");
SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048 lipo fuel gauge found\n");
+ SCAN_SIMPLE_CASE(MLX90614_ADDR, MLX90614, "MLX90614 IR temp sensor found\n");
case ICM20948_ADDR: // same as BMX160_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
diff --git a/src/main.cpp b/src/main.cpp
index 25059f3c7f..9ddc0864cb 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -580,10 +580,12 @@ void setup()
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::TSL2591, meshtastic_TelemetrySensorType_TSL25911FN)
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::OPT3001, meshtastic_TelemetrySensorType_OPT3001)
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::MLX90632, meshtastic_TelemetrySensorType_MLX90632)
+ SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614)
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::SHT4X, meshtastic_TelemetrySensorType_SHT4X)
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::AHT10, meshtastic_TelemetrySensorType_AHT10)
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::DFROBOT_LARK, meshtastic_TelemetrySensorType_DFROBOT_LARK)
SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948)
+ SCANNER_TO_SENSORS_MAP(ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102)
i2cScanner.reset();
#endif
diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp
index 97ded5a50f..0d96051618 100644
--- a/src/mesh/NodeDB.cpp
+++ b/src/mesh/NodeDB.cpp
@@ -533,6 +533,7 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role)
moduleConfig.telemetry.device_update_interval = UINT32_MAX;
moduleConfig.telemetry.environment_update_interval = UINT32_MAX;
moduleConfig.telemetry.air_quality_interval = UINT32_MAX;
+ moduleConfig.telemetry.health_update_interval = UINT32_MAX;
}
}
@@ -543,6 +544,7 @@ void NodeDB::initModuleConfigIntervals()
moduleConfig.telemetry.environment_update_interval = 0;
moduleConfig.telemetry.air_quality_interval = 0;
moduleConfig.telemetry.power_update_interval = 0;
+ moduleConfig.telemetry.health_update_interval = 0;
moduleConfig.neighbor_info.update_interval = 0;
moduleConfig.paxcounter.paxcounter_update_interval = 0;
}
diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index 554fad6a91..ad3f0ace45 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -58,6 +58,7 @@
#include "main.h"
#include "modules/Telemetry/AirQualityTelemetry.h"
#include "modules/Telemetry/EnvironmentTelemetry.h"
+#include "modules/Telemetry/HealthTelemetry.h"
#endif
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY
#include "modules/Telemetry/PowerTelemetry.h"
@@ -194,6 +195,10 @@ void setupModules()
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
new AirQualityTelemetryModule();
}
+ if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 ||
+ nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) {
+ new HealthTelemetryModule();
+ }
#endif
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
new PowerTelemetryModule();
diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp
new file mode 100644
index 0000000000..bcf9d9d57c
--- /dev/null
+++ b/src/modules/Telemetry/HealthTelemetry.cpp
@@ -0,0 +1,249 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO)
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "Default.h"
+#include "HealthTelemetry.h"
+#include "MeshService.h"
+#include "NodeDB.h"
+#include "PowerFSM.h"
+#include "RTC.h"
+#include "Router.h"
+#include "UnitConversions.h"
+#include "main.h"
+#include "power.h"
+#include "sleep.h"
+#include "target_specific.h"
+#include
+#include
+
+// Sensors
+#include "Sensor/MAX30102Sensor.h"
+#include "Sensor/MLX90614Sensor.h"
+
+MAX30102Sensor max30102Sensor;
+MLX90614Sensor mlx90614Sensor;
+
+#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
+#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
+
+#if (HAS_SCREEN)
+#include "graphics/ScreenFonts.h"
+#endif
+#include
+
+int32_t HealthTelemetryModule::runOnce()
+{
+ if (sleepOnNextExecution == true) {
+ sleepOnNextExecution = false;
+ uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.health_update_interval,
+ default_telemetry_broadcast_interval_secs);
+ LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.\n", nightyNightMs);
+ doDeepSleep(nightyNightMs, true);
+ }
+
+ uint32_t result = UINT32_MAX;
+
+ if (!(moduleConfig.telemetry.health_measurement_enabled || moduleConfig.telemetry.health_screen_enabled)) {
+ // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it
+ return disable();
+ }
+
+ if (firstTime) {
+ // This is the first time the OSThread library has called this function, so do some setup
+ firstTime = false;
+
+ if (moduleConfig.telemetry.health_measurement_enabled) {
+ LOG_INFO("Health Telemetry: Initializing\n");
+ // Initialize sensors
+ if (mlx90614Sensor.hasSensor())
+ result = mlx90614Sensor.runOnce();
+ if (max30102Sensor.hasSensor())
+ result = max30102Sensor.runOnce();
+ }
+ return result;
+ } else {
+ // if we somehow got to a second run of this module with measurement disabled, then just wait forever
+ if (!moduleConfig.telemetry.health_measurement_enabled) {
+ return disable();
+ }
+
+ if (((lastSentToMesh == 0) ||
+ !Throttle::isWithinTimespanMs(lastSentToMesh, 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();
+ } 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
+ // Only send while queue is empty (phone assumed connected)
+ sendTelemetry(NODENUM_BROADCAST, true);
+ lastSentToPhone = millis();
+ }
+ }
+ return min(sendToPhoneIntervalMs, result);
+}
+
+bool HealthTelemetryModule::wantUIFrame()
+{
+ return moduleConfig.telemetry.health_screen_enabled;
+}
+
+void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
+{
+ display->setTextAlignment(TEXT_ALIGN_LEFT);
+ display->setFont(FONT_SMALL);
+
+ if (lastMeasurementPacket == nullptr) {
+ // If there's no valid packet, display "Health"
+ display->drawString(x, y, "Health");
+ display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement");
+ return;
+ }
+
+ // Decode the last measurement packet
+ meshtastic_Telemetry lastMeasurement;
+ uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
+ const char *lastSender = getSenderShortName(*lastMeasurementPacket);
+
+ const meshtastic_Data &p = lastMeasurementPacket->decoded;
+ if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) {
+ display->drawString(x, y, "Measurement Error");
+ LOG_ERROR("Unable to decode last packet");
+ return;
+ }
+
+ // Display "Health From: ..." on its own
+ display->drawString(x, y, "Health From: " + String(lastSender) + "(" + String(agoSecs) + "s)");
+
+ String last_temp = String(lastMeasurement.variant.health_metrics.temperature, 0) + "°C";
+ if (moduleConfig.telemetry.environment_display_fahrenheit) {
+ last_temp = String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature), 0) + "°F";
+ }
+
+ // Continue with the remaining details
+ display->drawString(x, y += _fontHeight(FONT_SMALL), "Temp: " + last_temp);
+ if (lastMeasurement.variant.health_metrics.has_heart_bpm) {
+ display->drawString(x, y += _fontHeight(FONT_SMALL),
+ "Heart Rate: " + String(lastMeasurement.variant.health_metrics.heart_bpm, 0) + " bpm");
+ }
+ if (lastMeasurement.variant.health_metrics.has_spO2) {
+ display->drawString(x, y += _fontHeight(FONT_SMALL),
+ "spO2: " + String(lastMeasurement.variant.health_metrics.spO2, 0) + " %");
+ }
+}
+
+bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
+{
+ if (t->which_variant == meshtastic_Telemetry_health_metrics_tag) {
+#ifdef DEBUG_PORT
+ const char *sender = getSenderShortName(mp);
+
+ LOG_INFO("(Received from %s): temperature=%f, heart_bpm=%d, spO2=%d,\n", sender, t->variant.health_metrics.temperature,
+ t->variant.health_metrics.heart_bpm, t->variant.health_metrics.spO2);
+
+#endif
+ // release previous packet before occupying a new spot
+ if (lastMeasurementPacket != nullptr)
+ packetPool.release(lastMeasurementPacket);
+
+ lastMeasurementPacket = packetPool.allocCopy(mp);
+ }
+
+ return false; // Let others look at this message also if they want
+}
+
+bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m)
+{
+ bool valid = true;
+ bool hasSensor = false;
+ 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);
+ hasSensor = true;
+ }
+ if (mlx90614Sensor.hasSensor()) {
+ valid = valid && mlx90614Sensor.getMetrics(m);
+ hasSensor = true;
+ }
+
+ return valid && hasSensor;
+}
+
+meshtastic_MeshPacket *HealthTelemetryModule::allocReply()
+{
+ if (currentRequest) {
+ auto req = *currentRequest;
+ const auto &p = req.decoded;
+ meshtastic_Telemetry scratch;
+ meshtastic_Telemetry *decoded = NULL;
+ memset(&scratch, 0, sizeof(scratch));
+ if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) {
+ decoded = &scratch;
+ } else {
+ LOG_ERROR("Error decoding HealthTelemetry module!\n");
+ return NULL;
+ }
+ // Check for a request for health metrics
+ if (decoded->which_variant == meshtastic_Telemetry_health_metrics_tag) {
+ meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
+ if (getHealthTelemetry(&m)) {
+ LOG_INFO("Health telemetry replying to request\n");
+ return allocDataProtobuf(m);
+ } else {
+ return NULL;
+ }
+ }
+ }
+ return NULL;
+}
+
+bool HealthTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
+{
+ meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
+ m.which_variant = meshtastic_Telemetry_health_metrics_tag;
+ m.time = getTime();
+ if (getHealthTelemetry(&m)) {
+ LOG_INFO("(Sending): temperature=%f, heart_bpm=%d, spO2=%d\n", m.variant.health_metrics.temperature,
+ m.variant.health_metrics.heart_bpm, m.variant.health_metrics.spO2);
+
+ sensor_read_error_count = 0;
+
+ meshtastic_MeshPacket *p = allocDataProtobuf(m);
+ p->to = dest;
+ p->decoded.want_response = false;
+ if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR)
+ p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
+ else
+ p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
+ // release previous packet before occupying a new spot
+ if (lastMeasurementPacket != nullptr)
+ packetPool.release(lastMeasurementPacket);
+
+ lastMeasurementPacket = packetPool.allocCopy(*p);
+ if (phoneOnly) {
+ LOG_INFO("Sending packet to phone\n");
+ service->sendToPhone(p);
+ } else {
+ LOG_INFO("Sending packet to mesh\n");
+ service->sendToMesh(p, RX_SRC_LOCAL, true);
+
+ if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
+ LOG_DEBUG("Starting next execution in 5 seconds and then going to sleep.\n");
+ sleepOnNextExecution = true;
+ setIntervalFromNow(5000);
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+#endif
diff --git a/src/modules/Telemetry/HealthTelemetry.h b/src/modules/Telemetry/HealthTelemetry.h
new file mode 100644
index 0000000000..4ad0da8388
--- /dev/null
+++ b/src/modules/Telemetry/HealthTelemetry.h
@@ -0,0 +1,60 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO)
+
+#pragma once
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "NodeDB.h"
+#include "ProtobufModule.h"
+#include
+#include
+
+class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModule
+{
+ CallbackObserver nodeStatusObserver =
+ CallbackObserver(this, &HealthTelemetryModule::handleStatusUpdate);
+
+ public:
+ HealthTelemetryModule()
+ : concurrency::OSThread("HealthTelemetryModule"),
+ ProtobufModule("HealthTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
+ {
+ lastMeasurementPacket = nullptr;
+ nodeStatusObserver.observe(&nodeStatus->onNewStatus);
+ setIntervalFromNow(10 * 1000);
+ }
+
+#if !HAS_SCREEN
+ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
+#else
+ virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
+#endif
+
+ virtual bool wantUIFrame() override;
+
+ protected:
+ /** Called to handle a particular incoming message
+ @return true if you've guaranteed you've handled this message and no other handlers should be considered for it
+ */
+ virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override;
+ virtual int32_t runOnce() override;
+ /** Called to get current Health telemetry data
+ @return true if it contains valid data
+ */
+ bool getHealthTelemetry(meshtastic_Telemetry *m);
+ virtual meshtastic_MeshPacket *allocReply() override;
+ /**
+ * Send our Telemetry into the mesh
+ */
+ bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
+
+ private:
+ 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;
+};
+
+#endif
diff --git a/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp b/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp
new file mode 100644
index 0000000000..b3b20e5f28
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp
@@ -0,0 +1,83 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO)
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "MAX30102Sensor.h"
+#include "TelemetrySensor.h"
+#include
+
+MAX30102Sensor::MAX30102Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MAX30102, "MAX30102") {}
+
+int32_t MAX30102Sensor::runOnce()
+{
+ LOG_INFO("Init sensor: %s\n", sensorName);
+ if (!hasSensor()) {
+ return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
+ }
+
+ if (max30102.begin(*nodeTelemetrySensorsMap[sensorType].second, _speed, nodeTelemetrySensorsMap[sensorType].first) ==
+ true) // MAX30102 init
+ {
+ byte brightness = 60; // 0=Off to 255=50mA
+ byte sampleAverage = 4; // 1, 2, 4, 8, 16, 32
+ byte leds = 2; // 1 = Red only, 2 = Red + IR
+ byte sampleRate = 100; // 50, 100, 200, 400, 800, 1000, 1600, 3200
+ int pulseWidth = 411; // 69, 118, 215, 411
+ int adcRange = 4096; // 2048, 4096, 8192, 16384
+
+ max30102.enableDIETEMPRDY(); // Enable the temperature ready interrupt
+ max30102.setup(brightness, sampleAverage, leds, sampleRate, pulseWidth, adcRange);
+ LOG_DEBUG("MAX30102 Init Succeed\n");
+ status = true;
+ } else {
+ LOG_ERROR("MAX30102 Init Failed\n");
+ status = false;
+ }
+ return initI2CSensor();
+}
+
+void MAX30102Sensor::setup() {}
+
+bool MAX30102Sensor::getMetrics(meshtastic_Telemetry *measurement)
+{
+ uint32_t ir_buff[MAX30102_BUFFER_LEN];
+ uint32_t red_buff[MAX30102_BUFFER_LEN];
+ int32_t spo2;
+ int8_t spo2_valid;
+ int32_t heart_rate;
+ int8_t heart_rate_valid;
+ float temp = max30102.readTemperature();
+
+ measurement->variant.environment_metrics.temperature = temp;
+ measurement->variant.environment_metrics.has_temperature = true;
+ measurement->variant.health_metrics.temperature = temp;
+ measurement->variant.health_metrics.has_temperature = true;
+ for (byte i = 0; i < MAX30102_BUFFER_LEN; i++) {
+ while (max30102.available() == false)
+ max30102.check();
+
+ red_buff[i] = max30102.getRed();
+ ir_buff[i] = max30102.getIR();
+ max30102.nextSample();
+ }
+
+ maxim_heart_rate_and_oxygen_saturation(ir_buff, MAX30102_BUFFER_LEN, red_buff, &spo2, &spo2_valid, &heart_rate,
+ &heart_rate_valid);
+ LOG_DEBUG("heart_rate=%d(%d), sp02=%d(%d)", heart_rate, heart_rate_valid, spo2, spo2_valid);
+ if (heart_rate_valid) {
+ measurement->variant.health_metrics.has_heart_bpm = true;
+ measurement->variant.health_metrics.heart_bpm = heart_rate;
+ } else {
+ measurement->variant.health_metrics.has_heart_bpm = false;
+ }
+ if (spo2_valid) {
+ measurement->variant.health_metrics.has_spO2 = true;
+ measurement->variant.health_metrics.spO2 = spo2;
+ } else {
+ measurement->variant.health_metrics.has_spO2 = true;
+ }
+ return true;
+}
+
+#endif
diff --git a/src/modules/Telemetry/Sensor/MAX30102Sensor.h b/src/modules/Telemetry/Sensor/MAX30102Sensor.h
new file mode 100644
index 0000000000..426d9d3650
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/MAX30102Sensor.h
@@ -0,0 +1,26 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO)
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "TelemetrySensor.h"
+#include
+
+#define MAX30102_BUFFER_LEN 100
+
+class MAX30102Sensor : public TelemetrySensor
+{
+ private:
+ MAX30105 max30102 = MAX30105();
+ uint32_t _speed = 200000UL;
+
+ protected:
+ virtual void setup() override;
+
+ public:
+ MAX30102Sensor();
+ virtual int32_t runOnce() override;
+ virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
+};
+
+#endif
diff --git a/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp b/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp
new file mode 100644
index 0000000000..92c22bf212
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp
@@ -0,0 +1,44 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO)
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "MLX90614Sensor.h"
+#include "TelemetrySensor.h"
+MLX90614Sensor::MLX90614Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MLX90614, "MLX90614") {}
+
+int32_t MLX90614Sensor::runOnce()
+{
+ LOG_INFO("Init sensor: %s\n", sensorName);
+ if (!hasSensor()) {
+ return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
+ }
+
+ if (mlx.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second) == true) // MLX90614 init
+ {
+ LOG_DEBUG("MLX90614 emissivity: %f", mlx.readEmissivity());
+ if (fabs(MLX90614_EMISSIVITY - mlx.readEmissivity()) > 0.001) {
+ mlx.writeEmissivity(MLX90614_EMISSIVITY);
+ LOG_INFO("MLX90614 emissivity updated. In case of weird data, power cycle.");
+ }
+ LOG_DEBUG("MLX90614 Init Succeed\n");
+ status = true;
+ } else {
+ LOG_ERROR("MLX90614 Init Failed\n");
+ status = false;
+ }
+ return initI2CSensor();
+}
+
+void MLX90614Sensor::setup() {}
+
+bool MLX90614Sensor::getMetrics(meshtastic_Telemetry *measurement)
+{
+ measurement->variant.environment_metrics.temperature = mlx.readAmbientTempC();
+ measurement->variant.environment_metrics.has_temperature = true;
+ measurement->variant.health_metrics.temperature = mlx.readObjectTempC();
+ measurement->variant.health_metrics.has_temperature = true;
+ return true;
+}
+
+#endif
diff --git a/src/modules/Telemetry/Sensor/MLX90614Sensor.h b/src/modules/Telemetry/Sensor/MLX90614Sensor.h
new file mode 100644
index 0000000000..00f63449e5
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/MLX90614Sensor.h
@@ -0,0 +1,24 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_PORTDUINO)
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "TelemetrySensor.h"
+#include
+
+#define MLX90614_EMISSIVITY 0.98 // human skin
+
+class MLX90614Sensor : public TelemetrySensor
+{
+ private:
+ Adafruit_MLX90614 mlx = Adafruit_MLX90614();
+
+ protected:
+ virtual void setup() override;
+
+ public:
+ MLX90614Sensor();
+ virtual int32_t runOnce() override;
+ virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
+};
+
+#endif