You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(levoit): Core 200S filter life from the MCU, and a real filter reset - #67
The Core 200S MCU keeps its own filter counter and reports it in the status frame. The component ignored it and published an ESP-side estimate derived from used_cadr instead, which sat at 100% and never moved.
Root cause is a payload offset: the Core 200S was decoded with the Core 300S layout, but it follows the Core 400S one from byte 6 on. Byte 6 is the filter percentage and byte 7 is the display flag. Reading byte 6 as a bool also meant Display reported permanently ON, since a filter value of 100 is never zero.
filter_life_left (and filter_life_mcu) now publish the MCU value, and the ESP estimate is suppressed on this model so it cannot overwrite it, via a new filter_life_from_mcu() predicate. Other models are untouched.
Display now reads byte 7.
Filter Reset sends the MCU's own reset (01 E4 A5), captured from the stock firmware. resetFilter had no core implementation at all before - it existed only for the Superior 6000S - so the button previously cleared the CADR counters and left the filter untouched. It sits next to setFilterLedOff/On (01 E2 A5) in the same command family.
Bumps the component to 1.5.0 and folds this plus the filter_low device-class fix into the 1.5.0 changelog entry.
Evidence in devices/levoit-core200s/uart: startup.txt shows byte 6 constant at 0x64 while the stock app reported 100% filter life, and byte 7 tracking a boolean; filter_reset.txt captures the reset command.
Confirmed: the byte-7 display flag and the reset command. Still assumed: that byte 6 is specifically filter life rather than another 0-100 value - a capture at 99% would settle it outright.
The Core 200S MCU keeps its own filter counter and reports it in the status
frame. The component ignored it and published an ESP-side estimate derived
from used_cadr instead, which sat at 100% and never moved.
Root cause is a payload offset: the Core 200S was decoded with the Core 300S
layout, but it follows the Core 400S one from byte 6 on. Byte 6 is the filter
percentage and byte 7 is the display flag. Reading byte 6 as a bool also meant
Display reported permanently ON, since a filter value of 100 is never zero.
- filter_life_left (and filter_life_mcu) now publish the MCU value, and the
ESP estimate is suppressed on this model so it cannot overwrite it, via a
new filter_life_from_mcu() predicate. Other models are untouched.
- Display now reads byte 7.
- Filter Reset sends the MCU's own reset (01 E4 A5), captured from the stock
firmware. resetFilter had no core implementation at all before - it existed
only for the Superior 6000S - so the button previously cleared the CADR
counters and left the filter untouched. It sits next to setFilterLedOff/On
(01 E2 A5) in the same command family.
Bumps the component to 1.5.0 and folds this plus the filter_low device-class
fix into the 1.5.0 changelog entry.
Evidence in devices/levoit-core200s/uart: startup.txt shows byte 6 constant at
0x64 while the stock app reported 100% filter life, and byte 7 tracking a
boolean; filter_reset.txt captures the reset command.
Confirmed: the byte-7 display flag and the reset command. Still assumed: that
byte 6 is specifically filter life rather than another 0-100 value - a capture
at 99% would settle it outright.
Verified: Core 200S/300S/400S/600S all compile.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot caught a real miss on #67: the per-minute path in track_cadr_usage()
also published calculate_filter_life_left_percent() to FILTER_LIFE_LEFT, so on
a running Core200S the ESP estimate overwrote the MCU value every minute
despite the 10-second path being guarded. It only bites while the fan is on,
which is why it survived the first round of testing.
Rather than add a third copy of the guard, all three call sites now go through
publish_filter_stats_now(), which is the single place the estimate is computed
and the only place the filter_life_from_mcu() check lives. That also gives the
per-minute path the FILTER_LOW publish it was missing.
Verified: Core 200S and 400S compile; Everest Air - the only model where
push_filter_pct_if_changed() does anything - compiles too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This comment is now contradicted by the reset path added in core_commands.cpp and levoit_button.cpp: the ESP cannot edit the counter directly, but it can reset the MCU-owned value with 01 E4 A5. Please describe the value as MCU-owned and avoid saying it cannot be reset, otherwise this will mislead future changes to the reset behavior.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Core 200S MCU keeps its own filter counter and reports it in the status frame. The component ignored it and published an ESP-side estimate derived from used_cadr instead, which sat at 100% and never moved.
Root cause is a payload offset: the Core 200S was decoded with the Core 300S layout, but it follows the Core 400S one from byte 6 on. Byte 6 is the filter percentage and byte 7 is the display flag. Reading byte 6 as a bool also meant Display reported permanently ON, since a filter value of 100 is never zero.
Bumps the component to 1.5.0 and folds this plus the filter_low device-class fix into the 1.5.0 changelog entry.
Evidence in devices/levoit-core200s/uart: startup.txt shows byte 6 constant at 0x64 while the stock app reported 100% filter life, and byte 7 tracking a boolean; filter_reset.txt captures the reset command.
Confirmed: the byte-7 display flag and the reset command. Still assumed: that byte 6 is specifically filter life rather than another 0-100 value - a capture at 99% would settle it outright.
Verified: Core 200S/300S/400S/600S all compile.