diff --git a/platformio.ini b/platformio.ini index 3f562ab335a..010d40a7b3c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -193,6 +193,8 @@ lib_deps = https://github.com/DFRobot/DFRobot_RTU/archive/refs/tags/V1.0.6.zip # renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip + # renovate: datasource=github-tags depName=SparkFun AS3935 packageName=sparkfun/SparkFun_AS3935_Lightning_Detector_Arduino_Library + https://github.com/sparkfun/SparkFun_AS3935_Lightning_Detector_Arduino_Library/archive/refs/tags/v1.4.9.zip # renovate: datasource=github-tags depName=INA226 packageName=robtillaart/INA226 https://github.com/RobTillaart/INA226/archive/refs/tags/0.6.6.zip # renovate: datasource=github-tags depName=SparkFun MAX3010x packageName=sparkfun/SparkFun_MAX3010x_Sensor_Library diff --git a/src/configuration.h b/src/configuration.h index 0ed4dd893db..55080c7a3ae 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -310,7 +310,9 @@ along with this program. If not, see . #define DS248X_ADDR_ALT6 0x1E // same as HMC5883L_ADDR #define DS248X_ADDR_ALT7 0x1F // same as BBQ10_KB_ADDR #define HM330X_ADDR 0x40 - +#define AS3935_ADDR 0x03 // both address pins tied high, the common breakout-board default +#define AS3935_ADDR_ALT 0x01 +#define AS3935_ADDR_ALT2 0x02 // ----------------------------------------------------------------------------- // ACCELEROMETER diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 70f848b33a3..08b5a623920 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -104,7 +104,8 @@ class ScanI2C ISM330DHCX, SPA06, DS248X, - HM330X + HM330X, + AS3935 } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 21a74b75990..38a7b985a4a 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -1065,6 +1065,41 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) foundDevices[addr] = type; } } + +#ifdef AS3935_IRQ + // AS3935 addresses (0x01-0x03) fall in the reserved range the loop above skips; probe + // them separately rather than widening that loop for every board. + static const uint8_t as3935Candidates[] = {AS3935_ADDR_ALT, AS3935_ADDR_ALT2, AS3935_ADDR}; + for (uint8_t i = 0; i < sizeof(as3935Candidates); i++) { + // Respect the caller's address filter, same as the main loop above (line ~269). + if (asize != 0 && !in_array(address, asize, as3935Candidates[i])) + continue; + + DeviceAddress as3935Addr(port, as3935Candidates[i]); + i2cBus->beginTransmission(as3935Candidates[i]); + uint8_t as3935Err = i2cBus->endTransmission(); + if (as3935Err == 0) { + // No WHOAMI register, and a POR-only check can't survive a warm reboot (this + // driver rewrites REG0x00 on init). Instead, write a test pattern to bits[5:1] + // and confirm it reads back - initDevice() overwrites this field right after anyway. + constexpr uint8_t AS3935_PROBE_PATTERN = 0b01010; // arbitrary, bits[5:1] + i2cBus->beginTransmission(as3935Candidates[i]); + i2cBus->write((uint8_t)0x00); // REG0x00 (AFE_GAIN) + i2cBus->write((uint8_t)(AS3935_PROBE_PATTERN << 1)); // PWD=0, gain bits = pattern + if (i2cBus->endTransmission() == 0) { + uint16_t reg0 = getRegisterValue(ScanI2CTwoWire::RegisterLocation(as3935Addr, 0x00), 1); + if (((reg0 >> 1) & 0x1F) == AS3935_PROBE_PATTERN) { + logFoundDevice("AS3935", as3935Candidates[i]); + deviceAddresses[AS3935] = as3935Addr; + foundDevices[as3935Addr] = AS3935; + break; // only one AS3935 expected per bus + } else { + LOG_DEBUG("Unexpected REG0x00 readback for AS3935: addr=0x%x val=0x%x", as3935Candidates[i], reg0); + } + } + } + } +#endif } void ScanI2CTwoWire::scanPort(I2CPort port) diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 546651c5bc2..a2de555e90d 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -223,7 +223,7 @@ void setupModules() #if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (moduleConfig.has_telemetry && (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { - new EnvironmentTelemetryModule(); + environmentTelemetryModule = new EnvironmentTelemetryModule(); } #if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR if (moduleConfig.has_telemetry && diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 9a6076b2811..38e4aa38d08 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -102,6 +102,10 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "Sensor/DFRobotGravitySensor.h" #endif +#if __has_include() +#include "Sensor/AS3935Sensor.h" +#endif + #if __has_include() #include "Sensor/NAU7802Sensor.h" #endif @@ -150,6 +154,7 @@ EnvironmentTelemetryModule::DisplaySource gDisplaySource = EnvironmentTelemetryM } // namespace static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002; +static constexpr uint32_t IMMEDIATE_SEND_MAX_STALENESS_MS = 5UL * 60UL * 1000; // 5 minutes static constexpr uint32_t LOCAL_DISPLAY_REFRESH_INTERVAL_MS = 1000; EnvironmentTelemetryModule::DisplaySource EnvironmentTelemetryModule::getDisplaySource() @@ -290,6 +295,9 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_RAIN); #endif +#if __has_include() + addSensor(i2cScanner, ScanI2C::DeviceType::AS3935); +#endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::AHT10); #endif @@ -426,9 +434,15 @@ int32_t EnvironmentTelemetryModule::runOnce() } refreshDisplayedMeasurement(); + // Give up on a stale immediate-send request rather than fire an arbitrarily late broadcast. + if (immediateSendRequested && + !Throttle::isWithinTimespanMs(immediateSendRequestedAtMs, IMMEDIATE_SEND_MAX_STALENESS_MS)) { + immediateSendRequested = false; + } + uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0; - if (((lastTelemetry == 0) || + if (((lastTelemetry == 0) || immediateSendRequested || !Throttle::isWithinTimespanMs( lastTelemetry, Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.environment_update_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes, @@ -436,6 +450,7 @@ int32_t EnvironmentTelemetryModule::runOnce() airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); + immediateSendRequested = false; if (transmitHistory) transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h index 6d5678b35a8..0aabe86479e 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.h +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -56,6 +56,14 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif + /** Bypass the normal broadcast throttle once, for a sensor with a noteworthy event to + * report sooner than the next scheduled send (airtime limits still apply). */ + void requestImmediateSend() + { + immediateSendRequested = true; + immediateSendRequestedAtMs = millis(); + } + 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 @@ -87,6 +95,8 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, bool shouldDisplayRemoteNode(NodeNum nodeNum) const; bool firstTime = 1; + bool immediateSendRequested = false; + uint32_t immediateSendRequestedAtMs = 0; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t lastLocalDisplayRefreshMs = 0; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute diff --git a/src/modules/Telemetry/Sensor/AS3935Sensor.cpp b/src/modules/Telemetry/Sensor/AS3935Sensor.cpp new file mode 100644 index 00000000000..3c40fa34ecd --- /dev/null +++ b/src/modules/Telemetry/Sensor/AS3935Sensor.cpp @@ -0,0 +1,121 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "AS3935Sensor.h" +#include "TelemetrySensor.h" +#include "modules/Telemetry/EnvironmentTelemetry.h" +#include +#include + +namespace +{ +// No attachInterrupt(): the IRQ line stays asserted until read, so polling can't miss it, +// and the I2C read itself isn't ISR-safe anyway. +constexpr int32_t AS3935_CHECK_INTERVAL_MS = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; +constexpr uint8_t AS3935_DISTANCE_OUT_OF_RANGE = 0x3F; +// Strikes accumulate over a rolling window, reset by elapsed time rather than on +// getMetrics() (which also fires when replying to a peer's telemetry request). +constexpr uint32_t AS3935_STRIKE_WINDOW_MS = 60UL * 60UL * 1000; // 1 hour +} // namespace + +AS3935Sensor::AS3935Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_AS3935, "AS3935") {} + +AS3935Sensor::~AS3935Sensor() +{ + if (lightning) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" + delete lightning; +#pragma GCC diagnostic pop + lightning = nullptr; + } +} + +bool AS3935Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + + lightning = new SparkFun_AS3935(dev->address.address); + status = lightning->begin(*bus); + if (!status) { + initI2CSensor(); + return status; + } + + // Defaults match the library's own example, except outdoor mode and unmasked + // disturbers (kept visible in the log). + lightning->setIndoorOutdoor(OUTDOOR); + lightning->setNoiseLevel(2); + lightning->watchdogThreshold(2); + lightning->spikeRejection(2); + lightning->maskDisturber(false); + lightning->lightningThreshold(1); + +#ifdef AS3935_IRQ + pinMode(AS3935_IRQ, INPUT); +#endif + + windowStartMs = millis(); + initI2CSensor(); + return status; +} + +int32_t AS3935Sensor::runOnce() +{ +#ifdef AS3935_IRQ + if (digitalRead(AS3935_IRQ) == HIGH) { + classifyPendingIrq(); + } +#endif + if (!Throttle::isWithinTimespanMs(windowStartMs, AS3935_STRIKE_WINDOW_MS)) { + strikeCountWindow = 0; + lastDistanceKm = -1; + windowStartMs = millis(); + } + return AS3935_CHECK_INTERVAL_MS; +} + +void AS3935Sensor::classifyPendingIrq() +{ + uint8_t interruptReason = lightning->readInterruptReg(); + switch (interruptReason) { + case LIGHTNING: { + strikeCountWindow++; + uint8_t distance = lightning->distanceToStorm(); + if (distance != AS3935_DISTANCE_OUT_OF_RANGE) { + lastDistanceKm = distance; + LOG_INFO("%s: lightning strike detected, distance=%dkm", sensorName, distance); + } else { + LOG_INFO("%s: lightning strike detected, distance unknown (out of range)", sensorName); + } + // No debounce here - EnvironmentTelemetryModule's airtime gate already paces every send. + if (environmentTelemetryModule) { + environmentTelemetryModule->requestImmediateSend(); + } + break; + } + case DISTURBER_DETECT: + LOG_DEBUG("%s: disturber detected (ignored)", sensorName); + break; + case NOISE_TO_HIGH: + LOG_DEBUG("%s: noise floor too high", sensorName); + break; + default: + break; + } +} + +bool AS3935Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_lightning_strike_count_1h = true; + measurement->variant.environment_metrics.lightning_strike_count_1h = strikeCountWindow; + if (lastDistanceKm >= 0) { + measurement->variant.environment_metrics.has_lightning_distance_km = true; + measurement->variant.environment_metrics.lightning_distance_km = lastDistanceKm; + } + return true; +} + +#endif diff --git a/src/modules/Telemetry/Sensor/AS3935Sensor.h b/src/modules/Telemetry/Sensor/AS3935Sensor.h new file mode 100644 index 00000000000..cb8353c65d2 --- /dev/null +++ b/src/modules/Telemetry/Sensor/AS3935Sensor.h @@ -0,0 +1,32 @@ +#pragma once + +#ifndef _MT_AS3935SENSOR_H +#define _MT_AS3935SENSOR_H +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class AS3935Sensor : public TelemetrySensor +{ + private: + SparkFun_AS3935 *lightning = nullptr; + uint32_t strikeCountWindow = 0; + float lastDistanceKm = -1; // sentinel: no valid distance captured this window + uint32_t windowStartMs = 0; + + void classifyPendingIrq(); + + public: + AS3935Sensor(); + ~AS3935Sensor(); + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual int32_t runOnce() override; +}; + +#endif +#endif