Skip to content

fix(levoit): Core 200S filter life from the MCU, and a real filter reset - #67

Merged
tuct merged 3 commits into
mainfrom
fix/core200s-filter-life
Sep 26, 2026
Merged

tuct merged 3 commits into
mainfrom
fix/core200s-filter-life

Conversation

@tuct

@tuct tuct commented Sep 26, 2026

Copy link
Copy Markdown
Owner

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.

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 AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The minute-based path can still overwrite MCU filter life, and the README contract needs updating.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Fixes Core 200S filter-life decoding and reset behavior using MCU data and corrected status offsets.

Changes:

  • Adds MCU filter-life reporting and correct Display decoding.
  • Implements the Core 200S MCU filter-reset command.
  • Updates version, changelog, and UART evidence.
File Summary
README.md Documents the 1.5.0 changes.
devices/​levoit-core200s/​uart/​startup.txt Adds startup UART evidence.
devices/​levoit-core200s/​uart/​filter_reset.txt Adds filter-reset evidence.
components/​levoit/​levoit.h Adds MCU filter detection and version bump.
components/​levoit/​levoit.cpp Suppresses periodic ESP estimates.
components/​levoit/​core_status.cpp Decodes Core 200S filter and display values.
components/​levoit/​core_commands.cpp Implements the MCU reset command.
components/​levoit/​button/​levoit_button.cpp Sends the MCU filter reset.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread components/levoit/levoit.cpp Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Update the documented Core 200S example to use the 1.5.0 component release.

Review effort: Lite
Findings: 1 Low severity

Open (1)
Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Low severity Correct comment describing MCU-owned filter lifetime reset behavior

components/​levoit/​core_status.cpp:92

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.

Comment thread README.md
Comment on lines +451 to +456
* **Core 200S: filter life now comes from the MCU.** The MCU keeps its own
filter counter and reports it in the status frame; the component was
ignoring it and showing an ESP-side estimate derived from `used_cadr`, which
sat at 100% and never moved. `filter_life_left` (and `filter_life_mcu`, if
configured) now publish the real value, and the ESP estimate is suppressed on
this model so it cannot overwrite it. Other models are unchanged

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.

Addressed in 2198507.

Co-authored-by: tuct <2391725+tuct@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants