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