Rendered from firmware/README.md in the repository at build time.
Estimates are labelled as such in the source; nothing on this page is a measured result unless it says so.
ValiTilt firmware (esp-matter / ESP-IDF skeleton)#
Status: NOT COMPILED in this pack. esp-matter is large (~several GB with the
connectedhomeip submodules), so it was not installed here. The code follows the
esp-matter release/v1.4 API shape (checked against the public headers on
2026-10-17: endpoint::window_covering_device::create,
cluster::window_covering::feature::tilt::add(cluster, &cfg),
...::position_aware_tilt::add(cluster, &cfg)). On esp-matter main the
endpoint namespace is endpoint::window_covering and features are requested with
config.window_covering.feature_flags - build with -DVT_ESP_MATTER_NEW_API=1
(see create_blind_endpoint() in main/app_main.cpp). Expect small fixes on the
first build.
What it does#
- One Matter node,
VT_NUM_BLINDS(default 2) endpoints of device type Window Covering (0x0202), cluster Window Covering (0x0102) with Tilt + PositionAwareTilt only (no Lift). Type =TiltBlindTiltOnly, EndProductType =TiltOnlyInteriorBlind. TargetPositionTiltPercent100ths(0 = open/horizontal, 10000 = closed) is mapped to microsteps between the calibrated soft end stops; progress is written back toCurrentPositionTiltPercent100ths~4x per second and at the end of each move.- Motion: GPTimer step ISR, trapezoid ramp, one motor at a time (power budget), drivers disabled between moves (no hold current; the worm gear + 28BYJ-48 gearbox hold position).
- TMC2209 over single-wire UART: StealthChop, internal current reference (VREF pot
ignored), run current
VT_IRUN_MA, gentle homing currentVT_IHOME_MA, 1/8 microstepping. ULN2003 backend for the stock 5-wire 28BYJ-48. - Guided calibration (no sensor needed), low-current re-homing with small overtravel into the closed-up hard stop (clutch slips), NVS persistence of calibration and last position.
- Optional AS5600 multi-turn counting + manual-move resync (
VT_HAVE_AS5600).
Files#
| file | purpose |
|---|---|
CMakeLists.txt |
project, pulls esp-matter components via ESP_MATTER_PATH |
main/app_config.h |
all pins and tunables (XIAO ESP32C6 pin map, currents, rpm, travel) |
main/app_main.cpp |
Matter node + window covering endpoints, attribute callback, button actions |
main/tilt_motor.* |
step generation, ramp, serialized moves, sleep |
main/tmc2209.* |
UART register config (GCONF, CHOPCONF, IHOLD_IRUN, PWMCONF, TPOWERDOWN) |
main/uln2003.* |
half-step backend for ULN2003 |
main/as5600.* |
optional magnetic encoder, multi-turn accumulate |
main/calibration.* |
guided calibration, re-home, NVS |
main/ui.* |
button (short / 1-3 s / 3-10 s / >10 s) + LED patterns |
sdkconfig.defaults |
ESP32-C6, Thread FTD, BLE commissioning, USB-Serial-JTAG console |
sdkconfig.defaults.wifi |
overlay: Matter over Wi-Fi instead of Thread |
sdkconfig.defaults.battery |
overlay: Thread SED / ICD (battery variant, unverified) |
main/power_source.*, main/power_policy.* |
solar variant: battery reporting (Power Source cluster) + low-battery move policy |
partitions.csv |
4 MB layout as in esp-matter examples |
Solar + LiFePO4 variant (VT_POWER_SOLAR=1, NOT COMPILED)#
See ../docs/power-solar.md. Build: idf.py -D VT_POWER_SOLAR=1 -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.battery" build.
main/power_source.*- VBAT ADC (D2), energy ledger + LiFePO4 voltage anchors, Power Source cluster (0x002F) on endpoint 0 with Battery + Rechargeable features (BatVoltage, BatPercentRemaining, BatChargeLevel, BatChargeState, BatCapacity, EndpointList). Usescluster::power_source::createandattribute::create_bat_*(names checked in esp-matter release/v1.4 headers; not built).main/power_policy.*- adaptive move scheduling: NORMAL / ECO / LOW (move budget, opt-in harvest parking) / CRITICAL (only the button or moves toward open); re-home daily, weekly or never by level.tilt_motor.cpp- the motor rail (TPS61023 EN + VIO load switch) is powered only while moving; TMC2209 re-configured after each power-up (TMC2209 draws ~7-10 mA from VM even with ENN high); VBAT sag abort.- Pin changes: STEP_B → GPIO21, DIR shared, GPIO2 = VBAT ADC, GPIO18 = MOT_PWR_EN, LED → GPIO15.
- TODO: re-report Target = Current after a deferred move; harvest credit from VEML7700 lux; optional Wired Power Source for USB-C; GPIO hold of MOT_PWR_EN low in light sleep.
Build (once ESP-IDF + esp-matter are installed)#
# one-time: ESP-IDF v5.2+ (v5.4 recommended by recent esp-matter) and esp-matter
git clone --depth 1 -b release/v1.4 https://github.com/espressif/esp-matter.git
cd esp-matter && ./install.sh && cd ..
. $IDF_PATH/export.sh && . esp-matter/export.sh
cd valitilt/firmware
idf.py set-target esp32c6
idf.py build # Thread (default)
# or Wi-Fi:
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.wifi" build
idf.py -p /dev/ttyACM0 flash monitor
Compare sdkconfig.defaults with esp-matter/examples/light/sdkconfig.defaults*
for your esp-matter version (option names move between releases).
Commissioning (local, no cloud needed for control)#
The test build uses the default test DAC/PAI and the test setup code printed in
the log (SetupQRCode: [MT:...], manual code e.g. 34970112332 for the default
test passcode 20202021 / discriminator 3840). Controllers warn about an
"uncertified device" - accept it for a DIY device. For a permanent install,
generate your own passcode/discriminator with esp-matter-mfg-tool.
- Power the controller box (LED slow-blinks = not calibrated / not commissioned).
- Google Home: Google Home app -> Devices -> Add -> Matter-enabled device -> scan the QR code. Needs a Google hub (Nest Hub 2nd gen / Max, Nest Wifi Pro, Google TV Streamer...). For Thread, that hub must be a Thread border router. Note: tilt UI support in Google Home is reported as partial - test it; Home Assistant/Apple give fuller tilt control.
- Apple Home: Home app -> + -> Add Accessory -> scan. Thread needs a HomePod mini / HomePod 2 / Apple TV 4K (Ethernet/Wi-Fi+Thread model) as border router.
- Home Assistant: Matter integration (Matter Server add-on) + companion app
for BLE commissioning. Thread needs an OpenThread Border Router (e.g. HA
Connect ZBT-1/SkyConnect or any Matter TBR on the same network). Shows as a
coverwith tilt position. - Aqara / Xiaomi: an Aqara Matter controller hub (e.g. Hub M3) lists Window Covering (0x0202) as a supported Matter type; the M3 is also a Thread border router. Xiaomi Home Matter controller support depends on the hub/region - check before relying on it.
- Tapo: as of TP-Link's FAQ (checked 2026-10-17), the Tapo H500 Matter controller only adds third-party plugs and bulbs and has no Thread -> not usable for this device. You can still use Tapo for other things and run ValiTilt in Google/Apple/HA via multi-admin (share the device from the first controller).
- Multi-admin: after commissioning in one ecosystem, use its "share / pair with another app" function to add a second controller.
Calibration & buttons (controller box button)#
- short press: toggle both blinds open (horizontal) <-> closed (slats UP)
- 1-3 s: re-home both (low current into the closed-up stop = CCW end seen from below, back to last position) - use after manual turning
- 3-10 s: guided calibration, blind A then B:
- turns toward closed-UP (counter-clockwise seen from below) slowly at low current -> press when the slats are fully up (or let the clutch slip at the stop)
- turns toward closed-DOWN (clockwise seen from below) -> press at the stop. Measured travel is about 1 knob turn
in total (
VT_DEFAULT_SPAN_TURNS), each leg is capped atVT_CALIB_MAX_TURNS(2); wrong way ->VT_DIR_INVERT_A/_B - turns back -> press when the slats are horizontal
- > 10 s: Matter factory reset
Direction and Matter mapping (M2, confirmed 2026-10-17)#
Looking up at the knob from below: CCW end = closed, slats UP (preferred closed, position 0, home);
~0.5 turn CW = fully open; CW end = closed, slats DOWN toward the room (position span).
TiltPercent100ths: 0 = open = calibrated horizontal (default span/2); 10000 = closed = soft_margin
next to the slats-up stop; linear in between (VT_CLOSE_DIR_DOWN = 0). The slats-down half is not
reachable from Matter (reads as 0 %); set VT_CLOSE_DIR_DOWN = 1 to make slats-down the closed end
instead - only one closed end per build. VT_DIR_INVERT_A/_B flips a head wired the other way.
Unverified / to check on hardware#
- TMC2209 VM minimum is 4.75 V: USB 5 V after polyfuse + cable must stay above it under load (measure).
- StallGuard is NOT used: on a 28BYJ-48 (high coil resistance, ~50 mA, slow) the back-EMF signal is likely too small (unverified). Homing relies on low current + clutch slip.
attribute::update()is assumed to take the Matter lock itself and to trigger the PRE_UPDATE callback (used for the button toggle) - verify for your version; otherwise calltilt_motor::move_todirectly there.- The CHIP window-covering server updates
OperationalStatusfrom target/current; this skeleton does not set it explicitly. - Direction:
+steps= closed-DOWN (CW seen from below). If reversed, swap one coil pair.