Summary
On my Heltec Mesh Node T1, the reported battery voltage jumps between two distinct bands. I first noticed this manually in MeshCore clients. I then ran a read-only BLE test that reproduced the behaviour while charging and after unplugging the charger.
The data shows changes of hundreds of millivolts within a few seconds. I don't believe these changes can represent physical cell-voltage movement. An AI-assisted source review reported that the switched battery divider presents about 79.59 kΩ to the nRF52840 SAADC while the currently pinned Arduino core defaults to a 3 µs acquisition time, specified for sources up to 10 kΩ. This is a plausible explanation to test, but I have not yet confirmed it with patched firmware or a multimeter - don't want to ruin the water resistance on my T1 with a teardown.
Hardware and software
- Device: Heltec Mesh Node T1
- Firmware-reported model:
Heltec T1
- Firmware:
v1.16.0-07a3ca9
- Data client:
meshcore Python package 2.3.7, revision 5bac3573b51c4298062881885b6d15a994109076
- T1 battery path reviewed:
variants/heltec_t1/T1Board.cpp:69-80
- MeshCore source reviewed at commit:
a3a1aa5e3be34b42d8ac8c2cc244d30af6cdd71e
Reproduction and data collection
I kept the node on its normal charger, marked the full-on-charger point, unplugged it, and marked the unplug event. I then used a simple python logger that queried the existing battery command over BLE:
- five readings per batch, about two seconds apart;
- one batch every 120 seconds;
- 1,605 raw readings in 321 complete batches;
- over 8 hours represented in the charging phase and 3 hours after unplugging;
- every stored batch summary recomputes exactly from the raw readings.
A minimal manual reproduction you could do would be to connect to a T1 over BLE and issue five battery queries about two seconds apart, then repeat the group periodically. I observed the incompatible bands both while charging and after unplugging.
Observed result
The thresholds below only describe the two visible clusters; they are not assumed to identify the true cell voltage.
- While charging, the upper cluster has a median of 4201 mV and the lower excursions have a median of 3935 mV.
- After unplugging, the lower cluster has a median of 3352 mV and the upper excursions have a median of 3831 mV.
- There are 30 post-unplug pairs in which both bands occur in the same five-reading batch. Their median separation is 479 mV (443–504 mV), with the paired readings only about 2.16 seconds apart.
Because the raw voltage is not trustworthy, I didn't get the battery-percentage curve from this run for the T1 that I'd hopped to get.
Source-level hypothesis — not yet hardware-confirmed, AI assisted
I used Codex with GPT-5.6 Sol on Max reasoning to carry out several analysis tasks on the schematic and source code.
The Heltec schematic shows R11 = 390 kΩ and R16 = 100 kΩ on BAT_ADC. Their Thevenin resistance is about 79.59 kΩ.
The current T1 implementation enables the divider, waits 10 ms, takes one analogRead(), and disables the divider. The MeshCore-pinned Adafruit nRF52 core documents and implements a 3 µs default SAADC acquisition time. Nordic's nRF52840 SAADC specification lists:
- 3 µs for source resistance up to 10 kΩ;
- 10 µs for source resistance up to 100 kΩ.
The external 10 ms delay can settle the switched divider, but it does not lengthen the SAADC's internal sample-and-hold acquisition window. The approximately 79.6 kΩ source is therefore outside the 3 µs acquisition-time specification. This mismatch is confirmed in the source and datasheets; whether it causes the observed bimodality still needs a device-side A/B test.
The fixed multiplier does not explain the jumps: 4.916 versus the schematic divider ratio of 4.900 differs by about 0.327%.
Proposed next test
Before proposing a production fix, it could be helpful to test a narrowly scoped diagnostic build that:
- uses an acquisition time suitable for this divider (10 µs minimum; 40 µs for the initial diagnostic A/B);
- keeps the divider enabled across the conversion set;
- discards the first conversion;
- takes a small odd sample set and uses a robust aggregate such as the median;
- restores any global ADC setting changed by the function;
- leaves
ADC_MULTIPLIER unchanged.
I would compare the current and diagnostic builds on USB and battery-only power and check both against a multimeter before opening a firmware PR.
Current testing limits
- No firmware patch has been built or flashed.
- No current-versus-diagnostic A/B test has been performed.
- No independent multimeter measurement has been taken.
- No second T1 has been tested.
- The exact collection script was not included in the run bundle transferred for analysis. I have prepared a redacted evidence bundle containing the raw measurements, event metadata, and a standalone script that deterministically validates the reported statistics, and can provide it if useful.
- This issue reports reproducible measurement behaviour and a source-backed hypothesis, not a confirmed root cause or completed fix.
AI assistance and provenance
- The physical observations, charge/unplug actions, and phase markers were mine. I supplied the initial observations and test instructions in my own words, including voice-dictated notes.
- The read-only logger and collection workflow were created with assistance from OpenAI Codex using GPT 5.6 Sol on Max reasoning effort.
- Post-run integrity checking, source/specification review, hypothesis development, deterministic analysis scripting, and drafting were assisted by Hermes Agent, also using GPT 5.6 Sol on Max reasoning effort.
- The numerical results are reproducible from the attached raw data with a standalone Python script; they do not depend on an LLM summary.
- I reviewed the resulting evidence and am responsible for the report's scope, limitations, and decision to submit it. This started because of data I was seeing in the telemetry on the client applications. I wanted to better understand why the battery level seemed to be inaccurate for this model.
References
Summary
On my Heltec Mesh Node T1, the reported battery voltage jumps between two distinct bands. I first noticed this manually in MeshCore clients. I then ran a read-only BLE test that reproduced the behaviour while charging and after unplugging the charger.
The data shows changes of hundreds of millivolts within a few seconds. I don't believe these changes can represent physical cell-voltage movement. An AI-assisted source review reported that the switched battery divider presents about 79.59 kΩ to the nRF52840 SAADC while the currently pinned Arduino core defaults to a 3 µs acquisition time, specified for sources up to 10 kΩ. This is a plausible explanation to test, but I have not yet confirmed it with patched firmware or a multimeter - don't want to ruin the water resistance on my T1 with a teardown.
Hardware and software
Heltec T1v1.16.0-07a3ca9meshcorePython package 2.3.7, revision5bac3573b51c4298062881885b6d15a994109076variants/heltec_t1/T1Board.cpp:69-80a3a1aa5e3be34b42d8ac8c2cc244d30af6cdd71eReproduction and data collection
I kept the node on its normal charger, marked the full-on-charger point, unplugged it, and marked the unplug event. I then used a simple python logger that queried the existing battery command over BLE:
A minimal manual reproduction you could do would be to connect to a T1 over BLE and issue five battery queries about two seconds apart, then repeat the group periodically. I observed the incompatible bands both while charging and after unplugging.
Observed result
The thresholds below only describe the two visible clusters; they are not assumed to identify the true cell voltage.
Because the raw voltage is not trustworthy, I didn't get the battery-percentage curve from this run for the T1 that I'd hopped to get.
Source-level hypothesis — not yet hardware-confirmed, AI assisted
The Heltec schematic shows R11 = 390 kΩ and R16 = 100 kΩ on
BAT_ADC. Their Thevenin resistance is about 79.59 kΩ.The current T1 implementation enables the divider, waits 10 ms, takes one
analogRead(), and disables the divider. The MeshCore-pinned Adafruit nRF52 core documents and implements a 3 µs default SAADC acquisition time. Nordic's nRF52840 SAADC specification lists:The external 10 ms delay can settle the switched divider, but it does not lengthen the SAADC's internal sample-and-hold acquisition window. The approximately 79.6 kΩ source is therefore outside the 3 µs acquisition-time specification. This mismatch is confirmed in the source and datasheets; whether it causes the observed bimodality still needs a device-side A/B test.
The fixed multiplier does not explain the jumps: 4.916 versus the schematic divider ratio of 4.900 differs by about 0.327%.
Proposed next test
Before proposing a production fix, it could be helpful to test a narrowly scoped diagnostic build that:
ADC_MULTIPLIERunchanged.I would compare the current and diagnostic builds on USB and battery-only power and check both against a multimeter before opening a firmware PR.
Current testing limits
AI assistance and provenance
References
MeshCore/variants/heltec_t1/T1Board.cpp
Lines 69 to 80 in a3a1aa5
MeshCore/variants/heltec_t1/variant.h
Lines 150 to 165 in a3a1aa5