Skip to content

Commit 9802106

Browse files
committed
feat(esp32): port AP-First gate to the ESP32, covering 0x3FD/0x3EE/0x370 (#100/#108/#52)
The ESP32 injected ungated, so on 2026.14.x the FSD-enable (0x3FD/0x3EE) fired on the AP activation edge — the steer-jerk @dunckencn reports on China 2026.8.3.6 HW3 (#108) — and per @jewelrylin's #100 finding the nag-killer 0x370 echo trips the same preflight and can block AP readiness on HW4. AP-First was Flipper-only. Add the AP-First gate to the ESP32: a new ap_first prefs key + web-dashboard toggle (off by default), fsd_ap_first_allows() (das_ap_state >= 2 held stable for AP_FIRST_STABLE_MS=1000), ap_unstable_tick_ms stamped each frame while AP is not engaged, and the gate applied to the 0x3FD, 0x3EE and 0x370 TX paths. Strictly conservative: it can only withhold injection until AP is engaged and stable, never inject more; default-off preserves current behavior. Note vs the Flipper: the Flipper gates only 0x3FD/0x3EE; this also gates the nag 0x370 (the #100 extension). The Flipper side is unchanged in this commit. All 7 ESP32 envs build; host suite (234) unaffected (shared core untouched).
1 parent 1dacace commit 9802106

5 files changed

Lines changed: 50 additions & 3 deletions

File tree

esp32/.firmware/fsd_handler.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,17 @@ bool fsd_can_transmit(const FSDState *state) {
8585
return true;
8686
}
8787

88+
// AP-First gate (parity with the Flipper). When ap_first is on, hold AP/FSD/nag
89+
// injection until AP is engaged (das_ap_state >= 2) AND has held stable for
90+
// AP_FIRST_STABLE_MS — injecting on the activation edge is what trips the
91+
// 2026.14.x preflight (steer-jerk + AP disengage). now_ms = millis(),
92+
// ap_unstable_tick_ms is stamped whenever das_ap_state < 2.
93+
bool fsd_ap_first_allows(const FSDState *state, uint32_t now_ms) {
94+
if (!state->ap_first) return true; // gate off -> always allow
95+
if (state->das_ap_state < 2u) return false; // AP not engaged yet
96+
return (now_ms - state->ap_unstable_tick_ms) >= AP_FIRST_STABLE_MS;
97+
}
98+
8899
// ── HW version detection from GTW_carConfig (0x398) ──────────────────────────
89100

90101
TeslaHWVersion fsd_detect_hw_version(const CanFrame *frame) {

esp32/.firmware/fsd_handler.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ void fsd_apply_hw_version(FSDState *state, TeslaHWVersion hw);
3434
/** Returns true if current state allows transmitting CAN frames. */
3535
bool fsd_can_transmit(const FSDState *state);
3636

37+
/** AP-First stability window (ms): AP must hold engaged this long before AP/FSD/nag
38+
* injection is allowed when ap_first is on. */
39+
#define AP_FIRST_STABLE_MS 1000u
40+
41+
/** AP-First gate: true if injection is allowed now. When ap_first is off, always
42+
* true. When on, requires das_ap_state >= 2 stable for AP_FIRST_STABLE_MS.
43+
* now_ms = millis(); ap_unstable_tick_ms is stamped whenever das_ap_state < 2. */
44+
bool fsd_ap_first_allows(const FSDState *state, uint32_t now_ms);
45+
3746
/** Read GTW_carConfig (0x398) to detect HW version.
3847
* Returns TeslaHW_Unknown if frame is not 0x398 or version unrecognised. */
3948
TeslaHWVersion fsd_detect_hw_version(const CanFrame *frame);

esp32/.firmware/main.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,9 @@ static void update_led() {
983983
static void process_frame(CanBusId bus, const CanFrame &frame) {
984984
state_enter();
985985
g_state.rx_count++;
986+
// AP-First stability debounce: stamp the last time AP was not engaged, so
987+
// fsd_ap_first_allows() can require AP held stable for AP_FIRST_STABLE_MS (#100/#108).
988+
if (g_state.das_ap_state < 2u) g_state.ap_unstable_tick_ms = millis();
986989
if (frame.id == CAN_ID_GTW_CAR_STATE) g_state.seen_gtw_car_state++;
987990
if (frame.id == CAN_ID_GTW_CAR_CONFIG) g_state.seen_gtw_car_config++;
988991
if (frame.id == CAN_ID_AP_CONTROL) g_state.seen_ap_control++;
@@ -1133,14 +1136,17 @@ static void process_frame(CanBusId bus, const CanFrame &frame) {
11331136
// ── Beyond here only run when TX is allowed ───────────────────────────────
11341137
state_enter();
11351138
bool tx = fsd_can_transmit(&g_state);
1139+
// AP-First (#100/#108): when enabled, hold AP/FSD/nag injection until AP is
1140+
// engaged and stable. Gates 0x3FD / 0x3EE / 0x370 below; off by default.
1141+
bool ap_ok = fsd_ap_first_allows(&g_state, millis());
11361142
state_exit();
11371143

11381144
// NAG killer — build echo only when TX is currently allowed.
11391145
if (frame.id == CAN_ID_EPAS_STATUS) {
11401146
CanFrame echo;
11411147
state_enter();
11421148
fsd_handle_epas_status(&g_state, &frame);
1143-
bool fired = tx ? fsd_handle_nag_killer(&g_state, &frame, &echo) : false;
1149+
bool fired = (tx && ap_ok) ? fsd_handle_nag_killer(&g_state, &frame, &echo) : false;
11441150
state_exit();
11451151
if (fired) {
11461152
uint8_t lvl = (frame.data[4] >> 6) & 0x03;
@@ -1169,7 +1175,7 @@ static void process_frame(CanBusId bus, const CanFrame &frame) {
11691175
state_enter();
11701176
bool modified = fsd_handle_legacy_autopilot(&g_state, &f);
11711177
state_exit();
1172-
if (modified && tx) send_on_bus(bus, f);
1178+
if (modified && tx && ap_ok) send_on_bus(bus, f);
11731179
return;
11741180
}
11751181

@@ -1238,7 +1244,7 @@ static void process_frame(CanBusId bus, const CanFrame &frame) {
12381244
state_enter();
12391245
bool modified = fsd_handle_autopilot_frame(&g_state, &f);
12401246
state_exit();
1241-
if (modified && tx) send_on_bus(bus, f);
1247+
if (modified && tx && ap_ok) send_on_bus(bus, f);
12421248
return;
12431249
}
12441250
}

esp32/.firmware/prefs.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ void prefs_load(FSDState *state) {
1313
}
1414
state->nag_killer = g_prefs.getBool("nag", true);
1515
state->continuous_ap = g_prefs.getBool("contap", false);
16+
state->ap_first = g_prefs.getBool("apfirst",false);
1617
state->suppress_speed_chime = g_prefs.getBool("chime", true);
1718
state->ignore_ota = g_prefs.getBool("ignota", false);
1819
state->fsd_unlock = g_prefs.getBool("unlock", false);
@@ -59,6 +60,7 @@ void prefs_save(const FSDState *state) {
5960
g_prefs.putBool("ok", true);
6061
g_prefs.putBool("nag", state->nag_killer);
6162
g_prefs.putBool("contap", state->continuous_ap);
63+
g_prefs.putBool("apfirst",state->ap_first);
6264
g_prefs.putBool("chime", state->suppress_speed_chime);
6365
g_prefs.putBool("ignota", state->ignore_ota);
6466
g_prefs.putBool("unlock", state->fsd_unlock);

esp32/.firmware/web_dashboard.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,10 @@ input:checked+.sl2:before{transform:translateX(20px);background:#fff}
418418
<span class="lbl">Continuous AP</span>
419419
<label class="sw"><input type="checkbox" id="swContinuousAp" onchange="cmd('continuous_ap',this.checked)"><span class="sl2"></span></label>
420420
</div>
421+
<div class="row">
422+
<span class="lbl">AP-First (14.x)</span>
423+
<label class="sw"><input type="checkbox" id="swApFirst" onchange="cmd('ap_first',this.checked)"><span class="sl2"></span></label>
424+
</div>
421425
<div class="row">
422426
<span class="lbl">BMS Display</span>
423427
<label class="sw"><input type="checkbox" id="swBms" onchange="cmd('bms',this.checked)"><span class="sl2"></span></label>
@@ -661,6 +665,7 @@ function updateControlsSummary(d){
661665
if(d.fsd_unlock)items.push('FSD Unlock');
662666
if(d.nag_killer)items.push('NAG Killer');
663667
if(d.continuous_ap)items.push('Continuous AP');
668+
if(d.ap_first)items.push('AP-First');
664669
if(d.bms_output)items.push('BMS');
665670
if(d.force_fsd)items.push('Force FSD');
666671
if(d.china_mode)items.push('China');
@@ -720,6 +725,7 @@ function upd(d){
720725
if(document.getElementById('swFsdUnlock')) document.getElementById('swFsdUnlock').checked=d.fsd_unlock;
721726
if(document.getElementById('swNag')) document.getElementById('swNag').checked=d.nag_killer;
722727
if(document.getElementById('swContinuousAp')) document.getElementById('swContinuousAp').checked=d.continuous_ap;
728+
if(document.getElementById('swApFirst')) document.getElementById('swApFirst').checked=d.ap_first;
723729
if(document.getElementById('swBms')) document.getElementById('swBms').checked=d.bms_output;
724730
if(document.getElementById('swFsd')) document.getElementById('swFsd').checked=d.force_fsd;
725731
if(document.getElementById('swChina')) document.getElementById('swChina').checked=d.china_mode;
@@ -1235,6 +1241,7 @@ static String build_json() {
12351241
j += "\"fsd_unlock\":"; j += state.fsd_unlock ? "true" : "false"; j += ',';
12361242
j += "\"nag_killer\":"; j += state.nag_killer ? "true" : "false"; j += ',';
12371243
j += "\"continuous_ap\":"; j += state.continuous_ap ? "true" : "false"; j += ',';
1244+
j += "\"ap_first\":"; j += state.ap_first ? "true" : "false"; j += ',';
12381245
j += "\"bms_output\":"; j += state.bms_output ? "true" : "false"; j += ',';
12391246
j += "\"force_fsd\":"; j += state.force_fsd ? "true" : "false"; j += ',';
12401247
j += "\"china_mode\":"; j += state.china_mode ? "true" : "false"; j += ',';
@@ -1365,6 +1372,18 @@ static void ws_event(uint8_t num, WStype_t type,
13651372
Serial.printf("[Web] Continuous AP: %s\n", enabled ? "ON" : "OFF");
13661373
prefs_save(&saved);
13671374
}
1375+
} else if (strstr(buf, "\"ap_first\"")) {
1376+
if (vptr) {
1377+
while (*vptr == ' ' || *vptr == ':') vptr++;
1378+
bool enabled = (strncmp(vptr, "true", 4) == 0);
1379+
FSDState saved;
1380+
state_enter();
1381+
g_state->ap_first = enabled;
1382+
saved = *g_state;
1383+
state_exit();
1384+
Serial.printf("[Web] AP-First: %s\n", enabled ? "ON" : "OFF");
1385+
prefs_save(&saved);
1386+
}
13681387
}
13691388
#if defined(BOARD_TTGO_DISPLAY)
13701389
else if (strstr(buf, "\"disp\"")) {

0 commit comments

Comments
 (0)