Skip to content

Commit

Permalink
Add health telemetry module (#4927)
Browse files Browse the repository at this point in the history
* Add stub health telemetry module

* Add detection for MAX30102 Health Sensor

It lives on I2C bus at 0x57, which conflicts with an existing
sensor. Add code to check the PARTID register for its response 0x15
per spec.

* Add detection for MLX90614

An IR Temperature sensor suitable for livestock monitoring.

* Add libraries for MLX90614 and MAX30102 sensors

* Fix Trunk

* Add support for MLX90614 IR Temperature Sensor

* Add support for MAX30102 (Temperature)

* Make it build - our first HealthTelemetry on the mesh.

If a MAX30102 is connected, its temperature will be sent to the
mesh as HealthTelemetry.

* Add spo2 and heart rate calculations to MAX30102

* Switch MLX90614 to Adafruit library

Sparkfun was having fun with SDA/SCL variables which we can avoid
by switching to this highly similar library.

* Enable HealthTelemetry if MLX90614 detected

* Change MLX90614 emissivity for human skin.

* Add health screen!

* Remove autogenerated file from branch

* Preparing for review

* Fix MeshService master sync from before.

* Prepare for review

* For the americans

* Fix native build

* Fix for devices with no screen

* Remove extra log causing issues

---------

Co-authored-by: Tom Fifield <[email protected]>
  • Loading branch information
thebentern and fifieldt authored Oct 8, 2024
1 parent 1c54388 commit 411aeda
Show file tree
Hide file tree
Showing 13 changed files with 515 additions and 2 deletions.
2 changes: 2 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define MLX90632_ADDR 0x3A
#define DFROBOT_LARK_ADDR 0x42
#define NAU7802_ADDR 0x2A
#define MAX30102_ADDR 0x57
#define MLX90614_ADDR 0x5A

// -----------------------------------------------------------------------------
// ACCELEROMETER
Expand Down
4 changes: 3 additions & 1 deletion src/detect/ScanI2C.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 13 additions & 1 deletion src/detect/ScanI2CTwoWire.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/mesh/NodeDB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions src/modules/Modules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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();
Expand Down
249 changes: 249 additions & 0 deletions src/modules/Telemetry/HealthTelemetry.cpp
Original file line number Diff line number Diff line change
@@ -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 <OLEDDisplay.h>
#include <OLEDDisplayUi.h>

// 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 <Throttle.h>

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
Loading

0 comments on commit 411aeda

Please sign in to comment.