Skip to content

Add AS3935 lightning sensor support#10931

Draft
ndoo wants to merge 1 commit into
meshtastic:developfrom
meshmy:feat/as3935-lightning-sensor
Draft

Add AS3935 lightning sensor support#10931
ndoo wants to merge 1 commit into
meshtastic:developfrom
meshmy:feat/as3935-lightning-sensor

Conversation

@ndoo

@ndoo ndoo commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Implements #10774: support for the AS3935 Franklin lightning-detection IC as an environment telemetry sensor.

Rather than inventing a new "on-demand" sensor/packet type (as discussed in the issue), this reuses the existing TelemetrySensor scheduling infrastructure:

  • Counter mode (primary): strikes are counted over a fixed rolling ~1 hour window and reported as lightning_strike_count_1h / lightning_distance_km on the normal environment telemetry broadcast, the same way the DFRobot rain gauge reports rainfall_1h. The window is read non-destructively, so a peer's telemetry request answered between broadcasts can't silently zero out counted strikes.
  • Push mode (additive): when a genuine strike (not noise/disturber) is classified, the sensor also requests an out-of-cycle send via a new EnvironmentTelemetryModule::requestImmediateSend() hook. There's no fixed debounce on the request itself — the module's existing airtime/duty-cycle gate already paces every send, so it goes out as often as airtime allows rather than at an arbitrary fixed rate. The request does expire after 5 minutes unfulfilled, so it can't fire an arbitrarily stale broadcast if airtime was blocked for a long stretch.

Design notes

  • The AS3935's IRQ pin (opt-in per board via a new AS3935_IRQ define, not yet set on any board) is polled with a plain digitalRead() in runOnce(), not attachInterrupt(). This is deliberate, not an oversight — see "Why polling, not an interrupt?" below.
  • initDevice() only writes configuration once begin() has actually succeeded, so a device that's detected but fails to initialize isn't left in a half-configured state.
  • The AS3935's I2C addresses (0x01–0x03) fall inside the range this codebase's I2C scanner otherwise skips as reserved, so detection is a small dedicated probe in ScanI2CTwoWire.cpp, gated behind AS3935_IRQ and respecting the caller's address filter rather than widening the general scan loop for every board.
  • Presence is confirmed via a register write/readback round-trip rather than a fixed expected value — the AS3935 has no WHOAMI register, and a power-on-reset-only check can't survive a warm reboot that doesn't power-cycle the sensor (our own initDevice() permanently rewrites that register on first configuration).
  • All timing/debounce checks reuse the existing Throttle::isWithinTimespanMs helper already used throughout EnvironmentTelemetry.cpp — no new throttling mechanism.

Why polling, not an interrupt?

It'd be reasonable to expect an interrupt-capable pin to use attachInterrupt(), so this is worth calling out explicitly: it wouldn't buy anything here, for two independent reasons.

  1. Polling can't miss anything. The AS3935's IRQ line is a level, not a pulse — per the datasheet it stays asserted until its interrupt register is actually read — so a poll on any schedule will always catch a pending event. This is also exactly what the SparkFun library's own reference examples do (Example1_BasicLightning_I2C, Example2_More_Lightning_Features_I2C both use plain digitalRead() in a loop, never attachInterrupt()).
  2. The ISR couldn't do the useful work anyway. Classifying why the pin went high requires an I2C read (readInterruptReg()), which itself calls delay(2) per the datasheet's settle-time requirement — and blocking I2C/delay() calls are not safe to make from ISR context on any of this codebase's target platforms (esp32/nrf52/rp2xx0). So the ISR could only ever set a flag for runOnce() to drain later — at which point it has done no less work than just checking digitalRead() directly on the next tick.

Event flow

flowchart TD
    subgraph AS["AS3935Sensor::runOnce() (polled ~1s)"]
        CHECKPIN{"digitalRead(AS3935_IRQ)\n== HIGH?"}
        CLASSIFY["classifyPendingIrq()\nreadInterruptReg() over I2C"]
        REASON{"what did the\nregister report?"}
        LOGNOISE["log only"]
        LOGDIST["log only, ignored"]
        COUNT["strikeCountWindow++\ndistanceToStorm()"]
        DISTVALID{"distance valid?\n(not out-of-range)"}
        SAVEDIST["lastDistanceKm = distance"]
        SKIPDIST["distance stays unknown"]
        PUSHCALL["requestImmediateSend()\n(no fixed debounce)"]
        WINDOWCHECK{"~1h strike window\nelapsed?"}
        RESET["reset strikeCountWindow\n+ lastDistanceKm"]
        TICKDONE(("next tick"))

        CHECKPIN -->|"yes"| CLASSIFY
        CHECKPIN -->|"no"| WINDOWCHECK
        CLASSIFY --> REASON
        REASON -->|"noise floor too high"| LOGNOISE
        REASON -->|"disturber"| LOGDIST
        REASON -->|"lightning"| COUNT
        COUNT --> DISTVALID
        DISTVALID -->|"yes"| SAVEDIST
        DISTVALID -->|"no"| SKIPDIST
        SAVEDIST --> PUSHCALL
        SKIPDIST --> PUSHCALL
        PUSHCALL --> WINDOWCHECK
        LOGNOISE --> WINDOWCHECK
        LOGDIST --> WINDOWCHECK
        WINDOWCHECK -->|"yes"| RESET
        WINDOWCHECK -->|"no"| TICKDONE
        RESET --> TICKDONE
    end

    subgraph ETM["EnvironmentTelemetryModule (existing, unchanged scheduling)"]
        FLAG["immediateSendRequested = true\n(timestamped)"]
        STALE{"5 min elapsed\nsince requested?"}
        GIVEUP(("expire; counter still\nreported next normal broadcast"))
        AIRTIME{"airtime gate:\nisTxAllowedChannelUtil()\nand isTxAllowedAirUtil()?"}
        SEND["getMetrics()\nbroadcast interval, phone sync,\nor reply to a peer's request"]
        REPORT["report lightning_strike_count_1h\n+ lightning_distance_km\n(non-destructive read)"]

        FLAG --> STALE
        STALE -->|"yes"| GIVEUP
        STALE -->|"no"| AIRTIME
        AIRTIME -->|"yes"| SEND
        AIRTIME -->|"no, retry\nnext module tick"| STALE
        SEND --> REPORT
    end

    PUSHCALL -.->|"sets flag on\nthe module"| FLAG
Loading

Dependencies

This depends on the companion protobuf schema change adding lightning_strike_count_1h / lightning_distance_km to EnvironmentMetrics and AS3935 to TelemetrySensorType:

Related protobufs PR: meshtastic/protobufs#981, branch feat/as3935-lightning-sensor on ndoo/meshtastic-protobufs, based on meshtastic/protobufs's develop branch (the one this repo's develop submodule pin actually tracks, not master).

This firmware PR should stay a draft until that protobufs PR merges and the submodule pointer here is updated to the merged commit.

Testing locally before the protobufs PR merges: this branch's submodule pointer already points at the commit adding the AS3935 sensor type and lightning fields, but the generated src/mesh/generated/ files are intentionally not committed here (they'd conflict with the real regenerated output once meshtastic/protobufs#981 merges and the pointer is updated — see review discussion below). To build this branch locally: (1) git submodule update --init protobufs; (2) download the nanopb 0.4.9 prebuilt tool for your OS from https://jpa.kapsi.fi/nanopb/download/ into a nanopb-0.4.9/ directory at the repo root; (3) run bin/regen-protos.sh to regenerate src/mesh/generated/; (4) build as normal, e.g. pio run -e rak4631.

Board support

No board currently defines AS3935_IRQ — the sensor is entirely opt-in. A board wanting to support this sensor adds #define AS3935_IRQ <gpio> to its variants/<board>/variant.h.

🤝 Attestations

  • I have tested that my proposed changes behave as described.
    • Not yet tested on real hardware — I don't have physical AS3935 hardware to validate against. Verified via: a full, successful rak4631 (nRF52) firmware build; trunk fmt clean; and direct inspection of the SparkFun_AS3935 Arduino library source (register semantics, constructor/begin signatures, POR defaults) to avoid guessing at the datasheet.
    • The native test suite (./bin/run-tests.sh) currently reports RED in my dev environment, but that reproduces identically on the unmodified base branch (pre-existing -fprofile-abs-path clang/toolchain mismatch, unrelated to this change).
  • I have tested that my proposed changes do not cause any obvious regressions on the following devices:
    • Heltec (Lora32) V3
    • LilyGo T-Deck
    • LilyGo T-Beam
    • RAK WisBlock 4631 — compiles clean, not hardware-tested
    • Seeed Studio T-1000E tracker card
    • Other (please specify below)
    • No board currently wires AS3935_IRQ, so this feature can't regress any existing board's behavior — it's inert until a board opts in.

Closes #10774.

Summary by CodeRabbit

  • New Features

    • Added support for SparkFun AS3935 lightning sensors.
    • Lightning strikes and estimated distance can now be included in environmental telemetry.
    • The system can trigger an immediate telemetry update when lightning is detected.
    • Added support for detecting additional sensor addresses during hardware scanning.
  • Bug Fixes

    • Improved telemetry update handling so pending urgent sends don’t get stuck and stale requests are cleared automatically.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 888494b4-6e6f-4b69-ba46-88131d199e17

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds SparkFun AS3935 lightning detector support: a pinned library dependency, new I2C address macros and device enum, an I2C probing routine, a new AS3935Sensor telemetry driver, and EnvironmentTelemetryModule changes enabling immediate telemetry sends triggered by lightning events. Updates protobufs subproject reference.

Changes

AS3935 Lightning Sensor Integration

Layer / File(s) Summary
Dependency and address configuration
platformio.ini, src/configuration.h, protobufs
Adds SparkFun AS3935 library dependency (v1.4.9), defines primary and alternate I2C address macros, and advances the protobufs subproject pin.
I2C detection support
src/detect/ScanI2C.h, src/detect/ScanI2CTwoWire.cpp
Adds AS3935 device type enum value and a post-scan probing routine that checks candidate addresses, validates via register readback, and records found devices.
AS3935Sensor driver implementation
src/modules/Telemetry/Sensor/AS3935Sensor.h, src/modules/Telemetry/Sensor/AS3935Sensor.cpp
Implements a new AS3935Sensor class handling device init, IRQ-driven periodic polling, lightning/disturber classification, rolling strike-count/distance tracking, and metrics reporting.
Telemetry immediate-send wiring
src/modules/Telemetry/EnvironmentTelemetry.h, src/modules/Telemetry/EnvironmentTelemetry.cpp, src/modules/Modules.cpp
Adds requestImmediateSend() and related state, exposes a global module pointer, registers AS3935Sensor during scanning, updates runOnce() scheduling for immediate/stale requests, and assigns the module pointer in setup.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ScanI2CTwoWire
  participant AS3935Sensor
  participant EnvironmentTelemetryModule
  participant Mesh

  ScanI2CTwoWire->>ScanI2CTwoWire: probe AS3935 candidate addresses
  ScanI2CTwoWire->>AS3935Sensor: register found device
  AS3935Sensor->>AS3935Sensor: initDevice() configures thresholds and IRQ
  loop runOnce polling
    AS3935Sensor->>AS3935Sensor: check IRQ pin
    AS3935Sensor->>AS3935Sensor: classify interrupt reason
    AS3935Sensor->>EnvironmentTelemetryModule: requestImmediateSend()
  end
  EnvironmentTelemetryModule->>EnvironmentTelemetryModule: runOnce() detects immediateSendRequested
  EnvironmentTelemetryModule->>Mesh: sendTelemetry()
Loading

Suggested labels: enhancement, sensors, telemetry

Suggested reviewers: thebentern, GUVWAF

Poem:
A rabbit heard a distant boom,
And sniffed a storm cloud's coming gloom,
With AS3935 wired up right,
It counts each strike and flash of light,
Then hops to send the news real quick—
No waiting round for lightning's trick! ⚡🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding AS3935 lightning sensor support.
Description check ✅ Passed The description is substantive and follows the template’s intent, including purpose, design notes, testing, and attestations.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Note

Building this pull request… the flash button, badges and supported-board
list will appear here automatically once CI finishes.

@github-actions github-actions Bot added the hardware-support Hardware related: new devices or modules, problems specific to hardware label Jul 7, 2026
@ndoo
ndoo force-pushed the feat/as3935-lightning-sensor branch from 6504bfe to bee0a81 Compare July 7, 2026 18:58
@ndoo

ndoo commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

f-p both PRs to add _1h suffix to new protobuf field

@ndoo

ndoo commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/detect/ScanI2CTwoWire.cpp (1)

913-915: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Comment exceeds two-line limit.

This explanatory comment spans three lines. As per coding guidelines, "Keep code comments minimal: one or two lines maximum, only when the reason is not obvious, and do not restate the next line."

✏️ Suggested trim
-            // 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.
+            // No WHOAMI register; write a test pattern to bits[5:1] and confirm readback.
+            // initDevice() overwrites this field right after anyway.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/detect/ScanI2CTwoWire.cpp` around lines 913 - 915, The inline explanation
in initDevice() is too long and repeats implementation details, so trim it to at
most two lines while keeping only the essential rationale for the test-pattern
write/readback check in ScanI2CTwoWire. Keep the comment minimal and avoid
restating what the following code already shows, using the surrounding
initDevice logic and REG0x00/WHOAMI context to preserve meaning.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/modules/Telemetry/Sensor/AS3935Sensor.cpp`:
- Around line 82-107: The interrupt switch in AS3935Sensor::readInterruptReg
handling is using the wrong case labels, so lightning events may not be matched
correctly. Update the switch on interruptReason in AS3935Sensor.cpp to use the
SparkFun AS3935 interrupt constants that correspond to readInterruptReg()
values, specifically LIGHTNING_INT, DISTURBER_INT, and NOISE_INT, so the
lightning, disturber, and noise branches are triggered as intended.

---

Nitpick comments:
In `@src/detect/ScanI2CTwoWire.cpp`:
- Around line 913-915: The inline explanation in initDevice() is too long and
repeats implementation details, so trim it to at most two lines while keeping
only the essential rationale for the test-pattern write/readback check in
ScanI2CTwoWire. Keep the comment minimal and avoid restating what the following
code already shows, using the surrounding initDevice logic and REG0x00/WHOAMI
context to preserve meaning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ac0ce3ef-fa56-46e1-8c5f-5323a433093a

📥 Commits

Reviewing files that changed from the base of the PR and between d16ae2b and bee0a81.

⛔ Files ignored due to path filters (1)
  • src/mesh/generated/meshtastic/telemetry.pb.h is excluded by !**/generated/**, !src/mesh/generated/**
📒 Files selected for processing (10)
  • platformio.ini
  • protobufs
  • src/configuration.h
  • src/detect/ScanI2C.h
  • src/detect/ScanI2CTwoWire.cpp
  • src/modules/Modules.cpp
  • src/modules/Telemetry/EnvironmentTelemetry.cpp
  • src/modules/Telemetry/EnvironmentTelemetry.h
  • src/modules/Telemetry/Sensor/AS3935Sensor.cpp
  • src/modules/Telemetry/Sensor/AS3935Sensor.h

Comment thread src/modules/Telemetry/Sensor/AS3935Sensor.cpp
@ndoo

ndoo commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@caveman99
caveman99 requested a review from oscgonfer July 8, 2026 07:18
Comment thread src/mesh/generated/meshtastic/telemetry.pb.h Outdated
Implements meshtastic#10774: an AS3935Sensor (TelemetrySensor
subclass) that reports lightning_strike_count_1h and lightning_distance_km
on the normal environment telemetry interval, like a rain gauge -
strikes are counted over a fixed rolling ~1h window and read
non-destructively, so replying to a peer's telemetry request in
between broadcasts can't silently drop counted strikes.

The AS3935's IRQ pin (opt-in per board via AS3935_IRQ) is polled with a
plain digitalRead() in runOnce(), deliberately not attachInterrupt():
the IRQ line is a level that stays asserted until its interrupt
register is read, so polling can't miss an event regardless of timing,
matching the SparkFun library's own reference examples. An interrupt
would also buy nothing here even setting that aside - classification
requires an I2C read (readInterruptReg(), which itself calls delay(2)
per the datasheet's settle-time requirement), and blocking I2C/delay()
calls aren't safe from ISR context on any of this codebase's target
platforms, so the ISR could only ever set a flag for later draining -
no less work than just polling the pin directly on the next tick.

A genuine lightning classification also requests an immediate
out-of-cycle send via a new EnvironmentTelemetryModule::
requestImmediateSend() hook. There's no fixed debounce on the request
itself - EnvironmentTelemetryModule's existing airtime/duty-cycle gate
already paces every send, so it sends as often as airtime allows rather
than an arbitrary fixed rate. The request does expire after 5 minutes
unfulfilled, so it can't fire an arbitrarily stale broadcast if airtime
was blocked for a long stretch.

The AS3935's I2C addresses (0x01-0x03) fall inside the range this
codebase's I2C scanner otherwise skips as reserved, so detection is a
small dedicated probe gated behind AS3935_IRQ and respecting the
caller's address filter, rather than a change to the general scan
loop. Presence is confirmed via a register write/readback round-trip
rather than a fixed expected value, since the AS3935 has no WHOAMI
register and a power-on-reset-only check can't survive a warm reboot
that doesn't power-cycle the sensor (initDevice() permanently rewrites
that register on first configuration).

Generated files under src/mesh/generated/ are intentionally excluded
from this commit - they're regenerated from the protobufs submodule by
update_protobufs.yml, and hand edits get overwritten and conflict once
the companion protobufs PR merges and the submodule pointer updates.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>
@ndoo
ndoo force-pushed the feat/as3935-lightning-sensor branch from e72e437 to a0fac16 Compare July 9, 2026 07:04
@ndoo
ndoo requested a review from caveman99 July 10, 2026 21:48
@oscgonfer

Copy link
Copy Markdown
Contributor

I'm trying to find available hardware for this, but not lucky yet. Have you got any recommendations? I'm curious about how to test this reliably too...

@ndoo

ndoo commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

I'm trying to find available hardware for this, but not lucky yet. Have you got any recommendations? I'm curious about how to test this reliably too...

Seems like the main options are:

I’m waiting for the purple board to make its way here. It’s pretty expensive and I don’t have a use for it if Meshtastic support doesn't land, so I went for the cheap, slow shipping.

@Kealper

Kealper commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Hoping this ends up getting merged in, I've wanted to use these on sensor-role nodes for a while now!

@ndoo

ndoo commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Hoping this ends up getting merged in, I've wanted to use these on sensor-role nodes for a while now!

are you able to build and test? i'm still waiting for my breakout board to arrive

@Kealper

Kealper commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Hoping this ends up getting merged in, I've wanted to use these on sensor-role nodes for a while now!

are you able to build and test? i'm still waiting for my breakout board to arrive

Unfortunately not yet, as I'm also waiting on a board. I had held out on purchasing originally due to cost of the DFRobot board, but decided to get one after seeing the much cheaper purple board pop up on AliExpress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hardware-support Hardware related: new devices or modules, problems specific to hardware

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: Support for AS3935 sensors (lightning detection)

4 participants