[READ-ONLY] Mirror of https://github.com/SantaClaas/embedded-fan-control.

Read the fan speed on boot instead of assuming the fans are off master

The fans keep spinning while the controller resets, but the controller assumed nothing about them: current_set_point started as None and last_fan_state at SetPoint::ZERO. The LEDs and the Home Assistant state were wrong until someone issued a command, and a Home Assistant "on" restored a set point that was never in effect. Reading holding registers (function code 0x03) is now implemented. ReadHoldingRegister asks for exactly one register, which keeps the response a fixed length, and Client::read_holding_register returns its contents. What both functions share is factored out of the write path rather than copied: - send_request drives the line, writes the frame and hands the line back - read_header reads the device address and function code, turns an exception frame into Error::Exception, and returns once the header is the answer that was asked for, so each transaction only reads its own body - A read is rejected if it announces a byte count other than the single register asked for. That is checked before the checksum, because a different length means the wrong bytes were just read and the checksum would fail without naming the actual problem - Exception now covers both functions: 0x03 (response too long) exists only for reads, and WriteRefused became AccessRefused because 0x04 also means a register that cannot be read (specification sections 1.3.1 and 1.3.3) - send_3 is now write_holding_register, to pair with read_holding_register fan_control_routine reads the set point before it waits for anything and sends it into the display Watch, so the LEDs, Home Assistant and the button all start from what the fans are actually doing. read_set_point retries with the write path's backoff, which is now shared, and gives up early if a set point is requested in the meantime: a command is worth more than the state being read, and an unreachable fan costs a timeout per attempt. current_set_point stays an Option rather than becoming a plain SetPoint. A fan that does not answer leaves its state genuinely unknown, and both places that use it already treat not knowing as its own case; filling it with a guess would make them silently wrong instead. last_fan_state no longer starts at zero and no longer records the set points Home Assistant asks for. mqtt_brain_routine watches the confirmed display state of both fans instead, so it is seeded from the boot read and also picks up the speeds set with the button, and it remembers only non-zero speeds since zero is what "on" restores from. It now records what a fan accepted rather than what was asked of it, so a failed write no longer leaves behind a speed that was never in effect. The display Watch went from two receivers to three for this, behind a DISPLAY_STATE_RECEIVERS alias so the count lives in one place. display_routine also reports whether the fans are on or off on the first update after boot. It only published that on a change, and the first update is not a change, so the speed reached Home Assistant without the state it needs to render it. The 250 ms alternating LED pattern documentation.md describes as "while the initial fan speed data is getting read from the fan" needed no work: it is what led_routine already plays before it has an LedState. It just describes reality now instead of blinking until the first command. Untested on hardware. The read path has never run: worth checking that the fans answer 0x03 at all, what they report for a fan that is off, and whether the value comes back with the four least significant bits the fan ignores zeroed or as they were written. Note this removes an accident that kept the fan ping-pong in TODO.md item 4 from happening on a cold boot. current_set_point could not be set before a successful write, and now it can, so a bus that dies right after boot reaches that loop too. Recorded there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>


+496 -147
6 changed files