mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-21 13:55:24 -04:00
Merge branch 'develop' into claude/meshtasticd-ble-raspberry-pi-ypab2m
This commit is contained in:
14 files changed
+459
-128
No files matched your search
@@ -88,6 +88,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_PREHOP_DROP 1
|
||||
#endif
|
||||
|
||||
// Use polynomial approximations for trigonometric functions to save flash.
|
||||
// Override with -D MESHTASTIC_TRIG_APPROX=0 for exact trig for special use cases e.g. close to Earth's poles.
|
||||
#ifndef MESHTASTIC_TRIG_APPROX
|
||||
#define MESHTASTIC_TRIG_APPROX 1
|
||||
#endif
|
||||
|
||||
// Debug/test only: let a wired client (serial/TCP) inject frames into the RX pipeline as if they had
|
||||
// arrived over LoRa - a SIMULATOR_APP ToRadio packet is delivered through the real receive path on real
|
||||
// hardware (see MeshService::injectAsReceived). This forges over-the-air traffic, so it MUST stay 0 in
|
||||
|
||||
+1
-5
@@ -1389,11 +1389,7 @@ void GPS::down()
|
||||
#endif
|
||||
|
||||
if (softsleepSupported) {
|
||||
// How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than
|
||||
// GPS_SOFTSLEEP? Heuristic equation. A compromise manually fitted to power observations from U-blox NEO-6M
|
||||
// and M10050 https://www.desmos.com/calculator/6gvjghoumr This is not particularly accurate, but probably an
|
||||
// improvement over a single, fixed threshold
|
||||
uint32_t hardsleepThreshold = (2750 * pow(predictedSearchDuration / 1000, 1.22));
|
||||
uint32_t hardsleepThreshold = gpsHardsleepThresholdMs(predictedSearchDuration / 1000);
|
||||
LOG_DEBUG("gps_update_interval >= %us needed for hardsleep", hardsleepThreshold / 1000);
|
||||
|
||||
// If update interval too short: softsleep (if supported by hardware)
|
||||
|
||||
@@ -2,6 +2,31 @@
|
||||
|
||||
#include "Default.h"
|
||||
|
||||
// Sampled from the original `2750 * seconds^1.22` curve. Interpolation tracks it within 0.6% for
|
||||
// inputs >=10s and 1.7% below that; the 1s/2s/3s points keep the convex first segment from
|
||||
// overshooting (a 0s-to-5s chord reads 42% high at 1s).
|
||||
static constexpr uint32_t kThresholdCurveSecs[] = {0, 1, 2, 3, 5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 240, 300, 450, 600, 900};
|
||||
static constexpr uint32_t kThresholdCurveMs[] = {0, 2750, 6406, 10506, 19592, 45639, 74845,
|
||||
106314, 174350, 285925, 406141, 666053, 946093, 1551548,
|
||||
2203893, 2893481, 4745172, 6740269, 11053722};
|
||||
static constexpr size_t kThresholdCurvePoints = sizeof(kThresholdCurveSecs) / sizeof(kThresholdCurveSecs[0]);
|
||||
|
||||
// How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than
|
||||
// GPS_SOFTSLEEP? Avoids pow() so this heuristic doesn't pull double-precision libm into the image.
|
||||
uint32_t gpsHardsleepThresholdMs(uint32_t predictedSearchSecs)
|
||||
{
|
||||
if (predictedSearchSecs >= kThresholdCurveSecs[kThresholdCurvePoints - 1])
|
||||
return kThresholdCurveMs[kThresholdCurvePoints - 1];
|
||||
|
||||
size_t i = 1;
|
||||
while (kThresholdCurveSecs[i] < predictedSearchSecs)
|
||||
i++;
|
||||
|
||||
uint32_t x0 = kThresholdCurveSecs[i - 1], x1 = kThresholdCurveSecs[i];
|
||||
uint32_t y0 = kThresholdCurveMs[i - 1], y1 = kThresholdCurveMs[i];
|
||||
return y0 + (uint32_t)((uint64_t)(y1 - y0) * (predictedSearchSecs - x0) / (x1 - x0));
|
||||
}
|
||||
|
||||
// Mark the time when searching for GPS position begins
|
||||
void GPSUpdateScheduling::informSearching()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
// Approximates the GPS_HARDSLEEP/GPS_SOFTSLEEP crossover curve without pow(); see .cpp for the
|
||||
// sampled reference values it interpolates between.
|
||||
uint32_t gpsHardsleepThresholdMs(uint32_t predictedSearchSecs);
|
||||
|
||||
// Encapsulates code responsible for the timing of GPS updates
|
||||
class GPSUpdateScheduling
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "GeoCoord.h"
|
||||
#include "configuration.h"
|
||||
#include <cmath>
|
||||
|
||||
// Narrow a UTM meter value to its unsigned field, clamping non-finite/negative/oversized inputs: an
|
||||
@@ -433,6 +434,43 @@ void GeoCoord::convertWGS84ToOSGB36(const double lat, const double lon, double &
|
||||
//(airyA*airyA/(airyA / sqrt(1 - airyEcc*sin(osgb.latitude)*sin(osgb.latitude)))); // Not used, no OSTN data
|
||||
}
|
||||
|
||||
#if MESHTASTIC_TRIG_APPROX
|
||||
// cos(x) minimax approx for x in [-pi/2, pi/2] ("cos_52"): https://www.ganssle.com/approx.htm
|
||||
static double cosLatitudeApprox(double latRad)
|
||||
{
|
||||
constexpr double c1 = 0.9999932946, c2 = -0.4999124376, c3 = 0.0414877472, c4 = -0.0012712095;
|
||||
double x2 = latRad * latRad;
|
||||
return c1 + x2 * (c2 + x2 * (c3 + c4 * x2));
|
||||
}
|
||||
|
||||
/// Approximate distance in meters via equirectangular projection (not exact spherical trig).
|
||||
/// <1% error to ~500km, degrading near the poles at long range (see test_geocoord_distance).
|
||||
float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b)
|
||||
{
|
||||
// Don't do math if the points are the same
|
||||
if (lat_a == lat_b && lng_a == lng_b)
|
||||
return 0.0;
|
||||
|
||||
double a1 = lat_a / DEG_CONVERT;
|
||||
double a2 = lng_a / DEG_CONVERT;
|
||||
double b1 = lat_b / DEG_CONVERT;
|
||||
double b2 = lng_b / DEG_CONVERT;
|
||||
|
||||
double meanLat = (a1 + b1) / 2;
|
||||
double dLng = b2 - a2;
|
||||
// Wrap to [-PI, PI]: unlike cos()/sin(), a raw longitude difference doesn't handle points that
|
||||
// straddle the antimeridian (e.g. 179.9 and -179.9 are ~0.2 degrees apart, not ~360).
|
||||
if (dLng > PI)
|
||||
dLng -= 2 * PI;
|
||||
else if (dLng < -PI)
|
||||
dLng += 2 * PI;
|
||||
double x = dLng * cosLatitudeApprox(meanLat);
|
||||
double y = b1 - a1;
|
||||
double tt = sqrt(x * x + y * y);
|
||||
|
||||
return (float)(6366000 * tt);
|
||||
}
|
||||
#else
|
||||
/// Ported from my old java code, returns distance in meters along the globe
|
||||
/// surface (by Haversine formula)
|
||||
float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b)
|
||||
@@ -456,6 +494,7 @@ float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double
|
||||
|
||||
return (float)(6366000 * tt);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Computes the bearing in degrees between two points on Earth. Ported from my
|
||||
|
||||
+11
-1
@@ -991,7 +991,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
if (shouldPreserveKey) {
|
||||
config.security.private_key.size = 32;
|
||||
memcpy(config.security.private_key.bytes, private_key_temp, config.security.private_key.size);
|
||||
printBytes("Restored key", config.security.private_key.bytes, config.security.private_key.size);
|
||||
// Never log the key bytes: debug logs get pasted into public bug reports.
|
||||
LOG_DEBUG("Restored preserved private key");
|
||||
} else {
|
||||
config.security.private_key.size = 0;
|
||||
}
|
||||
@@ -3488,7 +3489,16 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
||||
}
|
||||
}
|
||||
info->num = contact.node_num;
|
||||
// CopyUserToNodeInfoLite assigns public_key unconditionally, and clients send add_contact before every
|
||||
// DM - often from an entry that carries no key at all. A contact may still supply or update a full
|
||||
// 32-byte key (that's what add_contact is for), but it must never *erase* a key we already hold, which
|
||||
// would be persisted below and break subsequent DMs with PKI_SEND_FAIL_PUBLIC_KEY.
|
||||
const meshtastic_NodeInfoLite_public_key_t storedKey = info->public_key;
|
||||
TypeConversions::CopyUserToNodeInfoLite(info, contact.user);
|
||||
if (storedKey.size == 32 && info->public_key.size != 32) {
|
||||
LOG_INFO("Contact 0x%08x has no key, keep the stored one", contact.node_num);
|
||||
info->public_key = storedKey;
|
||||
}
|
||||
if (contact.should_ignore) {
|
||||
// Block the contact and drop its rich satellite data, but keep the
|
||||
// public key copied above - an ignored peer keeps a usable identity
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
// FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in
|
||||
// RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, receive_queue[0]))
|
||||
#ifndef MAX_RX_TOPHONE
|
||||
#if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3))
|
||||
#if defined(ARCH_STM32WL) || (defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)))
|
||||
#define MAX_RX_TOPHONE 8
|
||||
#elif defined(NRF52840_XXAA)
|
||||
// Each slot is a ~340 B MeshPacket in the static pool (Router.cpp MAX_PACKETS_STATIC), so 32 slots
|
||||
@@ -34,10 +34,8 @@
|
||||
// the 8 classic ESP32 has shipped with for years; drops start when a stalled phone/serial client has
|
||||
// 16 packets queued.
|
||||
#define MAX_RX_TOPHONE 16
|
||||
#elif MESHTASTIC_MEM_CLASS >= MEM_CLASS_MEDIUM || defined(ARCH_RP2040) || defined(CONFIG_IDF_TARGET_ESP32C3) || \
|
||||
defined(ARCH_STM32WL)
|
||||
// RP2040/RP2350, ESP32-C3 and STM32WL keep their historical 32 (no field pressure to cut them;
|
||||
// STM32WL's pool is dynamic, so the constant only bounds in-flight packets there).
|
||||
#elif MESHTASTIC_MEM_CLASS >= MEM_CLASS_MEDIUM || defined(ARCH_RP2040) || defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
// RP2040/RP2350 and ESP32-C3 keep their historical 32.
|
||||
#define MAX_RX_TOPHONE 32
|
||||
#else
|
||||
#define MAX_RX_TOPHONE 16 // unclassified small parts: fail safe-small
|
||||
@@ -131,8 +129,12 @@ static inline int get_max_num_nodes()
|
||||
/// full mesh, floored at 100. Shared by PacketHistory's constructor clamp and
|
||||
/// the boot-cache budget assert below so the two cannot drift.
|
||||
#ifndef PACKETHISTORY_MAX
|
||||
#if defined(ARCH_STM32WL)
|
||||
#define PACKETHISTORY_MAX (MAX_NUM_NODES * 2) // 20 entries for 10-node STM32WL
|
||||
#else
|
||||
#define PACKETHISTORY_MAX (MAX_NUM_NODES * 2 > 100 ? (uint32_t)(MAX_NUM_NODES * 2) : (uint32_t)100)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// Per-map cap (position/telemetry/environment/status): only the freshest
|
||||
/// MAX_SATELLITE_NODES nodes keep satellite payloads, the rest just the
|
||||
|
||||
@@ -22,16 +22,18 @@ bool SEN5XSensor::getVersion()
|
||||
}
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
uint8_t versionBuffer[12]{};
|
||||
size_t charNumber = readBuffer(&versionBuffer[0], 3);
|
||||
if (charNumber == 0) {
|
||||
LOG_ERROR("%s: Error getting data ready flag value", sensorName);
|
||||
// Version reply layout: fw major/minor, fw debug, hw major/minor,
|
||||
// protocol major/minor, padding
|
||||
uint8_t versionBuffer[SEN5X_VERSION_BUFFER_SIZE]{};
|
||||
size_t charNumber = readBuffer(&versionBuffer[0], SEN5X_VERSION_BUFFER_SIZE + (SEN5X_VERSION_BUFFER_SIZE / 2));
|
||||
if (charNumber < SEN5X_VERSION_BUFFER_SIZE) {
|
||||
LOG_ERROR("%s: Error getting device version value", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
firmwareVer = versionBuffer[0] + (versionBuffer[1] / 10);
|
||||
hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10);
|
||||
protocolVer = versionBuffer[5] + (versionBuffer[6] / 10);
|
||||
firmwareVer = versionBuffer[0] + (versionBuffer[1] / 10.0f);
|
||||
hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10.0f);
|
||||
protocolVer = versionBuffer[5] + (versionBuffer[6] / 10.0f);
|
||||
|
||||
LOG_INFO("%s: Firmware Version: %0.2f", sensorName, firmwareVer);
|
||||
LOG_INFO("%s: Hardware Version: %0.2f", sensorName, hardwareVer);
|
||||
@@ -48,12 +50,11 @@ bool SEN5XSensor::findModel()
|
||||
}
|
||||
delay(50); // From Sensirion Datasheet
|
||||
|
||||
const uint8_t nameSize = 48;
|
||||
uint8_t name[nameSize];
|
||||
size_t charNumber = readBuffer(&name[0], nameSize);
|
||||
uint8_t name[SEN5X_PRODUCT_NAME_BUFFER_SIZE]{};
|
||||
size_t charNumber = readBuffer(&name[0], SEN5X_PRODUCT_NAME_BUFFER_SIZE + (SEN5X_PRODUCT_NAME_BUFFER_SIZE / 2));
|
||||
bool foundModel = false;
|
||||
|
||||
if (charNumber == 0) {
|
||||
if (charNumber < SEN5X_PRODUCT_NAME_BUFFER_SIZE) {
|
||||
LOG_ERROR("%s: Error getting device name", sensorName);
|
||||
return foundModel;
|
||||
}
|
||||
@@ -361,15 +362,18 @@ bool SEN5XSensor::vocStateFromSensor()
|
||||
|
||||
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));
|
||||
// Retrieve the data into a staging buffer so a partial read (e.g. a CRC
|
||||
// failure halfway through) cannot corrupt the current vocState.
|
||||
// The requested size accounts for the CRC bytes
|
||||
uint8_t stateBuffer[SEN5X_VOC_STATE_BUFFER_SIZE]{};
|
||||
size_t receivedNumber = readBuffer(&stateBuffer[0], SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2));
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
if (receivedNumber == 0) {
|
||||
if (receivedNumber < SEN5X_VOC_STATE_BUFFER_SIZE) {
|
||||
LOG_DEBUG("%s: Error getting VOC's state", sensorName);
|
||||
return false;
|
||||
}
|
||||
memcpy(vocState, stateBuffer, SEN5X_VOC_STATE_BUFFER_SIZE);
|
||||
|
||||
// Print the state (if debug is on)
|
||||
LOG_DEBUG("%s: VOC state from sensor: [%u, %u, %u, %u, %u, %u, %u, %u]", sensorName, vocState[0], vocState[1], vocState[2],
|
||||
@@ -427,6 +431,7 @@ bool SEN5XSensor::loadState()
|
||||
return okay;
|
||||
#else
|
||||
LOG_ERROR("%s: Filesystem not implemented", sensorName);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -472,6 +477,7 @@ bool SEN5XSensor::saveState()
|
||||
return okay;
|
||||
#else
|
||||
LOG_ERROR("%s: Filesystem not implemented", sensorName);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -497,8 +503,7 @@ uint32_t SEN5XSensor::wakeUp()
|
||||
// keep track of how long it has passed
|
||||
pmMeasureStarted = getTime();
|
||||
state = SEN5X_MEASUREMENT;
|
||||
if (state == SEN5X_MEASUREMENT)
|
||||
LOG_INFO("%s: Started measurement mode", sensorName);
|
||||
LOG_INFO("%s: Started measurement mode", sensorName);
|
||||
return SEN5X_WARMUP_MS_1;
|
||||
}
|
||||
|
||||
@@ -533,7 +538,7 @@ bool SEN5XSensor::startCleaning()
|
||||
// This message will be always printed so the user knows the device it's not hung
|
||||
LOG_INFO("%s: Started fan cleaning (10 sec)", sensorName);
|
||||
|
||||
uint16_t started = millis();
|
||||
uint32_t started = millis();
|
||||
while (millis() - started < 10500) {
|
||||
delay(500);
|
||||
}
|
||||
@@ -658,9 +663,9 @@ bool SEN5XSensor::readValues()
|
||||
LOG_TRACE("%s: Reading PM Values", sensorName);
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
uint8_t dataBuffer[16]{};
|
||||
size_t receivedNumber = readBuffer(&dataBuffer[0], 24);
|
||||
if (receivedNumber == 0) {
|
||||
uint8_t dataBuffer[SEN5X_READ_VALUES_BUFFER_SIZE]{};
|
||||
size_t receivedNumber = readBuffer(&dataBuffer[0], SEN5X_READ_VALUES_BUFFER_SIZE + (SEN5X_READ_VALUES_BUFFER_SIZE / 2));
|
||||
if (receivedNumber < SEN5X_READ_VALUES_BUFFER_SIZE) {
|
||||
LOG_ERROR("%s: Error getting values", sensorName);
|
||||
return false;
|
||||
}
|
||||
@@ -676,15 +681,17 @@ bool SEN5XSensor::readValues()
|
||||
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;
|
||||
// Convert values based on Sensirion Arduino lib.
|
||||
// Map values the sensor reports as unavailable (SEN5X_UINT_INVALID /
|
||||
// SEN5X_INT_INVALID) to the sentinels getMetrics() checks for
|
||||
sen5xmeasurement.pM1p0 = (uint_pM1p0 != SEN5X_UINT_INVALID) ? (uint_pM1p0 / 10) : UINT16_MAX;
|
||||
sen5xmeasurement.pM2p5 = (uint_pM2p5 != SEN5X_UINT_INVALID) ? (uint_pM2p5 / 10) : UINT16_MAX;
|
||||
sen5xmeasurement.pM4p0 = (uint_pM4p0 != SEN5X_UINT_INVALID) ? (uint_pM4p0 / 10) : UINT16_MAX;
|
||||
sen5xmeasurement.pM10p0 = (uint_pM10p0 != SEN5X_UINT_INVALID) ? (uint_pM10p0 / 10) : UINT16_MAX;
|
||||
sen5xmeasurement.humidity = (int_humidity != SEN5X_INT_INVALID) ? (int_humidity / 100.0f) : FLT_MAX;
|
||||
sen5xmeasurement.temperature = (int_temperature != SEN5X_INT_INVALID) ? (int_temperature / 200.0f) : FLT_MAX;
|
||||
sen5xmeasurement.vocIndex = (int_vocIndex != SEN5X_INT_INVALID) ? (int_vocIndex / 10.0f) : FLT_MAX;
|
||||
sen5xmeasurement.noxIndex = (int_noxIndex != SEN5X_INT_INVALID) ? (int_noxIndex / 10.0f) : FLT_MAX;
|
||||
|
||||
LOG_TRACE("%s: Got readings: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sensorName, sen5xmeasurement.pM1p0,
|
||||
sen5xmeasurement.pM2p5, sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0);
|
||||
@@ -711,9 +718,9 @@ bool SEN5XSensor::readPNValues(bool cumulative)
|
||||
LOG_TRACE("%s: Reading PN Values", sensorName);
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
uint8_t dataBuffer[20]{};
|
||||
size_t receivedNumber = readBuffer(&dataBuffer[0], 30);
|
||||
if (receivedNumber == 0) {
|
||||
uint8_t dataBuffer[SEN5X_READ_PM_BUFFER_SIZE]{};
|
||||
size_t receivedNumber = readBuffer(&dataBuffer[0], SEN5X_READ_PM_BUFFER_SIZE + (SEN5X_READ_PM_BUFFER_SIZE / 2));
|
||||
if (receivedNumber < SEN5X_READ_PM_BUFFER_SIZE) {
|
||||
LOG_ERROR("%s: Error getting PN values", sensorName);
|
||||
return false;
|
||||
}
|
||||
@@ -730,22 +737,29 @@ bool SEN5XSensor::readPNValues(bool cumulative)
|
||||
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;
|
||||
// Convert values based on Sensirion Arduino lib.
|
||||
// Raw PN values are #/cm3 with 0.1 resolution; multiplying by 10
|
||||
// converts to #/0.1l without the truncation of dividing first.
|
||||
// Map values the sensor reports as unavailable (SEN5X_UINT_INVALID) to the
|
||||
// sentinels getMetrics() checks for
|
||||
sen5xmeasurement.pN0p5 = (uint_pN0p5 != SEN5X_UINT_INVALID) ? ((uint32_t)uint_pN0p5 * 10) : UINT32_MAX;
|
||||
sen5xmeasurement.pN1p0 = (uint_pN1p0 != SEN5X_UINT_INVALID) ? ((uint32_t)uint_pN1p0 * 10) : UINT32_MAX;
|
||||
sen5xmeasurement.pN2p5 = (uint_pN2p5 != SEN5X_UINT_INVALID) ? ((uint32_t)uint_pN2p5 * 10) : UINT32_MAX;
|
||||
sen5xmeasurement.pN4p0 = (uint_pN4p0 != SEN5X_UINT_INVALID) ? ((uint32_t)uint_pN4p0 * 10) : UINT32_MAX;
|
||||
sen5xmeasurement.pN10p0 = (uint_pN10p0 != SEN5X_UINT_INVALID) ? ((uint32_t)uint_pN10p0 * 10) : UINT32_MAX;
|
||||
sen5xmeasurement.tSize = (uint_tSize != SEN5X_UINT_INVALID) ? (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;
|
||||
if (sen5xmeasurement.pN10p0 != UINT32_MAX && sen5xmeasurement.pN4p0 != UINT32_MAX)
|
||||
sen5xmeasurement.pN10p0 -= sen5xmeasurement.pN4p0;
|
||||
if (sen5xmeasurement.pN4p0 != UINT32_MAX && sen5xmeasurement.pN2p5 != UINT32_MAX)
|
||||
sen5xmeasurement.pN4p0 -= sen5xmeasurement.pN2p5;
|
||||
if (sen5xmeasurement.pN2p5 != UINT32_MAX && sen5xmeasurement.pN1p0 != UINT32_MAX)
|
||||
sen5xmeasurement.pN2p5 -= sen5xmeasurement.pN1p0;
|
||||
if (sen5xmeasurement.pN1p0 != UINT32_MAX && sen5xmeasurement.pN0p5 != UINT32_MAX)
|
||||
sen5xmeasurement.pN1p0 -= sen5xmeasurement.pN0p5;
|
||||
}
|
||||
|
||||
LOG_TRACE("%s: Got readings: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sensorName,
|
||||
@@ -767,10 +781,10 @@ uint8_t SEN5XSensor::getMeasurements()
|
||||
}
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
uint8_t dataReadyBuffer[3];
|
||||
size_t charNumber = readBuffer(&dataReadyBuffer[0], 3);
|
||||
if (charNumber == 0) {
|
||||
LOG_ERROR("%s: Error getting device version value", sensorName);
|
||||
uint8_t dataReadyBuffer[SEN5X_DATA_READY_BUFFER_SIZE]{};
|
||||
size_t charNumber = readBuffer(&dataReadyBuffer[0], SEN5X_DATA_READY_BUFFER_SIZE + (SEN5X_DATA_READY_BUFFER_SIZE / 2));
|
||||
if (charNumber < SEN5X_DATA_READY_BUFFER_SIZE) {
|
||||
LOG_ERROR("%s: Error getting data ready flag value", sensorName);
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -909,7 +923,7 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
measurement->variant.air_quality_metrics.has_pm_temperature = true;
|
||||
measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature;
|
||||
}
|
||||
if (sen5xmeasurement.noxIndex != FLT_MAX) {
|
||||
if (sen5xmeasurement.vocIndex != FLT_MAX) {
|
||||
measurement->variant.air_quality_metrics.has_pm_voc_idx = true;
|
||||
measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,18 @@ class SEN5XSensor : public TelemetrySensor
|
||||
#define SEN5X_READ_RAW_VALUES 0x03D2
|
||||
#define SEN5X_READ_PM_VALUES 0x0413
|
||||
|
||||
// Values the sensor reports when a reading is unavailable
|
||||
#define SEN5X_UINT_INVALID 0xFFFF
|
||||
#define SEN5X_INT_INVALID 0x7FFF
|
||||
|
||||
// Reply payload sizes in data bytes; the raw I2C transfer adds one CRC byte
|
||||
// per 2-byte group, so requests are <size> + <size> / 2 raw bytes
|
||||
#define SEN5X_VERSION_BUFFER_SIZE 8
|
||||
#define SEN5X_PRODUCT_NAME_BUFFER_SIZE 32
|
||||
#define SEN5X_DATA_READY_BUFFER_SIZE 2
|
||||
#define SEN5X_READ_VALUES_BUFFER_SIZE 16
|
||||
#define SEN5X_READ_PM_BUFFER_SIZE 20
|
||||
|
||||
#define SEN5X_VOC_VALID_TIME 600
|
||||
#define SEN5X_VOC_VALID_DATE 1514764800
|
||||
|
||||
@@ -114,8 +126,23 @@ See: https://sensirion.com/resource/application_note/low_power_mode/sen5x
|
||||
#define SEN5X_PN4P0_CONC_THD 100
|
||||
|
||||
bool sendCommand(uint16_t command);
|
||||
/**
|
||||
* @brief Send a command word followed by a data payload; a CRC byte is
|
||||
* computed and inserted on the wire after every 2-byte pair.
|
||||
* @param command 16-bit command code, sent big-endian
|
||||
* @param buffer payload data bytes, without CRCs
|
||||
* @param byteNumber payload size in data bytes; must be even
|
||||
* @return true when the full transfer is written and acknowledged
|
||||
*/
|
||||
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
|
||||
/**
|
||||
* @brief Read a reply, verifying and stripping the interleaved CRC bytes.
|
||||
* @param buffer destination for the data bytes (byteNumber * 2 / 3 of them)
|
||||
* @param byteNumber raw transfer size including CRCs; must be a multiple
|
||||
* of 3 (2 data bytes + 1 CRC per group)
|
||||
* @return the number of data bytes written to buffer, or 0 on any error
|
||||
*/
|
||||
uint8_t readBuffer(uint8_t *buffer, uint8_t byteNumber);
|
||||
uint8_t sen5xCRC(const uint8_t *buffer);
|
||||
bool startCleaning();
|
||||
uint8_t getMeasurements();
|
||||
|
||||
@@ -18,20 +18,9 @@ static bool stm32wlRtcValid = false;
|
||||
#endif
|
||||
|
||||
// ─── Bootloader redirect ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Why .noinit + constructor instead of TAMP backup registers:
|
||||
//
|
||||
// The STM32duino startup sequence initialises clocks which may call
|
||||
// __HAL_RCC_BACKUPRESET_FORCE/RELEASE when configuring the LSE oscillator,
|
||||
// wiping the entire backup domain (including TAMP->BKP0R) before setup()
|
||||
// ever runs. The backup-register approach therefore cannot reliably survive
|
||||
// a soft reset in this toolchain.
|
||||
//
|
||||
// Solution: store the magic in a .noinit SRAM variable.
|
||||
// - NVIC_SystemReset() does NOT clear SRAM.
|
||||
// - The linker script skips zero-init for .noinit sections.
|
||||
// - __attribute__((constructor)) fires before main()/HAL_Init(), so we can
|
||||
// intercept and jump before anything disturbs peripheral state.
|
||||
// Uses .noinit SRAM instead of TAMP backup registers: STM32duino's clock init can wipe the
|
||||
// backup domain via __HAL_RCC_BACKUPRESET_FORCE/RELEASE before setup() runs, but .noinit
|
||||
// survives NVIC_SystemReset() and this constructor fires before HAL_Init() touches anything.
|
||||
|
||||
#define BOOTLOADER_MAGIC 0xD00DB007UL
|
||||
#define SYS_MEM_BASE 0x1FFF0000UL
|
||||
@@ -58,8 +47,10 @@ __attribute__((constructor(101), used)) static void earlyBootCheck(void)
|
||||
SCB->VTOR = SYS_MEM_BASE;
|
||||
__set_MSP(*(volatile uint32_t *)SYS_MEM_BASE);
|
||||
((void (*)(void))(*(volatile uint32_t *)(SYS_MEM_BASE + 4)))();
|
||||
while (1)
|
||||
;
|
||||
// Should never be reached: the bootloader ROM does not return. A bare reset
|
||||
// (rather than returning normally) avoids unwinding through this function's
|
||||
// epilogue, which would restore registers relative to the now-repointed MSP.
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
|
||||
void enterDfuMode()
|
||||
@@ -169,15 +160,7 @@ void cpuDeepSleep(uint32_t msecToWake)
|
||||
#endif
|
||||
}
|
||||
|
||||
// Hacks to force more code and data out.
|
||||
|
||||
// By default __assert_func uses fiprintf which pulls in stdio.
|
||||
extern "C" void __wrap___assert_func(const char *, int, const char *, const char *)
|
||||
{
|
||||
while (true)
|
||||
;
|
||||
return;
|
||||
}
|
||||
// ─── Linker hacks to reduce code size ─────────────────────────────────────────
|
||||
|
||||
// By default strerror has a lot of strings we probably don't use. Make it return an empty string instead.
|
||||
char empty = 0;
|
||||
@@ -197,6 +180,8 @@ extern "C" void __wrap__tzset_unlocked_r(struct _reent *reent_ptr)
|
||||
}
|
||||
#endif
|
||||
|
||||
// ─── Fault handling & recovery ────────────────────────────────────────────────
|
||||
|
||||
// Taken from https://interrupt.memfault.com/blog/cortex-m-hardfault-debug
|
||||
typedef struct __attribute__((packed)) ContextStateFrame {
|
||||
uint32_t r0;
|
||||
@@ -233,32 +218,11 @@ static void debug_printf(const char *format, ...)
|
||||
uart_debug_write((uint8_t *)hardfault_message_buffer, min((unsigned int)length, sizeof(hardfault_message_buffer) - 1));
|
||||
}
|
||||
|
||||
// N picked by guessing
|
||||
#define DOT_TIME 1200000
|
||||
static void dot()
|
||||
// By default __assert_func uses fiprintf which pulls in stdio.
|
||||
extern "C" void __wrap___assert_func(const char *file, int line, const char *func, const char *failedexpr)
|
||||
{
|
||||
digitalWrite(LED_POWER, LED_STATE_ON);
|
||||
for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */
|
||||
}
|
||||
digitalWrite(LED_POWER, LED_STATE_OFF);
|
||||
for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */
|
||||
}
|
||||
}
|
||||
|
||||
static void dash()
|
||||
{
|
||||
digitalWrite(LED_POWER, LED_STATE_ON);
|
||||
for (volatile int i = 0; i < (DOT_TIME * 3); i++) { /* busy wait */
|
||||
}
|
||||
digitalWrite(LED_POWER, LED_STATE_OFF);
|
||||
for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */
|
||||
}
|
||||
}
|
||||
|
||||
static void space()
|
||||
{
|
||||
for (volatile int i = 0; i < (DOT_TIME * 3); i++) { /* busy wait */
|
||||
}
|
||||
debug_printf("assert: %s:%d in %s: %s\r\n", file, line, func, failedexpr);
|
||||
HAL_NVIC_SystemReset();
|
||||
}
|
||||
|
||||
// Disable optimizations for this function so "frame" argument
|
||||
@@ -277,17 +241,5 @@ extern "C" __attribute__((optimize("O0"))) void HardFault_Handler_C(sContextStat
|
||||
|
||||
HALT_IF_DEBUGGING();
|
||||
|
||||
// blink SOS forever
|
||||
while (1) {
|
||||
dot();
|
||||
dot();
|
||||
dot();
|
||||
dash();
|
||||
dash();
|
||||
dash();
|
||||
dot();
|
||||
dot();
|
||||
dot();
|
||||
space();
|
||||
}
|
||||
}
|
||||
HAL_NVIC_SystemReset();
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <unity.h>
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
// Pins latLongToMeter()'s equirectangular-approximation accuracy against the original spherical
|
||||
// law of cosines, so a future change can't silently regress it.
|
||||
|
||||
static constexpr double kPi = 3.14159265358979323846;
|
||||
|
||||
static double referenceSphericalLawOfCosines(double lat_a, double lng_a, double lat_b, double lng_b)
|
||||
{
|
||||
double a1 = lat_a * kPi / 180.0;
|
||||
double a2 = lng_a * kPi / 180.0;
|
||||
double b1 = lat_b * kPi / 180.0;
|
||||
double b2 = lng_b * kPi / 180.0;
|
||||
double t1 = std::cos(a1) * std::cos(a2) * std::cos(b1) * std::cos(b2);
|
||||
double t2 = std::cos(a1) * std::sin(a2) * std::cos(b1) * std::sin(b2);
|
||||
double t3 = std::sin(a1) * std::sin(b1);
|
||||
double arg = t1 + t2 + t3;
|
||||
if (arg > 1.0)
|
||||
arg = 1.0;
|
||||
if (arg < -1.0)
|
||||
arg = -1.0;
|
||||
return 6366000 * std::acos(arg);
|
||||
}
|
||||
|
||||
// Below ~1m, relative error is dominated by rounding noise rather than the formula itself, so
|
||||
// assert an absolute bound instead (still catches a badly-broken implementation).
|
||||
static constexpr double kNearZeroAbsoluteToleranceMeters = 0.5;
|
||||
|
||||
// An order of magnitude above what the implementation currently produces per group - tight enough to
|
||||
// catch a regression, loose enough not to track float rounding. Groups differ because
|
||||
// equirectangular error grows with both separation and latitude.
|
||||
static constexpr double kLocalTolerancePercent = 0.01;
|
||||
static constexpr double kRegionalTolerancePercent = 0.1;
|
||||
static constexpr double kHighLatitudeTolerancePercent = 0.2;
|
||||
static constexpr double kAntimeridianTolerancePercent = 0.01;
|
||||
|
||||
static void assertWithinPercent(double expected, double actual, double pct, const char *msg)
|
||||
{
|
||||
if (expected < 1.0) {
|
||||
if (std::fabs(actual - expected) > kNearZeroAbsoluteToleranceMeters) {
|
||||
char buf[160];
|
||||
snprintf(buf, sizeof(buf), "%s: expected=%.3f actual=%.3f (near-zero, limit %.1fm absolute)", msg, expected, actual,
|
||||
kNearZeroAbsoluteToleranceMeters);
|
||||
TEST_FAIL_MESSAGE(buf);
|
||||
}
|
||||
return;
|
||||
}
|
||||
double err = std::fabs(actual - expected) / expected * 100.0;
|
||||
if (err > pct) {
|
||||
char buf[160];
|
||||
snprintf(buf, sizeof(buf), "%s: expected=%.1f actual=%.1f err=%.2f%% (limit %.2f%%)", msg, expected, actual, err, pct);
|
||||
TEST_FAIL_MESSAGE(buf);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_identical_points_is_zero(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_FLOAT(0.0f, GeoCoord::latLongToMeter(51.5, -0.1, 51.5, -0.1));
|
||||
}
|
||||
|
||||
static void test_local_distances(void)
|
||||
{
|
||||
// Movement-threshold scale (meters to a few km) - the most common real usage.
|
||||
struct {
|
||||
double la, lo, lb, lob;
|
||||
} cases[] = {
|
||||
{51.5074, -0.1278, 51.5080, -0.1278}, // ~67m north
|
||||
{51.5074, -0.1278, 51.5074, -0.1200}, // ~540m east at London's latitude
|
||||
{0.0, 0.0, 0.001, 0.001}, // ~157m near the equator
|
||||
{65.0, 25.0, 65.001, 25.002}, // high-ish latitude, small delta
|
||||
{-33.87, 151.21, -33.865, 151.215}, // Sydney, southern hemisphere
|
||||
};
|
||||
for (auto &c : cases) {
|
||||
double expected = referenceSphericalLawOfCosines(c.la, c.lo, c.lb, c.lob);
|
||||
double actual = GeoCoord::latLongToMeter(c.la, c.lo, c.lb, c.lob);
|
||||
assertWithinPercent(expected, actual, kLocalTolerancePercent, "local distance");
|
||||
}
|
||||
}
|
||||
|
||||
static void test_regional_distances(void)
|
||||
{
|
||||
// City-to-city scale (tens to ~500km) below 60 degrees; see test_high_latitude_distances.
|
||||
struct {
|
||||
double la, lo, lb, lob;
|
||||
} cases[] = {
|
||||
{51.5074, -0.1278, 48.8566, 2.3522}, // London to Paris, ~344km
|
||||
{40.7128, -74.0060, 42.3601, -71.0589}, // NYC to Boston, ~306km
|
||||
{35.6762, 139.6503, 34.6937, 135.5023}, // Tokyo to Osaka, ~400km
|
||||
{-33.8688, 151.2093, -37.8136, 144.9631}, // Sydney to Melbourne, ~714km
|
||||
};
|
||||
for (auto &c : cases) {
|
||||
double expected = referenceSphericalLawOfCosines(c.la, c.lo, c.lb, c.lob);
|
||||
double actual = GeoCoord::latLongToMeter(c.la, c.lo, c.lb, c.lob);
|
||||
assertWithinPercent(expected, actual, kRegionalTolerancePercent, "regional distance");
|
||||
}
|
||||
}
|
||||
|
||||
static void test_high_latitude_distances(void)
|
||||
{
|
||||
// Regional scale above 60 degrees, where equirectangular error grows fastest - a 500km pair at
|
||||
// 80 degrees already exceeds 1%.
|
||||
struct {
|
||||
double la, lo, lb, lob;
|
||||
} cases[] = {
|
||||
{69.6492, 18.9553, 67.2804, 14.4049}, // Tromso to Bodo, ~322km
|
||||
{64.8378, -147.7164, 61.2181, -149.9003}, // Fairbanks to Anchorage, ~417km
|
||||
{78.2232, 15.6469, 78.9230, 11.9219}, // Longyearbyen to Ny-Alesund, ~113km
|
||||
};
|
||||
for (auto &c : cases) {
|
||||
double expected = referenceSphericalLawOfCosines(c.la, c.lo, c.lb, c.lob);
|
||||
double actual = GeoCoord::latLongToMeter(c.la, c.lo, c.lb, c.lob);
|
||||
assertWithinPercent(expected, actual, kHighLatitudeTolerancePercent, "high-latitude distance");
|
||||
}
|
||||
}
|
||||
|
||||
static void test_antimeridian_wraparound(void)
|
||||
{
|
||||
// Two points ~22km apart straddling the 180th meridian - regression case for the antimeridian
|
||||
// wraparound fix (a naive b2-a2 would compute this as ~40,000km).
|
||||
double expected = referenceSphericalLawOfCosines(0.0, 179.9, 0.0, -179.9);
|
||||
double actual = GeoCoord::latLongToMeter(0.0, 179.9, 0.0, -179.9);
|
||||
assertWithinPercent(expected, actual, kAntimeridianTolerancePercent, "antimeridian distance");
|
||||
TEST_ASSERT_LESS_THAN_FLOAT(1000000.0f, actual); // sanity: nowhere near the naive-bug's ~40,000km
|
||||
}
|
||||
|
||||
static void test_symmetry(void)
|
||||
{
|
||||
// distance(a,b) should equal distance(b,a)
|
||||
double d1 = GeoCoord::latLongToMeter(51.5074, -0.1278, 48.8566, 2.3522);
|
||||
double d2 = GeoCoord::latLongToMeter(48.8566, 2.3522, 51.5074, -0.1278);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, d1, d2);
|
||||
}
|
||||
|
||||
static void test_no_nan_at_extreme_latitudes(void)
|
||||
{
|
||||
float d1 = GeoCoord::latLongToMeter(90.0, 0.0, -90.0, 0.0);
|
||||
float d2 = GeoCoord::latLongToMeter(89.9, 10.0, 89.9, -170.0);
|
||||
float d3 = GeoCoord::latLongToMeter(-89.9, 45.0, -89.9, -135.0);
|
||||
TEST_ASSERT_FALSE(std::isnan(d1));
|
||||
TEST_ASSERT_FALSE(std::isnan(d2));
|
||||
TEST_ASSERT_FALSE(std::isnan(d3));
|
||||
TEST_ASSERT_TRUE(d1 > 0);
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_identical_points_is_zero);
|
||||
RUN_TEST(test_local_distances);
|
||||
RUN_TEST(test_regional_distances);
|
||||
RUN_TEST(test_high_latitude_distances);
|
||||
RUN_TEST(test_antimeridian_wraparound);
|
||||
RUN_TEST(test_symmetry);
|
||||
RUN_TEST(test_no_nan_at_extreme_latitudes);
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "Arduino.h"
|
||||
#include "TestUtil.h"
|
||||
#include "gps/GPSUpdateScheduling.h"
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <unity.h>
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
// Confirms gpsHardsleepThresholdMs()'s pow()-free lookup table tracks the original
|
||||
// `2750 * pow(seconds, 1.22)` curve closely.
|
||||
static double originalFormula(uint32_t seconds)
|
||||
{
|
||||
return 2750.0 * std::pow((double)seconds, 1.22);
|
||||
}
|
||||
|
||||
static void test_matches_original_formula_at_sampled_points(void)
|
||||
{
|
||||
// Off-breakpoint values only - a breakpoint interpolates exactly by construction, so it would
|
||||
// test nothing here (test_exact_at_table_breakpoints covers those). Includes both worst-error
|
||||
// inputs: 7s (1.60%) and 728s (0.55%). Capped at 900s, the pre-existing 15-minute search clamp.
|
||||
const uint32_t samples[] = {4, 6, 7, 8, 9, 33, 100, 150, 500, 728, 899};
|
||||
for (uint32_t s : samples) {
|
||||
double expected = originalFormula(s);
|
||||
uint32_t actual = gpsHardsleepThresholdMs(s);
|
||||
// Pure integer arithmetic, so results are bit-identical everywhere - no float noise to
|
||||
// leave headroom for, and these sit just above the measured worst cases.
|
||||
double tolerance = expected * (s < 10 ? 0.02 : 0.0075);
|
||||
TEST_ASSERT_DOUBLE_WITHIN(tolerance, expected, (double)actual);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_zero_seconds_is_zero(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT32(0, gpsHardsleepThresholdMs(0));
|
||||
}
|
||||
|
||||
static void test_monotonically_nondecreasing(void)
|
||||
{
|
||||
uint32_t prev = gpsHardsleepThresholdMs(0);
|
||||
for (uint32_t s = 1; s <= 1200; s += 7) {
|
||||
uint32_t cur = gpsHardsleepThresholdMs(s);
|
||||
TEST_ASSERT_GREATER_OR_EQUAL_UINT32(prev, cur);
|
||||
prev = cur;
|
||||
}
|
||||
}
|
||||
|
||||
static void test_exact_at_table_breakpoints(void)
|
||||
{
|
||||
// Every breakpoint must return its own sampled value. Catches an off-by-one in the segment
|
||||
// scan, which a percentage bound on interpolated points would absorb.
|
||||
const uint32_t breakpoints[] = {0, 1, 2, 3, 5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 240, 300, 450, 600, 900};
|
||||
for (uint32_t s : breakpoints) {
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), "breakpoint %us", s);
|
||||
// Within 2ms, not exact: the 30s entry is rounded 1ms high, and pow() can differ by an ULP
|
||||
// across libm implementations. A real off-by-one in the scan misses by thousands.
|
||||
TEST_ASSERT_UINT32_WITHIN_MESSAGE(2, (uint32_t)(originalFormula(s) + 0.5), gpsHardsleepThresholdMs(s), msg);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_clamps_above_table_range(void)
|
||||
{
|
||||
uint32_t atMax = gpsHardsleepThresholdMs(900);
|
||||
TEST_ASSERT_EQUAL_UINT32(atMax, gpsHardsleepThresholdMs(2000));
|
||||
TEST_ASSERT_EQUAL_UINT32(atMax, gpsHardsleepThresholdMs(UINT32_MAX));
|
||||
}
|
||||
|
||||
static void test_clamp_boundary(void)
|
||||
{
|
||||
// The clamp must engage exactly at the last table point, not before or after it.
|
||||
TEST_ASSERT_LESS_THAN_UINT32(gpsHardsleepThresholdMs(900), gpsHardsleepThresholdMs(899));
|
||||
TEST_ASSERT_EQUAL_UINT32(gpsHardsleepThresholdMs(900), gpsHardsleepThresholdMs(901));
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
delay(10);
|
||||
initializeTestEnvironment();
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_matches_original_formula_at_sampled_points);
|
||||
RUN_TEST(test_zero_seconds_is_zero);
|
||||
RUN_TEST(test_monotonically_nondecreasing);
|
||||
RUN_TEST(test_exact_at_table_breakpoints);
|
||||
RUN_TEST(test_clamps_above_table_range);
|
||||
RUN_TEST(test_clamp_boundary);
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
@@ -28,4 +28,4 @@ build_src_filter =
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
# renovate: datasource=github-tags depName=ESP32-CH390 packageName=meshtastic/ESP32-CH390
|
||||
https://github.com/meshtastic/ESP32-CH390/archive/refs/tags/v1.1.0.zip
|
||||
https://github.com/meshtastic/ESP32-CH390/archive/v1.1.1.zip
|
||||
@@ -28,4 +28,4 @@ build_src_filter =
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
# renovate: datasource=github-tags depName=ESP32-CH390 packageName=meshtastic/ESP32-CH390
|
||||
https://github.com/meshtastic/ESP32-CH390/archive/refs/tags/v1.1.0.zip
|
||||
https://github.com/meshtastic/ESP32-CH390/archive/v1.1.1.zip
|
||||
Reference in new issue
Block a user