feat: retain multi-channel voltage telemetry

This commit is contained in:
mikecarper committed 2026-08-25 22:51:22 -07:00
1 parent ad1ff3f03e
commit 131bbafc67
17 files changed
+1603 -138

No files matched your search

+529
View File
@@ -0,0 +1,529 @@
#pragma once
#include <math.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
namespace mesh {
// Boot-local history for external voltage monitors. Each configured LPP
// channel owns four days of half-hour samples. Codes are packed at 15 bits:
// zero means missing and 1..32767 represent 0.02..655.34 V.
class ExternalVoltageHistory {
public:
struct Reading {
uint8_t channel;
float voltage;
bool valid;
};
static constexpr uint32_t SAMPLE_INTERVAL_SECONDS = 30UL * 60UL;
static constexpr uint8_t SAMPLES_PER_DAY = 48;
static constexpr uint8_t RETENTION_DAYS = 4;
static constexpr uint16_t RETENTION_SAMPLES =
RETENTION_DAYS * SAMPLES_PER_DAY;
static constexpr uint8_t SAMPLE_BITS = 15;
static constexpr uint16_t MAX_CODE = 0x7FFF;
static constexpr uint32_t STEP_MILLIVOLTS = 20;
static constexpr uint32_t MAX_MILLIVOLTS =
(uint32_t)MAX_CODE * STEP_MILLIVOLTS;
static constexpr uint8_t MAX_CHANNELS = 16;
static constexpr size_t BYTES_PER_CHANNEL =
(RETENTION_SAMPLES * SAMPLE_BITS + 7U) / 8U;
static constexpr uint8_t PAGE_PAYLOAD_TYPE_V1 = 0x14;
static constexpr uint8_t PAGE_COUNT = RETENTION_DAYS;
static constexpr size_t PAGE_HEADER_SIZE = 8;
static constexpr size_t PAGE_DATA_SIZE =
(SAMPLES_PER_DAY * SAMPLE_BITS + 7U) / 8U;
static constexpr size_t BINARY_SOURCE_ID_SIZE = 8;
static constexpr size_t BINARY_HEADER_SIZE = 20;
static constexpr uint8_t BINARY_SAMPLES_PER_CHUNK = 64;
static constexpr size_t BINARY_DATA_SIZE =
(BINARY_SAMPLES_PER_CHUNK * SAMPLE_BITS + 7U) / 8U;
static constexpr size_t BINARY_PAYLOAD_SIZE =
BINARY_HEADER_SIZE + BINARY_DATA_SIZE;
ExternalVoltageHistory()
: _samples(NULL), _channel_count(0), _next(0), _count(0),
_last_bucket(0), _has_bucket(false) {
memset(_channels, 0, sizeof(_channels));
}
~ExternalVoltageHistory() {
free(_samples);
}
ExternalVoltageHistory(const ExternalVoltageHistory&) = delete;
ExternalVoltageHistory& operator=(const ExternalVoltageHistory&) = delete;
bool configure(const uint8_t channels[], uint8_t count) {
uint8_t unique[MAX_CHANNELS];
uint8_t unique_count = 0;
if (channels != NULL) {
for (uint8_t i = 0; i < count && unique_count < MAX_CHANNELS; i++) {
if (channels[i] == 0) continue;
bool duplicate = false;
for (uint8_t j = 0; j < unique_count; j++) {
if (unique[j] == channels[i]) {
duplicate = true;
break;
}
}
if (!duplicate) unique[unique_count++] = channels[i];
}
}
if (unique_count == 0) {
free(_samples);
_samples = NULL;
_channel_count = 0;
memset(_channels, 0, sizeof(_channels));
clear();
return true;
}
const size_t bytes = (size_t)unique_count * BYTES_PER_CHANNEL;
uint8_t* replacement = static_cast<uint8_t*>(malloc(bytes));
if (replacement == NULL) return false;
memset(replacement, 0, bytes);
free(_samples);
_samples = replacement;
_channel_count = unique_count;
memset(_channels, 0, sizeof(_channels));
memcpy(_channels, unique, (size_t)unique_count * sizeof(unique[0]));
_next = 0;
_count = 0;
_last_bucket = 0;
_has_bucket = false;
return true;
}
void clear() {
if (_samples != NULL) {
memset(_samples, 0, (size_t)_channel_count * BYTES_PER_CHANNEL);
}
_next = 0;
_count = 0;
_last_bucket = 0;
_has_bucket = false;
}
uint8_t channelCount() const { return _channel_count; }
uint8_t channelAt(uint8_t index) const {
return index < _channel_count ? _channels[index] : 0;
}
uint8_t populatedChannelCount() const {
uint8_t count = 0;
for (uint8_t i = 0; i < _channel_count; i++) {
if (channelIndexHasValue(i)) count++;
}
return count;
}
uint8_t populatedChannelAt(uint8_t index) const {
for (uint8_t i = 0; i < _channel_count; i++) {
if (!channelIndexHasValue(i)) continue;
if (index == 0) return _channels[i];
index--;
}
return 0;
}
bool channelHasValue(uint8_t channel) const {
const int channel_index = findChannel(channel);
return channel_index >= 0
&& channelIndexHasValue((uint8_t)channel_index);
}
uint16_t sampleCount() const { return _count; }
size_t storageBytes() const {
return (size_t)_channel_count * BYTES_PER_CHANNEL;
}
bool sampleDue(uint32_t epoch_seconds) const {
return !_has_bucket
|| epoch_seconds / SAMPLE_INTERVAL_SECONDS != _last_bucket;
}
static uint16_t encodeVoltage(float voltage, bool valid = true) {
if (!valid || !isfinite(voltage) || voltage <= 0.0f) return 0;
const double scaled = (double)voltage * 1000.0
/ (double)STEP_MILLIVOLTS;
if (scaled >= MAX_CODE) return MAX_CODE;
long code = lround(scaled);
if (code < 1) code = 1;
return (uint16_t)code;
}
static uint32_t decodeMillivolts(uint16_t code) {
return code == 0 ? 0 : (uint32_t)code * STEP_MILLIVOLTS;
}
void record(uint32_t epoch_seconds, const Reading readings[],
uint8_t reading_count) {
if (_samples == NULL || _channel_count == 0) return;
const uint32_t bucket = epoch_seconds / SAMPLE_INTERVAL_SECONDS;
if (!_has_bucket) {
append(readings, reading_count);
_last_bucket = bucket;
_has_bucket = true;
return;
}
if (bucket == _last_bucket) return;
if (bucket < _last_bucket
|| bucket - _last_bucket > RETENTION_SAMPLES) {
clear();
append(readings, reading_count);
_last_bucket = bucket;
_has_bucket = true;
return;
}
const uint32_t skipped = bucket - _last_bucket - 1U;
for (uint32_t i = 0; i < skipped; i++) append(NULL, 0);
append(readings, reading_count);
_last_bucket = bucket;
}
uint8_t binaryChunkCount() const {
return (uint8_t)((_count + BINARY_SAMPLES_PER_CHUNK - 1U)
/ BINARY_SAMPLES_PER_CHUNK);
}
size_t formatBinarySnapshot(
const uint8_t source_id[BINARY_SOURCE_ID_SIZE], uint8_t channel,
uint8_t chunk_index, uint8_t* dest, size_t dest_size) const {
const int channel_index = findChannel(channel);
const uint8_t chunk_count = binaryChunkCount();
if (source_id == NULL || dest == NULL || channel_index < 0
|| !channelIndexHasValue((uint8_t)channel_index)
|| chunk_index >= chunk_count) {
return 0;
}
const uint16_t chronological_start =
(uint16_t)(chunk_index * BINARY_SAMPLES_PER_CHUNK);
uint16_t sample_count = (uint16_t)(_count - chronological_start);
if (sample_count > BINARY_SAMPLES_PER_CHUNK) {
sample_count = BINARY_SAMPLES_PER_CHUNK;
}
const size_t data_size =
(sample_count * SAMPLE_BITS + 7U) / 8U;
const size_t payload_size = BINARY_HEADER_SIZE + data_size;
if (dest_size < payload_size) return 0;
memset(dest, 0, payload_size);
memcpy(dest, "IVB1", 4);
memcpy(&dest[4], source_id, BINARY_SOURCE_ID_SIZE);
const uint16_t oldest_offset =
(uint16_t)(_count - 1U - chronological_start);
putUint32LE(&dest[12], startEpochForOffset(oldest_offset));
putUint16LE(&dest[16],
(uint16_t)(SAMPLE_INTERVAL_SECONDS / 60U));
dest[18] = channel;
dest[19] = (uint8_t)sample_count;
BitWriter writer(&dest[BINARY_HEADER_SIZE], data_size);
for (uint16_t slot = 0; slot < sample_count; slot++) {
writer.write(codeAtOffset(
(uint8_t)channel_index, (uint16_t)(oldest_offset - slot)));
}
return payload_size;
}
bool formatPageReply(const char* args, char* reply,
size_t reply_size) const {
if (reply == NULL || reply_size == 0) return false;
reply[0] = 0;
if (_channel_count == 0) {
copyReply(reply, reply_size, "Err - no I2C voltage sensors");
return false;
}
if (!_has_bucket) {
copyReply(reply, reply_size, "Err - I2C voltage history is empty");
return false;
}
if (populatedChannelCount() == 0) {
copyReply(reply, reply_size,
"Err - no connected I2C voltage channels");
return false;
}
const char* cursor = skipSpaces(args == NULL ? "" : args);
if (*cursor == 0) return formatStatusReply(reply, reply_size);
char token[12];
unsigned channel = 0;
unsigned page = 1;
if (!readToken(cursor, token, sizeof(token))
|| !parseUnsigned(token, channel) || channel > 255U) {
return formatUsage(reply, reply_size);
}
if (readToken(cursor, token, sizeof(token))) {
if (!parseUnsigned(token, page)) {
return formatUsage(reply, reply_size);
}
}
cursor = skipSpaces(cursor);
if (*cursor != 0) return formatUsage(reply, reply_size);
const int channel_index = findChannel((uint8_t)channel);
if (channel_index < 0
|| !channelIndexHasValue((uint8_t)channel_index)) {
snprintf(reply, reply_size,
"Err - I2C voltage channel %u has no readings", channel);
return false;
}
if (page < 1U || page > PAGE_COUNT) {
copyReply(reply, reply_size,
"Err - telemetry.volt.i2c page must be 1-4");
return false;
}
return formatPage((uint8_t)channel_index, (uint8_t)page,
reply, reply_size);
}
private:
class BitWriter {
uint8_t* _dest;
size_t _capacity;
size_t _bits;
public:
BitWriter(uint8_t* dest, size_t capacity)
: _dest(dest), _capacity(capacity), _bits(0) {
memset(dest, 0, capacity);
}
bool write(uint16_t value) {
if (_bits + SAMPLE_BITS > _capacity * 8U) return false;
for (int bit = SAMPLE_BITS - 1; bit >= 0; bit--) {
if ((value & ((uint16_t)1U << bit)) != 0) {
_dest[_bits / 8U] |=
(uint8_t)(1U << (7U - (_bits % 8U)));
}
_bits++;
}
return true;
}
};
uint8_t* _samples;
uint8_t _channels[MAX_CHANNELS];
uint8_t _channel_count;
uint16_t _next;
uint16_t _count;
uint32_t _last_bucket;
bool _has_bucket;
int findChannel(uint8_t channel) const {
for (uint8_t i = 0; i < _channel_count; i++) {
if (_channels[i] == channel) return i;
}
return -1;
}
bool channelIndexHasValue(uint8_t channel_index) const {
for (uint16_t offset = 0; offset < _count; offset++) {
if (codeAtOffset(channel_index, offset) != 0) return true;
}
return false;
}
size_t bitOffset(uint8_t channel_index, uint16_t sample_index) const {
return ((size_t)channel_index * RETENTION_SAMPLES + sample_index)
* SAMPLE_BITS;
}
void setCode(uint8_t channel_index, uint16_t sample_index,
uint16_t value) {
const size_t first_bit = bitOffset(channel_index, sample_index);
for (uint8_t bit = 0; bit < SAMPLE_BITS; bit++) {
const size_t absolute = first_bit + bit;
const uint8_t mask = (uint8_t)(1U << (7U - absolute % 8U));
if ((value & ((uint16_t)1U << (SAMPLE_BITS - 1U - bit))) != 0) {
_samples[absolute / 8U] |= mask;
} else {
_samples[absolute / 8U] &= (uint8_t)~mask;
}
}
}
uint16_t codeAt(uint8_t channel_index, uint16_t sample_index) const {
const size_t first_bit = bitOffset(channel_index, sample_index);
uint16_t value = 0;
for (uint8_t bit = 0; bit < SAMPLE_BITS; bit++) {
const size_t absolute = first_bit + bit;
value = (uint16_t)((value << 1U)
| ((_samples[absolute / 8U] >> (7U - absolute % 8U)) & 1U));
}
return value;
}
uint16_t codeAtOffset(uint8_t channel_index, uint16_t offset) const {
if (offset >= _count) return 0;
const uint16_t index = (uint16_t)(
(_next + RETENTION_SAMPLES - 1U - offset) % RETENTION_SAMPLES);
return codeAt(channel_index, index);
}
void append(const Reading readings[], uint8_t reading_count) {
if (readings == NULL) reading_count = 0;
for (uint8_t channel_index = 0;
channel_index < _channel_count; channel_index++) {
uint16_t code = 0;
for (uint8_t reading = 0; reading < reading_count; reading++) {
if (readings[reading].channel == _channels[channel_index]) {
code = encodeVoltage(readings[reading].voltage,
readings[reading].valid);
break;
}
}
setCode(channel_index, _next, code);
}
_next = (uint16_t)((_next + 1U) % RETENTION_SAMPLES);
if (_count < RETENTION_SAMPLES) _count++;
}
uint32_t startEpochForOffset(uint16_t oldest_offset) const {
const uint32_t start_bucket = _last_bucket >= oldest_offset
? _last_bucket - oldest_offset : 0;
return start_bucket * SAMPLE_INTERVAL_SECONDS;
}
bool formatPage(uint8_t channel_index, uint8_t page,
char* reply, size_t reply_size) const {
uint8_t payload[PAGE_HEADER_SIZE + PAGE_DATA_SIZE];
memset(payload, 0, sizeof(payload));
const uint16_t oldest_offset =
(uint16_t)(page * SAMPLES_PER_DAY - 1U);
payload[0] = PAGE_PAYLOAD_TYPE_V1;
putUint32LE(&payload[1], startEpochForOffset(oldest_offset));
payload[5] = (uint8_t)(SAMPLE_INTERVAL_SECONDS / 60U);
payload[6] = SAMPLES_PER_DAY;
payload[7] = _channels[channel_index];
BitWriter writer(&payload[PAGE_HEADER_SIZE], PAGE_DATA_SIZE);
for (uint8_t slot = 0; slot < SAMPLES_PER_DAY; slot++) {
writer.write(codeAtOffset(
channel_index, (uint16_t)(oldest_offset - slot)));
}
return formatBase64Reply(payload, sizeof(payload), reply, reply_size);
}
bool formatStatusReply(char* reply, size_t reply_size) const {
size_t used = (size_t)snprintf(reply, reply_size, "> channels=");
bool first = true;
for (uint8_t i = 0; i < _channel_count && used < reply_size; i++) {
if (!channelIndexHasValue(i)) continue;
const int written = snprintf(reply + used, reply_size - used,
"%s%u", first ? "" : ",",
(unsigned)_channels[i]);
if (written < 0) return false;
used += (size_t)written;
first = false;
}
if (used < reply_size) {
snprintf(reply + used, reply_size - used,
" samples=%u retention=4d step=0.02V max=655.34V",
(unsigned)_count);
}
return true;
}
static const char* skipSpaces(const char* value) {
while (*value == ' ') value++;
return value;
}
static bool readToken(const char*& cursor, char* token,
size_t token_size) {
cursor = skipSpaces(cursor);
if (*cursor == 0) return false;
size_t len = 0;
while (*cursor != 0 && *cursor != ' ') {
if (len + 1U >= token_size) {
while (*cursor != 0 && *cursor != ' ') cursor++;
token[0] = 0;
return true;
}
token[len++] = *cursor++;
}
token[len] = 0;
return true;
}
static bool parseUnsigned(const char* token, unsigned& value) {
if (token == NULL || *token == 0) return false;
unsigned parsed = 0;
while (*token != 0) {
if (*token < '0' || *token > '9' || parsed > 1000U) return false;
parsed = parsed * 10U + (unsigned)(*token++ - '0');
}
value = parsed;
return true;
}
static void copyReply(char* reply, size_t reply_size, const char* text) {
if (reply_size == 0) return;
snprintf(reply, reply_size, "%s", text);
}
static bool formatUsage(char* reply, size_t reply_size) {
copyReply(reply, reply_size,
"Err - use: get telemetry.volt.i2c <channel> [page]");
return false;
}
static void putUint32LE(uint8_t* dest, uint32_t value) {
dest[0] = (uint8_t)value;
dest[1] = (uint8_t)(value >> 8);
dest[2] = (uint8_t)(value >> 16);
dest[3] = (uint8_t)(value >> 24);
}
static void putUint16LE(uint8_t* dest, uint16_t value) {
dest[0] = (uint8_t)value;
dest[1] = (uint8_t)(value >> 8);
}
static size_t base64Encode(const uint8_t* source, size_t source_len,
char* dest, size_t dest_size) {
static const char alphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const size_t output_len = ((source_len + 2U) / 3U) * 4U;
if (dest_size <= output_len) return 0;
size_t in = 0;
size_t out = 0;
while (in < source_len) {
const size_t remaining = source_len - in;
const uint32_t a = source[in++];
const uint32_t b = remaining > 1U ? source[in++] : 0U;
const uint32_t c = remaining > 2U ? source[in++] : 0U;
const uint32_t value = (a << 16) | (b << 8) | c;
dest[out++] = alphabet[(value >> 18) & 0x3FU];
dest[out++] = alphabet[(value >> 12) & 0x3FU];
dest[out++] = remaining > 1U ? alphabet[(value >> 6) & 0x3FU] : '=';
dest[out++] = remaining > 2U ? alphabet[value & 0x3FU] : '=';
}
dest[out] = 0;
return out;
}
static bool formatBase64Reply(const uint8_t* payload, size_t payload_len,
char* reply, size_t reply_size) {
if (reply_size < 3U) return false;
reply[0] = '>';
reply[1] = ' ';
if (base64Encode(payload, payload_len, &reply[2],
reply_size - 2U) == 0) {
copyReply(reply, reply_size,
"Err - telemetry reply buffer too small");
return false;
}
return true;
}
};
} // namespace mesh
+18
View File
@@ -65,6 +65,12 @@ protected:
#endif
public:
struct VoltageSensorReading {
uint8_t channel;
float voltage;
bool valid;
};
double node_lat, node_lon; // modify these, if you want to affect Advert location
double node_altitude; // altitude in meters
bool powersaving_enabled; // powersaving mode
@@ -73,6 +79,18 @@ public:
virtual bool i2c_probe(TwoWire& wire, uint8_t addr) { return false; }
virtual bool begin() { return false; }
virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; }
virtual uint8_t getVoltageSensorChannels(uint8_t channels[],
uint8_t capacity) const {
(void)channels;
(void)capacity;
return 0;
}
virtual uint8_t queryVoltageSensors(VoltageSensorReading readings[],
uint8_t capacity) {
(void)readings;
(void)capacity;
return 0;
}
virtual void loop() { }
virtual void setTelemetryLocationAccessAvailable(bool available);
virtual int getNumSettings() const { return 0; }
@@ -421,6 +421,16 @@ static void query_ina3221(uint8_t ch, uint8_t sub_ch, CayenneLPP& lpp) {
}
}
}
static bool query_ina3221_voltage(uint8_t sub_ch, float& voltage) {
uint8_t seen = 0;
for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
if (!INA3221.isChannelEnabled(i)) continue;
if (seen++ != sub_ch) continue;
voltage = INA3221.getBusVoltage(i);
return isfinite(voltage);
}
return false;
}
#endif
#if ENV_INCLUDE_INA219
@@ -433,6 +443,10 @@ static void query_ina219(uint8_t ch, uint8_t, CayenneLPP& lpp) {
lpp.addCurrent(ch, INA219.getCurrent_mA() / 1000.0f);
lpp.addPower(ch, INA219.getPower_mW() / 1000.0f);
}
static bool query_ina219_voltage(uint8_t, float& voltage) {
voltage = INA219.getBusVoltage_V();
return INA219.success() && isfinite(voltage);
}
#endif
#if ENV_INCLUDE_INA260
@@ -444,6 +458,10 @@ static void query_ina260(uint8_t ch, uint8_t, CayenneLPP& lpp) {
lpp.addCurrent(ch, INA260.readCurrent() / 1000.0f);
lpp.addPower(ch, INA260.readPower() / 1000.0f);
}
static bool query_ina260_voltage(uint8_t, float& voltage) {
voltage = INA260.readBusVoltage() / 1000.0f;
return isfinite(voltage);
}
#endif
#if ENV_INCLUDE_INA226
@@ -458,6 +476,10 @@ static void query_ina226(uint8_t ch, uint8_t, CayenneLPP& lpp) {
lpp.addCurrent(ch, INA226.getCurrent_mA() / 1000.0f);
lpp.addPower(ch, INA226.getPower_mW() / 1000.0f);
}
static bool query_ina226_voltage(uint8_t, float& voltage) {
voltage = INA226.getBusVoltage();
return isfinite(voltage);
}
#endif
#if ENV_INCLUDE_MLX90614
@@ -600,64 +622,69 @@ struct SensorDef {
const char* name;
uint8_t (*init)(TwoWire* wire, uint8_t address);
void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
bool (*query_voltage)(uint8_t sub_channel, float& voltage);
};
#define TELEM_BOSCH_ALT_ADDR(addr) ((uint8_t)((addr) == 0x76 ? 0x77 : 0x76))
static const SensorDef SENSOR_TABLE[] = {
#if ENV_INCLUDE_AHTX0
{ TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0 },
{ TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0, NULL },
#endif
#ifdef ENV_INCLUDE_BME680
{ TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680", init_bme680, query_bme680 },
{ TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680, NULL },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680", init_bme680, query_bme680, NULL },
#endif
#if ENV_INCLUDE_BME680_BSEC
{ TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680+BSEC", init_bme680_bsec, query_bme680_bsec },
{ TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec, NULL },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680+BSEC", init_bme680_bsec, query_bme680_bsec, NULL },
#endif
#if ENV_INCLUDE_BME280
{ TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME280_ADDRESS), "BME280", init_bme280, query_bme280 },
{ TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280, NULL },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME280_ADDRESS), "BME280", init_bme280, query_bme280, NULL },
#endif
#if ENV_INCLUDE_BMP280
{ TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BMP280_ADDRESS), "BMP280", init_bmp280, query_bmp280 },
{ TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280, NULL },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BMP280_ADDRESS), "BMP280", init_bmp280, query_bmp280, NULL },
#endif
#if ENV_INCLUDE_SHTC3
{ 0x70, "SHTC3", init_shtc3, query_shtc3 },
{ 0x70, "SHTC3", init_shtc3, query_shtc3, NULL },
#endif
#if ENV_INCLUDE_SHT4X
{ TELEM_SHT4X_ADDRESS, "SHT4X", init_sht4x, query_sht4x },
{ TELEM_SHT4X_ADDRESS, "SHT4X", init_sht4x, query_sht4x, NULL },
#endif
#if ENV_INCLUDE_LPS22HB
{ 0x5C, "LPS22HB", init_lps22hb, query_lps22hb },
{ 0x5C, "LPS22HB", init_lps22hb, query_lps22hb, NULL },
#endif
#if ENV_INCLUDE_INA3221
{ TELEM_INA3221_ADDRESS, "INA3221", init_ina3221, query_ina3221 },
{ TELEM_INA3221_ADDRESS, "INA3221", init_ina3221, query_ina3221,
query_ina3221_voltage },
#endif
#if ENV_INCLUDE_INA219
{ TELEM_INA219_ADDRESS, "INA219", init_ina219, query_ina219 },
{ TELEM_INA219_ADDRESS, "INA219", init_ina219, query_ina219,
query_ina219_voltage },
#endif
#if ENV_INCLUDE_INA260
{ TELEM_INA260_ADDRESS, "INA260", init_ina260, query_ina260 },
{ TELEM_INA260_ADDRESS, "INA260", init_ina260, query_ina260,
query_ina260_voltage },
#endif
#if ENV_INCLUDE_INA226
{ TELEM_INA226_ADDRESS, "INA226", init_ina226, query_ina226 },
{ TELEM_INA226_ADDRESS, "INA226", init_ina226, query_ina226,
query_ina226_voltage },
#endif
#if ENV_INCLUDE_MLX90614
{ TELEM_MLX90614_ADDRESS,"MLX90614", init_mlx90614, query_mlx90614 },
{ TELEM_MLX90614_ADDRESS,"MLX90614", init_mlx90614, query_mlx90614, NULL },
#endif
#if ENV_INCLUDE_VL53L0X
{ TELEM_VL53L0X_ADDRESS, "VL53L0X", init_vl53l0x, query_vl53l0x },
{ TELEM_VL53L0X_ADDRESS, "VL53L0X", init_vl53l0x, query_vl53l0x, NULL },
#endif
#ifdef ENV_INCLUDE_BMP085
{ 0x77, "BMP085", init_bmp085, query_bmp085 },
{ 0x77, "BMP085", init_bmp085, query_bmp085, NULL },
#endif
#if ENV_INCLUDE_RAK12035
{ TELEM_RAK12035_ADDRESS,"RAK12035", init_rak12035, query_rak12035 },
{ TELEM_RAK12035_ADDRESS,"RAK12035", init_rak12035, query_rak12035, NULL },
#endif
{ 0, nullptr, nullptr, nullptr } // sentinel - keeps the array non-empty
{ 0, nullptr, nullptr, nullptr, nullptr } // sentinel keeps array non-empty
};
#undef TELEM_BOSCH_ALT_ADDR
@@ -722,7 +749,9 @@ bool EnvironmentSensorManager::begin() {
MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, def.address);
detected[def.address] = false; // consumed; later entries must not re-claim this device
for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) {
_active_sensors[_active_sensor_count++] = { def.query, sub };
_active_sensors[_active_sensor_count++] = {
def.query, def.query_voltage, sub
};
}
}
@@ -752,7 +781,34 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
return true;
}
uint8_t EnvironmentSensorManager::getVoltageSensorChannels(
uint8_t channels[], uint8_t capacity) const {
uint8_t count = 0;
for (int i = 0; i < _active_sensor_count; i++) {
if (_active_sensors[i].query_voltage == NULL) continue;
if (count < capacity && channels != NULL) {
channels[count] = (uint8_t)(TELEM_CHANNEL_SELF + 1 + i);
}
count++;
}
return count < capacity ? count : capacity;
}
uint8_t EnvironmentSensorManager::queryVoltageSensors(
VoltageSensorReading readings[], uint8_t capacity) {
uint8_t count = 0;
for (int i = 0; i < _active_sensor_count && count < capacity; i++) {
const ActiveSensor& sensor = _active_sensors[i];
if (sensor.query_voltage == NULL) continue;
VoltageSensorReading& reading = readings[count++];
reading.channel = (uint8_t)(TELEM_CHANNEL_SELF + 1 + i);
reading.voltage = 0.0f;
reading.valid = sensor.query_voltage(sensor.sub_channel,
reading.voltage)
&& isfinite(reading.voltage);
}
return count;
}
int EnvironmentSensorManager::getNumSettings() const {
int settings = 0;
#if ENV_INCLUDE_GPS
@@ -12,6 +12,7 @@ protected:
// Sub-channel is 0 for all single-output sensors.
struct ActiveSensor {
void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
bool (*query_voltage)(uint8_t sub_channel, float& voltage);
uint8_t sub_channel;
};
@@ -48,6 +49,10 @@ public:
bool i2c_probe(TwoWire& wire, uint8_t addr) override;
bool begin() override;
bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
uint8_t getVoltageSensorChannels(uint8_t channels[],
uint8_t capacity) const override;
uint8_t queryVoltageSensors(VoltageSensorReading readings[],
uint8_t capacity) override;
#if ENV_INCLUDE_GPS || defined(ENV_INCLUDE_BME680_BSEC)
void loop() override;
#endif