Drive the heater blower motor from the MOSFET on D2 #90

Merged
rob merged 14 commits from feat/heater-blower-output into feat/heater-si7021-sensor 2026-10-10 14:24:41 +00:00
Collaborator

Adds the blower motor as a LevelEndpoint (EP_FAN, index 13) on D2, PWM 25 kHz 8-bit, failsafe autonomous. In main.cpp, a rising edge on the heater's blower output requests full speed and a falling edge switches off, both with origin automation. A manual or HA command therefore holds until the heater next changes state, and the ten second lockout applies. Index 6 is unchanged.

Based on #89, so this shows its commits until that merges.

Tested: pio test -e native and pio run -e node_heater. Not tested on hardware.

Ticket: CCS-WHP-12

Adds the blower motor as a `LevelEndpoint` (`EP_FAN`, index 13) on D2, PWM 25 kHz 8-bit, failsafe autonomous. In `main.cpp`, a rising edge on the heater's blower output requests full speed and a falling edge switches off, both with origin automation. A manual or HA command therefore holds until the heater next changes state, and the ten second lockout applies. Index 6 is unchanged. Based on #89, so this shows its commits until that merges. Tested: `pio test -e native` and `pio run -e node_heater`. Not tested on hardware. Ticket: CCS-WHP-12
Add Si7021 cabin temperature and humidity to the heater node
Some checks failed
Build and test / release (pull_request) Has been cancelled
Build and test / build (pull_request) Has been cancelled
08b6a5b24e
Fix humidity in the semantic class table and pin its value
Some checks failed
Build and test / release (pull_request) Has been cancelled
Build and test / build (pull_request) Has been cancelled
c1ea6c75b1
Default the humidity reading to the uint16 unknown sentinel
All checks were successful
Build and test / build (pull_request) Successful in 1m38s
Build and test / release (pull_request) Has been skipped
3d3357a592
Drive the heater blower motor from the MOSFET on D2
Some checks failed
Build and test / release (pull_request) Has been cancelled
Build and test / build (pull_request) Has been cancelled
ebd38a0524
Claude left a comment

Two real problems, one smaller point.

Two real problems, one smaller point.
@ -1232,1 +1233,4 @@
| 10 | `EP_SETTING` | Twait warm up (`0x20` byte 2) |
| 11 | `EP_TEMPERATURE` | Cabin temperature, from an Si7021 on I2C |
| 12 | `EP_HUMIDITY` | Cabin humidity, from the same Si7021 |
| 13 | `EP_FAN` | Blower motor on a MOSFET. Follows index 6 at full speed with origin automation, failsafe autonomous |
Author
Collaborator

Spec says the endpoint follows index 6, but it is a normal bus-writable Level endpoint, so a CMD_LEVEL from HA overrides it and Manual origin locks automation out for 10 s. Say that in the row (or make it non-writable), otherwise the spec and behaviour disagree.

Spec says the endpoint follows index 6, but it is a normal bus-writable Level endpoint, so a CMD_LEVEL from HA overrides it and Manual origin locks automation out for 10 s. Say that in the row (or make it non-writable), otherwise the spec and behaviour disagree.
@ -91,1 +112,3 @@
node.loop();
cabinSensor.loop();
const uint32_t now = ctrl::nowMs();
if (heater.blowerOn() != blowerWasOn) {
Author
Collaborator

Edge-only sync loses the edge if a request is refused. A manual command to index 13 starts the 10 s lockout; if blowerOn() flips inside it, request()/setOn() return false (commandAllowed), blowerWasOn is already updated, and the motor stays out of step with the heater until the next edge. Worst case the motor keeps running after the blower stops. Only set blowerWasOn when the call returns true, or compare against blowerMotor.isOn() each loop instead of keeping a flag.

Edge-only sync loses the edge if a request is refused. A manual command to index 13 starts the 10 s lockout; if blowerOn() flips inside it, request()/setOn() return false (commandAllowed), blowerWasOn is already updated, and the motor stays out of step with the heater until the next edge. Worst case the motor keeps running after the blower stops. Only set blowerWasOn when the call returns true, or compare against blowerMotor.isOn() each loop instead of keeping a flag.
Author
Collaborator

heater.blowerOn() is never cleared on link loss (blowerOn_ is only written from a status frame, heater_endpoint.cpp:147). If the heater link drops with the blower on, the motor stays at full speed with no way to stop it from this code. Gate on the link being up, or treat link loss as blower off.

heater.blowerOn() is never cleared on link loss (blowerOn_ is only written from a status frame, heater_endpoint.cpp:147). If the heater link drops with the blower on, the motor stays at full speed with no way to stop it from this code. Gate on the link being up, or treat link loss as blower off.
Retry the blower edge when the lockout refuses it
Some checks failed
Build and test / release (pull_request) Has been cancelled
Build and test / build (pull_request) Has been cancelled
341e4a40ca
Claude left a comment

One finding on 341e4a4.

One finding on 341e4a4.
@ -92,0 +115,4 @@
if (blowerOn != blowerWasOn) {
const bool applied =
blowerOn ? blowerMotor.request(blowerMotorFullSpeed, now, can::Origin::Automation) : blowerMotor.setOn(false, now, can::Origin::Automation);
if (applied) blowerWasOn = blowerOn;
Author
Collaborator

The retry floods the bus. While the endpoint is locked out (10 s after a manual command), request()/setOn() returns false via refuse(), which calls markDirty(). reportDue() is then true on every pass, so node.loop sends a state frame every loop iteration until the lockout ends. Fix: rate-limit the retry (e.g. only retry once the lockout has lapsed, using blowerMotor.lockedOut(now), or a short retry interval) instead of calling every loop.

The retry floods the bus. While the endpoint is locked out (10 s after a manual command), request()/setOn() returns false via refuse(), which calls markDirty(). reportDue() is then true on every pass, so node.loop sends a state frame every loop iteration until the lockout ends. Fix: rate-limit the retry (e.g. only retry once the lockout has lapsed, using blowerMotor.lockedOut(now), or a short retry interval) instead of calling every loop.
rob changed target branch from main to feat/heater-si7021-sensor 2026-10-10 12:58:23 +00:00
Skip the blower edge while the motor is locked out
All checks were successful
Build and test / build (pull_request) Successful in 1m35s
Build and test / release (pull_request) Has been skipped
f7c56f6bee
Claude left a comment

Round 3: the state-frame flood is fixed. Two things left.

Round 3: the state-frame flood is fixed. Two things left.
@ -103,1 +113,3 @@
node.loop();
const uint32_t now = ctrl::nowMs();
const bool blowerOn = heater.blowerOn();
if (blowerOn != blowerWasOn && !blowerMotor.lockedOut(now)) {
Author
Collaborator

Stale blower state. HeaterEndpoint::loseLink (heater_endpoint.cpp:61) never clears blowerOn_, so if the heater link drops while the blower runs, heater.blowerOn() stays true and the motor stays at full speed indefinitely. Drive the motor off blowerOn && !linkLost, or clear blowerOn_ in loseLink. The latter also fixes HeaterBlowerInput.

Stale blower state. `HeaterEndpoint::loseLink` (heater_endpoint.cpp:61) never clears `blowerOn_`, so if the heater link drops while the blower runs, `heater.blowerOn()` stays true and the motor stays at full speed indefinitely. Drive the motor off `blowerOn && !linkLost`, or clear `blowerOn_` in `loseLink`. The latter also fixes `HeaterBlowerInput`.
Author
Collaborator

Edge-only sync. After a manual command the motor never realigns with the heater until the next blower edge. A manual off while the blower runs stays off. A manual on while the blower is off keeps spinning with nothing to stop it. Compare against the motor's state (blowerMotor.isOn() != blowerOn) instead of a blowerWasOn flag; that also drops the flag and the retry logic. This glue sits in main.cpp with no test, so the lockout-retry path is unpinned.

Edge-only sync. After a manual command the motor never realigns with the heater until the next blower edge. A manual off while the blower runs stays off. A manual on while the blower is off keeps spinning with nothing to stop it. Compare against the motor's state (`blowerMotor.isOn() != blowerOn`) instead of a `blowerWasOn` flag; that also drops the flag and the retry logic. This glue sits in `main.cpp` with no test, so the lockout-retry path is unpinned.
rob added 11 commits 2026-10-10 14:24:05 +00:00
Regulate the cabin by blower speed from a stored heater setpoint
Some checks failed
Build and test / release (pull_request) Has been cancelled
Build and test / build (pull_request) Has been cancelled
fbac6d591b
Skip the idle blower write and test the setpoint edge cases
Some checks failed
Build and test / release (pull_request) Has been cancelled
Build and test / build (pull_request) Has been cancelled
17e3b63e65
Run the blower controller every pass and integrate per reading
Some checks failed
Build and test / release (pull_request) Has been cancelled
Build and test / build (pull_request) Has been cancelled
eca188558c
Pin the blower level holding between readings
Some checks failed
Build and test / release (pull_request) Has been cancelled
Build and test / build (pull_request) Has been cancelled
083f0fb5f6
Apply the link-loss blower demand with failsafe origin
Some checks failed
Build and test / release (pull_request) Has been cancelled
Build and test / build (pull_request) Has been cancelled
7d9de0af46
Note the Si7021 wiring in the heater node
All checks were successful
Build and test / build (pull_request) Successful in 1m56s
Build and test / release (pull_request) Has been skipped
1f46064102
Poll the Si7021 every 100 ms and report cabin readings on change
Some checks failed
Build and test / release (pull_request) Has been cancelled
Build and test / build (pull_request) Has been cancelled
079194a00d
Test the status change on its own in the readout tests
Some checks failed
Build and test / release (pull_request) Has been cancelled
Build and test / build (pull_request) Has been cancelled
8596e44854
Limit blower writes to once a second and re-assert every 10 s
All checks were successful
Build and test / build (pull_request) Successful in 1m32s
Build and test / release (pull_request) Has been skipped
0ca99e367c
Merge pull request 'Poll the Si7021 every 100 ms and report cabin readings on change' (#92) from feat/heater-sensor-rate into feat/heater-cabin-control
All checks were successful
Build and test / build (pull_request) Successful in 2m32s
Build and test / release (pull_request) Has been skipped
1909942941
Reviewed-on: #92
Merge pull request 'Regulate the cabin by blower speed from a stored heater setpoint' (#91) from feat/heater-cabin-control into feat/heater-blower-output
All checks were successful
Build and test / build (pull_request) Successful in 2m59s
Build and test / release (pull_request) Has been skipped
db713f3874
Reviewed-on: #91
rob merged commit 60afdeadc0 into feat/heater-si7021-sensor 2026-10-10 14:24:41 +00:00
rob deleted branch feat/heater-blower-output 2026-10-10 14:24:41 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/CampervanControlSystems!90
No description provided.