@@ -34,6 +34,14 @@ static hisense_status_cb_t s_status_cb = NULL;
3434static TaskHandle_t s_bus_task_handle = NULL ;
3535static bool s_initialized = false ;
3636static hisense_recommission_cb_t s_recommission_cb = NULL ;
37+ static hisense_recommission_cancel_cb_t s_recommission_cancel_cb = NULL ;
38+ /* Whole last 0x1E LINK reply, so a bench can diff the frame across a "77" press instead of
39+ * trusting the RE bit map. Seven presses left the documented bit (payload[4] 0x08/0x20) at
40+ * zero, so the map is unproven on THIS unit and the raw frame is the only ground truth. */
41+ static uint8_t s_last_link_frame[40 ];
42+ static uint8_t s_last_link_frame_len = 0 ;
43+ static hisense_link_frame_cb_t s_link_frame_cb = NULL ;
44+ static bool s_recommission_momentary = false ; // latched by a 1-frame pulse
3745static uint8_t s_last_link_req = 0 ; // masked "77" request bits from the last 0x1E reply (diag)
3846static uint8_t s_link_req_streak = 0 ; // consecutive 0x1E replies with the request asserted (debounce)
3947static bool s_recommission_latched = false ; // fired for the current sustained assertion (fire-once)
@@ -1047,22 +1055,28 @@ static void hisense_consume_status(size_t n)
10471055// so a single reflected/glitched frame must not trip a commissioning window. The
10481056// request stays asserted while the user holds "77", so a few frames of hold cleanly
10491057// separates a deliberate press from a 1-frame artefact. Not static: linked by tests.
1050- bool hisense_recommission_debounce (uint8_t req_bits, uint8_t *streak,
1051- bool *latched, uint8_t hold_frames)
1058+ int hisense_recommission_debounce (uint8_t req_bits, uint8_t *streak,
1059+ bool *latched, uint8_t hold_frames)
10521060{
10531061 if (req_bits != 0 ) {
10541062 if (*streak < 0xFF ) {
10551063 (*streak)++;
10561064 }
10571065 if (*streak >= hold_frames && !*latched) {
10581066 *latched = true ;
1059- return true ; // fire once for this sustained assertion
1067+ return 1 ; // fire once for this sustained assertion
10601068 }
1061- return false ;
1069+ return 0 ;
10621070 }
1063- *streak = 0 ; // request cleared -> reset + re-arm
1071+ /* Request cleared. If we had LATCHED, the user has taken the A/C out of "77" -- report the
1072+ * falling edge so the handler can shut the commissioning window it opened. Without this the
1073+ * window stayed open for its full duration after the user backed out, leaving the device
1074+ * joinable with nothing on the panel to indicate it. A clear that arrives BEFORE the latch
1075+ * (a glitch that never fired) reports nothing, because no window was ever opened. */
1076+ bool was_latched = *latched;
1077+ *streak = 0 ; // reset + re-arm
10641078 *latched = false ;
1065- return false ;
1079+ return was_latched ? - 1 : 0 ;
10661080}
10671081
10681082// Link-health edge detector (pure -> host-testable, #56). `silent` = link currently
@@ -1092,21 +1106,74 @@ static void hisense_check_link_reply(size_t n)
10921106 if (n <= 17 || s_msg_buf[13 ] != 0x1E ) {
10931107 return ;
10941108 }
1109+ { // snapshot the whole reply for bench diffing (see s_last_link_frame)
1110+ size_t cp = (n < sizeof (s_last_link_frame)) ? n : sizeof (s_last_link_frame);
1111+ for (size_t i = 0 ; i < cp; i++) s_last_link_frame[i] = s_msg_buf[i];
1112+ s_last_link_frame_len = (uint8_t ) cp;
1113+ if (s_link_frame_cb != NULL ) s_link_frame_cb (s_last_link_frame, s_last_link_frame_len);
1114+ }
10951115 uint8_t req = s_msg_buf[17 ] & HISENSE_LINK_REQ_RECOMMISSION ; // payload[4]
10961116 // Re-arm if the request TYPE changes while still asserted (e.g. RECONFIG->SMARTCFG
10971117 // with no intervening all-zero frame) so the second reason isn't swallowed by the latch.
10981118 if (req != 0 && req != s_last_link_req) {
10991119 s_link_req_streak = 0 ;
11001120 s_recommission_latched = false ;
11011121 }
1102- if (hisense_recommission_debounce (req, &s_link_req_streak, &s_recommission_latched,
1103- HISENSE_RECOMMISSION_HOLD_FRAMES )
1104- && s_recommission_cb != NULL ) {
1105- s_recommission_cb (s_msg_buf[17 ]);
1122+ /* Hold requirement depends on WHICH bit asserted, because only one of them can echo.
1123+ *
1124+ * bit3 (0x08, reconfig) doubles as our OUTBOUND prov_status, so a reflected or glitched
1125+ * frame could trip a commissioning window -- it needs the multi-frame hold.
1126+ *
1127+ * bit5 (0x20, smartcfg) is never transmitted by us: it is purely an A/C-originated request,
1128+ * so there is nothing to echo and nothing to debounce. Measured on the bench 2026-07-20:
1129+ * this unit asserts 0x20 for EXACTLY ONE ~1Hz frame per remote press (three presses ->
1130+ * LINK#93, #100, #114, never consecutive). Requiring 3 consecutive frames therefore made
1131+ * "77" impossible to trigger here -- the handler never once fired. Fire on the first frame.
1132+ *
1133+ * Mixed bits keep the conservative hold: if 0x08 is present the echo risk is present too. */
1134+ uint8_t hold = (req == HISENSE_LINK_REQ_SMARTCFG ) ? 1 : HISENSE_RECOMMISSION_HOLD_FRAMES ;
1135+ int edge = hisense_recommission_debounce (req, &s_link_req_streak, &s_recommission_latched,
1136+ hold);
1137+ if (edge > 0 ) {
1138+ /* Remember whether this was a MOMENTARY request. The A/C pulses 0x20 for one frame and
1139+ * drops it, so the very next reply is a falling edge -- which must NOT be read as "the
1140+ * user left 77", or the window would slam shut ~1s after opening. A sustained request
1141+ * (0x08 held for the hold period) genuinely does track the user's state, so its falling
1142+ * edge IS meaningful. */
1143+ s_recommission_momentary = (hold == 1 );
1144+ if (s_recommission_cb != NULL ) s_recommission_cb (s_msg_buf[17 ]);
1145+ } else if (edge < 0 ) {
1146+ bool momentary = s_recommission_momentary;
1147+ s_recommission_momentary = false ;
1148+ if (!momentary && s_recommission_cancel_cb != NULL ) {
1149+ s_recommission_cancel_cb (); // sustained request released -> user left "77"
1150+ }
11061151 }
11071152 s_last_link_req = req; // retained for diagnostics + reason-change detection
11081153}
11091154
1155+ void hisense_set_link_frame_cb (hisense_link_frame_cb_t cb)
1156+ {
1157+ s_link_frame_cb = cb;
1158+ }
1159+
1160+ uint8_t hisense_get_last_link_frame (uint8_t *out, uint8_t cap)
1161+ {
1162+ uint8_t n = s_last_link_frame_len < cap ? s_last_link_frame_len : cap;
1163+ for (uint8_t i = 0 ; i < n; i++) out[i] = s_last_link_frame[i];
1164+ return n;
1165+ }
1166+
1167+ uint8_t hisense_get_last_link_req (void )
1168+ {
1169+ return s_last_link_req;
1170+ }
1171+
1172+ void hisense_set_recommission_cancel_cb (hisense_recommission_cancel_cb_t cb)
1173+ {
1174+ s_recommission_cancel_cb = cb;
1175+ }
1176+
11101177void hisense_set_recommission_cb (hisense_recommission_cb_t cb)
11111178{
11121179 s_recommission_cb = cb;
0 commit comments