Summary
device_db.yaml contains 5 Telink entries whose BT<pin> token points at a GPIO that has
no ADC channel on the TLSR825x. The reported battery percentage is then meaningless, and the
failure is completely silent — no error, no warning.
The symptom is not deterministic. An invalid pin leaves gpio_no = 0, which selects no
channel at all; the resulting reading is undefined and saturates either low or high depending
on the pin. I measured both cases on the same PCB family, using the same partially discharged
cell:
| Unit |
BT pin |
Before fix |
After BTB5 |
Cell (multimeter) |
Δ |
_TZ3000_itb0omhv (TS0041) #1 |
C2 |
stuck 0% |
83% → 2830 mV |
2.860 V |
−30 mV |
_TZ3000_tzvbimpq (TS0042) #1 |
D2 |
stuck 100% |
89% → 2890 mV |
2.860 V |
+30 mV |
_TZ3000_tzvbimpq (TS0042) #2 |
D2 |
0% * |
93% → 2930 mV |
2.956 V |
−26 mV |
_TZ3000_itb0omhv (TS0041) #2 |
C2 |
stuck 0% |
100% (fresh cell, clamped) |
>3.0 V |
— |
After the fix, the three units measured against a partially discharged cell track its real
voltage within ±30 mV (chip-to-chip ADC spread), i.e. about 3 percentage points. The fourth was
tested with a fresh cell: 100% is ambiguous with the cluster default on its own, but the
transition 0% → 100% is not — a broken reading stays at 0%.
* Note the two TS0042 units are the same model, same manufacturer name, same BTD2 config
string, yet one saturated at 100% and the other at 0%. That is strong evidence the reading is
genuinely undefined rather than merely miscalibrated. (Caveat: on unit #2 the 0% was observed
over ~97 s and did not survive to a periodic refresh before I applied the fix, so treat it as
strongly suggestive rather than proven.)
Worth noting the stuck-100% case is the more dangerous: it never raises a low-battery alert, so
the device simply dies one day with no warning. A stuck 0% at least gets noticed.
I traced this on the TS0041, confirmed the root cause in the Telink SDK, fixed it by changing
one token, and then reproduced the whole thing on the TS0042 — different entry, different pin,
different symptom, same fix.
Root cause
battery_init() always uses VBAT mode:
hal_adc_init(HAL_ADC_INPUT_VBAT, battery->pin);
On the 8258 the pin is genuinely used — unlike newer cores which discard it
(proj/drivers/drv_adc.c):
#elif defined(MCU_CORE_8258) || defined(MCU_CORE_8278)
} else if (mode == DRV_ADC_VBAT_MODE) {
adc_vbat_init(pin); // B91 / TL321X / TL323X do `(void)pin;`
}
And adc_vbat_init() → adc_vbat_pin_init() (platform/chip_8258/adc.c, SDK V3.6.8.6):
void adc_vbat_pin_init(GPIO_PinTypeDef pin)
{
gpio_set_func(pin, AS_GPIO);
gpio_set_input_en(pin, 0);
gpio_set_output_en(pin, 1); // pin driven as OUTPUT
gpio_write(pin, 1); // forced HIGH -> sits at VDD
for (i = 0; i < 10; i++)
if (pin == ADC_GPIO_tab[i]) { gpio_no = i+1; break; }
adc_set_input_mode(ADC_MISC_CHN, DIFFERENTIAL_MODE);
adc_set_ain_channel_differential_mode(ADC_MISC_CHN, gpio_no, GND);
}
So VBAT mode drives the chosen pin high and measures that pad differentially against GND.
The pin must therefore (a) be free of any external load, and (b) exist in the ADC
table:
const GPIO_PinTypeDef ADC_GPIO_tab[10] = {
GPIO_PB0, GPIO_PB1, GPIO_PB2, GPIO_PB3,
GPIO_PB4, GPIO_PB5, GPIO_PB6, GPIO_PB7,
GPIO_PC4, GPIO_PC5
};
C2, D2 and D4 are not in that table. The loop finds no match, gpio_no stays 0
— not a valid channel, the valid range being 1..10 — and the ADC ends up sampling nothing
meaningful. Note the lookup has no failure path: gpio_no = 0 is passed on silently.
What the reading then becomes is undefined, and both clamps in battery_get_status() can be
hit depending on the pin:
if (voltage_mv < battery->voltage_min) status.charge = 0; // observed on C2
else if (voltage_mv > battery->voltage_max) status.charge = charge_range; // observed on D2
Hence the two opposite symptoms in the table above. Note the pin does not determine which
clamp is hit: both BTD2 units are the same model with the same config string, and one
saturated high while the other saturated low.
Affected entries
| Entry |
BT pin |
In ADC_GPIO_tab? |
Status in db |
REMOTE_TUYA_SWITCH_TS0041 |
C2 |
❌ |
mostly_supported |
REMOTE_TUYA_SWITCH_TS0042 |
D2 |
❌ |
mostly_supported |
REMOTE_TUYA_SWITCH_TS0043 |
D4 |
❌ |
mostly_supported |
REMOTE_TUYA_SWITCH_TS0043_1 |
D4 |
❌ |
fully_supported |
REMOTE_IHSENO_TS0044 |
D2 |
❌ |
fully_supported |
The other 9 Telink entries all use B5 or C5 and are fine.
The four REMOTE_TUYA_SWITCH_* entries all come from the pinout contributed in #293, which
documented switch and LED pins but no battery pin — a switch or LED pin appears to have
been reused to fill the field. Note two of the affected entries are currently marked
fully_supported.
Fix (verified)
For REMOTE_TUYA_SWITCH_TS0041:
- config_str: itb0omhv;TS0041-MOES;SC2u;IC4;BTC2;M;
+ config_str: itb0omhv;TS0041-MOES;SC2u;IC4;BTB5;M;
I inspected the PCB: TX, RX, B5, B4 and A0 have no pads at all on this board,
so B5 is both ADC-capable and completely unloaded — ideal for VBAT mode.
Verification — a healthy 3.0 V cell is useless here, since it produces 3000 mV / 100%,
identical to the cluster defaults. I used a partially discharged cell instead:
|
|
| Cell, open circuit (multimeter) |
2.86 V |
battery reported after fix |
83 % |
Back-calculation: 83% → charge_ZCL = 166 → V = 2000 + 166 × 1000/200 = 2830 mV, i.e.
2.83 V under load for 2.86 V open circuit. Neither the defaults (100%) nor the failure mode
(0%) can produce 83%.
Before the fix this device reported 0% at every 5-minute refresh.
Verified on two of the five entries (TS0041 itb0omhv, TS0042 tzvbimpq), across four
physical units, two of each. The remaining three are untested; REMOTE_TUYA_SWITCH_TS0043* is
the same PCB, so BTB5 is very likely correct there too, but that should be confirmed by
someone with the hardware.
The TS0041 populates only S2 and the TS0042 only S1/S3 — same board, different front-faces, as
documented in #293.
Unrelated side note: itb0omhv is not always a 1-gang
REMOTE_TUYA_SWITCH_TS0041 describes _TZ3000_itb0omhv as a 1-gang, and #293 notes it "uses
only S2 / S2 LED". One of my two units has three push-buttons actually populated and wired
(verified on the board: S1=D2, S2=C2, S3=C3, LEDs D4/C4/D7) — the stock Tuya firmware simply
never reads S1 and S3, so two physical buttons are dead out of the box.
Writing a 3-gang config string turns it into a fully working 3-gang:
pkeqinnt;TS0043-MOES;SD2u;ID4;SC2u;IC4;SC3u;ID7;BTB5;M;
Three endpoints, three genOnOff/genLevelCtrl client pairs, three multistate inputs. Nothing
to fix in the firmware — it is a nice illustration that the gang count is a config line, not a
product property. It might be worth a note in supported_devices.md that this variant ships in
several front-face versions on a shared PCB.
Suggested guard
A schema check would close this whole class of bug: for any mcu matching TLSR825x, the
BT pin must be in {B0..B7, C4, C5}. Roughly ten lines against device_db.yaml.
Two minor notes
- Silent failure. When the
BT pin has no ADC channel, nothing is logged anywhere —
gpio_no = 0 propagates through the whole stack. A build-time or boot-time warning would
have saved a long investigation.
- Z2M UI,
device_config field. It applies on change, with no Save button — on an
attribute that writes to NV and reboots the device. A confirmation step would be safer.
Bonus: OTA conversion of _TZ3000_itb0omhv works (no disassembly needed)
I could not find any report of an OTA conversion for this variant: #293 contains only
wired-flash reports (two of them experienced as bricking), and #424 is a documentation
question from someone following the wired procedure. I may well have missed something —
Discord in particular — so I am not claiming a first, just adding a data point with
reproducible conditions:
- image
tlc_switch-1.1.2-8542fc05-from_tuya.zigbee (imageType 54179 = stock image type)
- Z2M 2.13.0-1, index
index_end_device.json, both converters installed
- LQI 220 (device moved next to the coordinator), fresh CR2032
image_block_response_delay 250 → 50 ms
- transfer completed in 383 s
Two things that were not obvious:
- The device is a sleeping end device: it must be woken immediately before clicking
Check for updates, and kept awake until the transfer starts. A first attempt failed
because the presses came 25 s after the 60 s window had closed. Once the transfer is
running, the device polls autonomously and no further presses are needed.
- After flashing, Z2M refreshed the identifiers (
modelId, manufName, appVersion)
during configure, but not the endpoint descriptors — outClusterList stayed as the
stock [genOta, genTime], so no client clusters and no binding possible. Neither
device_announce, nor the automatic configure, nor a manual Reconfigure fixed it.
Only a factory reset followed by a clean re-join forced the coordinator to re-read the
descriptors. It might be worth making that an explicit step in updating.md.
After the clean re-join, endpoint 1 exposes genOta, genOnOff and genLevelCtrl as output
clusters, the phantom ep2-4 are gone, and direct binding works (short press = toggle, hold =
level ramp, release = stop).
Summary
device_db.yamlcontains 5 Telink entries whoseBT<pin>token points at a GPIO that hasno ADC channel on the TLSR825x. The reported battery percentage is then meaningless, and the
failure is completely silent — no error, no warning.
The symptom is not deterministic. An invalid pin leaves
gpio_no = 0, which selects nochannel at all; the resulting reading is undefined and saturates either low or high depending
on the pin. I measured both cases on the same PCB family, using the same partially discharged
cell:
BTpinBTB5_TZ3000_itb0omhv(TS0041) #1C2_TZ3000_tzvbimpq(TS0042) #1D2_TZ3000_tzvbimpq(TS0042) #2D2_TZ3000_itb0omhv(TS0041) #2C2After the fix, the three units measured against a partially discharged cell track its real
voltage within ±30 mV (chip-to-chip ADC spread), i.e. about 3 percentage points. The fourth was
tested with a fresh cell: 100% is ambiguous with the cluster default on its own, but the
transition 0% → 100% is not — a broken reading stays at 0%.
* Note the two TS0042 units are the same model, same manufacturer name, same
BTD2configstring, yet one saturated at 100% and the other at 0%. That is strong evidence the reading is
genuinely undefined rather than merely miscalibrated. (Caveat: on unit #2 the 0% was observed
over ~97 s and did not survive to a periodic refresh before I applied the fix, so treat it as
strongly suggestive rather than proven.)
Worth noting the stuck-100% case is the more dangerous: it never raises a low-battery alert, so
the device simply dies one day with no warning. A stuck 0% at least gets noticed.
I traced this on the TS0041, confirmed the root cause in the Telink SDK, fixed it by changing
one token, and then reproduced the whole thing on the TS0042 — different entry, different pin,
different symptom, same fix.
Root cause
battery_init()always uses VBAT mode:On the 8258 the pin is genuinely used — unlike newer cores which discard it
(
proj/drivers/drv_adc.c):And
adc_vbat_init()→adc_vbat_pin_init()(platform/chip_8258/adc.c, SDK V3.6.8.6):So VBAT mode drives the chosen pin high and measures that pad differentially against GND.
The pin must therefore (a) be free of any external load, and (b) exist in the ADC
table:
C2,D2andD4are not in that table. The loop finds no match,gpio_nostays0— not a valid channel, the valid range being 1..10 — and the ADC ends up sampling nothing
meaningful. Note the lookup has no failure path:
gpio_no = 0is passed on silently.What the reading then becomes is undefined, and both clamps in
battery_get_status()can behit depending on the pin:
Hence the two opposite symptoms in the table above. Note the pin does not determine which
clamp is hit: both
BTD2units are the same model with the same config string, and onesaturated high while the other saturated low.
Affected entries
BTpinADC_GPIO_tab?REMOTE_TUYA_SWITCH_TS0041C2REMOTE_TUYA_SWITCH_TS0042D2REMOTE_TUYA_SWITCH_TS0043D4REMOTE_TUYA_SWITCH_TS0043_1D4REMOTE_IHSENO_TS0044D2The other 9 Telink entries all use
B5orC5and are fine.The four
REMOTE_TUYA_SWITCH_*entries all come from the pinout contributed in #293, whichdocumented switch and LED pins but no battery pin — a switch or LED pin appears to have
been reused to fill the field. Note two of the affected entries are currently marked
fully_supported.Fix (verified)
For
REMOTE_TUYA_SWITCH_TS0041:I inspected the PCB:
TX,RX,B5,B4andA0have no pads at all on this board,so
B5is both ADC-capable and completely unloaded — ideal for VBAT mode.Verification — a healthy 3.0 V cell is useless here, since it produces
3000 mV / 100%,identical to the cluster defaults. I used a partially discharged cell instead:
batteryreported after fixBack-calculation: 83% →
charge_ZCL = 166→V = 2000 + 166 × 1000/200 = 2830 mV, i.e.2.83 V under load for 2.86 V open circuit. Neither the defaults (100%) nor the failure mode
(0%) can produce 83%.
Before the fix this device reported 0% at every 5-minute refresh.
Verified on two of the five entries (TS0041
itb0omhv, TS0042tzvbimpq), across fourphysical units, two of each. The remaining three are untested;
REMOTE_TUYA_SWITCH_TS0043*isthe same PCB, so
BTB5is very likely correct there too, but that should be confirmed bysomeone with the hardware.
The TS0041 populates only S2 and the TS0042 only S1/S3 — same board, different front-faces, as
documented in #293.
Unrelated side note:
itb0omhvis not always a 1-gangREMOTE_TUYA_SWITCH_TS0041describes_TZ3000_itb0omhvas a 1-gang, and #293 notes it "usesonly S2 / S2 LED". One of my two units has three push-buttons actually populated and wired
(verified on the board: S1=D2, S2=C2, S3=C3, LEDs D4/C4/D7) — the stock Tuya firmware simply
never reads S1 and S3, so two physical buttons are dead out of the box.
Writing a 3-gang config string turns it into a fully working 3-gang:
Three endpoints, three
genOnOff/genLevelCtrlclient pairs, three multistate inputs. Nothingto fix in the firmware — it is a nice illustration that the gang count is a config line, not a
product property. It might be worth a note in
supported_devices.mdthat this variant ships inseveral front-face versions on a shared PCB.
Suggested guard
A schema check would close this whole class of bug: for any
mcumatchingTLSR825x, theBTpin must be in{B0..B7, C4, C5}. Roughly ten lines againstdevice_db.yaml.Two minor notes
BTpin has no ADC channel, nothing is logged anywhere —gpio_no = 0propagates through the whole stack. A build-time or boot-time warning wouldhave saved a long investigation.
device_configfield. It applies on change, with no Save button — on anattribute that writes to NV and reboots the device. A confirmation step would be safer.
Bonus: OTA conversion of
_TZ3000_itb0omhvworks (no disassembly needed)I could not find any report of an OTA conversion for this variant: #293 contains only
wired-flash reports (two of them experienced as bricking), and #424 is a documentation
question from someone following the wired procedure. I may well have missed something —
Discord in particular — so I am not claiming a first, just adding a data point with
reproducible conditions:
tlc_switch-1.1.2-8542fc05-from_tuya.zigbee(imageType 54179 = stock image type)index_end_device.json, both converters installedimage_block_response_delay250 → 50 msTwo things that were not obvious:
Check for updates, and kept awake until the transfer starts. A first attempt failed
because the presses came 25 s after the 60 s window had closed. Once the transfer is
running, the device polls autonomously and no further presses are needed.
modelId,manufName,appVersion)during
configure, but not the endpoint descriptors —outClusterListstayed as thestock
[genOta, genTime], so no client clusters and no binding possible. Neitherdevice_announce, nor the automaticconfigure, nor a manual Reconfigure fixed it.Only a factory reset followed by a clean re-join forced the coordinator to re-read the
descriptors. It might be worth making that an explicit step in
updating.md.After the clean re-join, endpoint 1 exposes
genOta,genOnOffandgenLevelCtrlas outputclusters, the phantom ep2-4 are gone, and direct binding works (short press = toggle, hold =
level ramp, release = stop).