The battery percentage on the Freenove FNK0104B is believed correct: the divider is confirmed (GPIO9, ×2.0, from Freenove's own Sketch_05.1_Battery_Voltage) and GPIO9 is ADC1_CH8, so Wi-Fi does not disturb it.
The charging / discharging verdict is not validated. getChargingState() infers it from cell voltage alone — this board, like the E32R28T-1, brings no charge-status line out to a GPIO — and every constant it infers with was measured against a different charger.
Why the existing constants are suspect here
From src/hal/BoardPower.cpp, tuned on the E32R28T-1's TP4054:
V_CHARGER_HELD = 4.21 V — "no resting cell reaches this"
CHARGE_STEP_V — the per-sample step that makes the icon respond to a cable within ~2 s
CHARGE_WINDOW_MS = 45000 — the low-pass window for everything in between
CHARGE_FULL_V — guards the flat-window fallback
Measured on the FNK0104B so far:
| Condition |
Reading |
| USB, no pack |
4.09 – 4.16 V |
| USB + 3000 mAh pack fitted |
3.93 V |
At 3.93 V, V_CHARGER_HELD never fires, so the verdict falls through to the 45-second trend. Whether that produces the right answer on this charger is unknown. The icon may simply be wrong.
The one thing that must NOT be done
Do not add an isBatteryPresent(). It existed once, as a constant true. On the E32R28T-1 it was disproved by measurement (recorded in src/hal/BoardPower.cpp): pack+USB 4.224 V, USB with no pack 4.159 V, pack alone 4.066 V — the no-pack case sits between the other two, because the charger holds BAT at float voltage with or without a cell.
The same inversion is already visible on this board: 4.09–4.16 V with no pack against 3.93 V with one. No threshold separates them in either direction. Do not reintroduce the question in a new form or retune a constant to answer it.
V_SENSOR_MAX (4.50 V) means the ADC is faulty, not "no pack".
What needs doing
- Port
env:batdiag to the ESP32-S3. It currently extends [esp32_common] (board = esp32dev), so it cannot be flashed to an S3 at all — the image carries a chip ID esptool refuses. A new [env:batdiag_fnk0104b] extending [esp32s3_common] with ${board_fnk0104b.build_flags} is the starting point. Note batdiag needs ARDUINO_USB_MODE=1 and ARDUINO_USB_CDC_ON_BOOT=1 on this board — it has no USB-UART bridge, so without them serial output goes nowhere.
- Work the pages it already has: NO-BATT (resolve what this charger does with no cell), CALIBRATE (confirm the ×2.0 divider against a meter) and LOG (CSV
ms,raw,adc_mv,cell_mv,pct,state over a full discharge — the 3000 mAh pack makes this a long run).
- If the constants need to differ per board, they move into
BatteryProfile and get filled in for all four boards in the same commit; tools/check_boards.py enforces that. If they turn out to be the same, say so in a comment with the measurements, so the next person does not re-ask.
battery_diag.cpp keeps a deliberate copy of the charge-inference constants so that what it shows is what the product will do. CLAUDE.md is explicit: change one, change both.
- Update the FNK0104B limitations note in
README.md and the battery section of src/hal/CLAUDE.md with whatever is measured.
Reference
- Dev pack in use: 3000 mAh / 11.1 Wh, single-cell LiPo (3.7 V nominal), MX1.25 connector.
- The E32R28T-1 measurements and the reasoning behind every existing constant are in the comment block at the top of
src/hal/BoardPower.cpp. Read it before changing anything there.
The battery percentage on the Freenove FNK0104B is believed correct: the divider is confirmed (GPIO9, ×2.0, from Freenove's own
Sketch_05.1_Battery_Voltage) and GPIO9 is ADC1_CH8, so Wi-Fi does not disturb it.The charging / discharging verdict is not validated.
getChargingState()infers it from cell voltage alone — this board, like the E32R28T-1, brings no charge-status line out to a GPIO — and every constant it infers with was measured against a different charger.Why the existing constants are suspect here
From
src/hal/BoardPower.cpp, tuned on the E32R28T-1's TP4054:V_CHARGER_HELD = 4.21 V— "no resting cell reaches this"CHARGE_STEP_V— the per-sample step that makes the icon respond to a cable within ~2 sCHARGE_WINDOW_MS = 45000— the low-pass window for everything in betweenCHARGE_FULL_V— guards the flat-window fallbackMeasured on the FNK0104B so far:
At 3.93 V,
V_CHARGER_HELDnever fires, so the verdict falls through to the 45-second trend. Whether that produces the right answer on this charger is unknown. The icon may simply be wrong.The one thing that must NOT be done
Do not add an
isBatteryPresent(). It existed once, as a constanttrue. On the E32R28T-1 it was disproved by measurement (recorded insrc/hal/BoardPower.cpp): pack+USB 4.224 V, USB with no pack 4.159 V, pack alone 4.066 V — the no-pack case sits between the other two, because the charger holds BAT at float voltage with or without a cell.The same inversion is already visible on this board: 4.09–4.16 V with no pack against 3.93 V with one. No threshold separates them in either direction. Do not reintroduce the question in a new form or retune a constant to answer it.
V_SENSOR_MAX(4.50 V) means the ADC is faulty, not "no pack".What needs doing
env:batdiagto the ESP32-S3. It currently extends[esp32_common](board = esp32dev), so it cannot be flashed to an S3 at all — the image carries a chip ID esptool refuses. A new[env:batdiag_fnk0104b]extending[esp32s3_common]with${board_fnk0104b.build_flags}is the starting point. NotebatdiagneedsARDUINO_USB_MODE=1andARDUINO_USB_CDC_ON_BOOT=1on this board — it has no USB-UART bridge, so without them serial output goes nowhere.ms,raw,adc_mv,cell_mv,pct,stateover a full discharge — the 3000 mAh pack makes this a long run).BatteryProfileand get filled in for all four boards in the same commit;tools/check_boards.pyenforces that. If they turn out to be the same, say so in a comment with the measurements, so the next person does not re-ask.battery_diag.cppkeeps a deliberate copy of the charge-inference constants so that what it shows is what the product will do.CLAUDE.mdis explicit: change one, change both.README.mdand the battery section ofsrc/hal/CLAUDE.mdwith whatever is measured.Reference
src/hal/BoardPower.cpp. Read it before changing anything there.