diff --git a/panda/tests/wake_can/README.md b/panda/tests/wake_can/README.md index 23b550b492..62a83f6813 100644 --- a/panda/tests/wake_can/README.md +++ b/panda/tests/wake_can/README.md @@ -55,8 +55,8 @@ lacks the extra non-OFF wake; optional `--regression-ref REV` can test a histori version that masked the DRIVE edge if available locally. Host tests cover Tesla detection, stale identity, firmware-selection combinations, -missing files and normal update/signature behaviour. Galaxy/native settings tests -cover Tesla-only visibility and capability checks, parked-only writes, required +missing files and normal update/signature behaviour. Galaxy settings tests cover +Tesla-only visibility and capability checks, parked-only writes, required confirmation, cancellation and firmware preflight. `host_policy.py` exercises actual shutdown methods with synthetic clocks and Params, without device access. diff --git a/panda/tests/wake_can/host_policy.py b/panda/tests/wake_can/host_policy.py index 4d996cdfe6..57bb0fc30b 100644 --- a/panda/tests/wake_can/host_policy.py +++ b/panda/tests/wake_can/host_policy.py @@ -34,7 +34,6 @@ def main(): pm = SimpleNamespace(low_voltage_start_time=None,car_voltage_mV=14000,car_battery_capacity_uWh=30e6, params=SimpleNamespace(get_bool=lambda name:values[name])) toggles = SimpleNamespace(device_shutdown_time=3600,low_voltage_shutdown=11.8) - # Wake is deliberately independent, but not a supported input to this policy. pm.wake_on_can = True reason = lambda: ns['shutdown_reason'](pm,False,True,1.,False,toggles) observed = {} @@ -44,12 +43,9 @@ def main(): if result and 'first_shutdown' not in observed: observed['first_shutdown']={'monotonic_s':t,'offroad_s':t-1,'reason':result,'fresh_awake':True} assert observed['first_shutdown']=={'monotonic_s':3602,'offroad_s':3601,'reason':'offroad_timeout','fresh_awake':True} - # The one-hour constant is NOT an unconditional one-hour timeout. toggles.device_shutdown_time=0 assert reason() is None observed['no_timeout_healthy_after_hour']=reason() - # Existing emergency/protective inputs stay intact; never bypass by changing ignition - # or setting DisablePowerDown to implement stay-awake. pm.car_battery_capacity_uWh=0 assert reason()=='battery_capacity_exhausted' observed['exhausted']=reason() @@ -64,7 +60,7 @@ def main(): toggles.device_shutdown_time=3600 for awake in [True,False]: pm.wake_on_can=awake - assert reason()=='offroad_timeout' # same answer; no telemetry seam exists yet + assert reason()=='offroad_timeout' observed['contract']='Wake-only CAN deliberately does not inhibit stock host shutdown.' observed['source_sha256']=hashlib.sha256(path.read_bytes()).hexdigest() observed['thermal']='Not modeled here; unchanged host thermal protection must be independently preserved in any extension.' diff --git a/panda/tests/wake_can/run.py b/panda/tests/wake_can/run.py index cdd0f5b674..046f9a3fb5 100644 --- a/panda/tests/wake_can/run.py +++ b/panda/tests/wake_can/run.py @@ -27,11 +27,9 @@ def between(text, start, end): def generate(read): can, main, boot, cuatro = [read(p) for p in FILES] independent_wake = 'bool recent_heartbeat, bool wake)' in boot - # Historical two-argument definitions must not include today's declaration. if not independent_wake: boot = boot.replace('#include "bootkick_declarations.h"', '') globals_ = between(can, 'bool ignition_can = false;', '\nbool can_silent') - # Added wake globals immediately precede ignition globals in candidate. if 'bool wake_on_can = false;' in can: globals_ = between(can, 'bool wake_on_can = false;', '\nbool can_silent') else: diff --git a/panda/tests/wake_can/scenarios.h b/panda/tests/wake_can/scenarios.h index 96faea1efc..5c645cbd8b 100644 --- a/panda/tests/wake_can/scenarios.h +++ b/panda/tests/wake_can/scenarios.h @@ -1,10 +1,8 @@ -// Included after verbatim production functions. Each scenario gets a fresh process. static void tick(void) { timer.SR = 1U; tick_handler(); } static void second(void) { for (int i=0; i<8; i++) tick(); } static CANPacket_t packet(unsigned bus, unsigned addr, unsigned dlc, unsigned state, unsigned counter) { CANPacket_t p = {0}; p.bus=bus; p.addr=addr; p.data_len_code=dlc; p.data[0]=state<<5; p.data[6]=counter<<4; - // Tesla DBC checksum: address bytes plus seven payload bytes, modulo 256. unsigned sum=(addr & 255U)+(addr >> 8U); for (unsigned i=0; i<7; i++) sum+=p.data[i]; p.data[7]=sum & 255U; @@ -16,13 +14,13 @@ static void tesla(unsigned state, unsigned counter) { static void pair(unsigned state) { tesla(state,14); tesla(state,15); } static void wake_case(void) { assert(!wake_on_can && !ignition_can); - tesla(2,14); assert(!wake_on_can && !ignition_can); // prime only + tesla(2,14); assert(!wake_on_can && !ignition_can); for (unsigned state=0; state<4; state++) { tesla(state,(15+state)%16); assert(wake_on_can == (state != 0)); assert(ignition_can == (state == 3)); assert(wake_on_can_cnt == 0 && ignition_can_cnt == 0); } - tesla(0,3); assert(!wake_on_can && !ignition_can); // accepted OFF clears immediately + tesla(0,3); assert(!wake_on_can && !ignition_can); } static void invalid_case(void) { pair(2); assert(wake_on_can); wake_on_can_cnt=2; @@ -37,32 +35,31 @@ static void invalid_case(void) { } CANPacket_t p=packet(0,0x220,8,0,0); ignition_can_hook(&p); assert(wake_on_can && wake_on_can_cnt==2); - tesla(0,15); assert(wake_on_can && wake_on_can_cnt==2); // duplicate - tesla(0,7); assert(wake_on_can && wake_on_can_cnt==2); // jump - tesla(0,8); assert(!wake_on_can && wake_on_can_cnt==0); // reacquire after jump + tesla(0,15); assert(wake_on_can && wake_on_can_cnt==2); + tesla(0,7); assert(wake_on_can && wake_on_can_cnt==2); + tesla(0,8); assert(!wake_on_can && wake_on_can_cnt==0); tesla(2,9); assert(wake_on_can); wake_on_can_cnt=2; p=packet(0,0x221,8,2,10); p.data[7]^=1; ignition_can_hook(&p); - assert(wake_on_can && wake_on_can_cnt==2); // invalid frames cannot refresh wake - tesla(0,11); assert(wake_on_can && wake_on_can_cnt==2); // valid frame primes again + assert(wake_on_can && wake_on_can_cnt==2); + tesla(0,11); assert(wake_on_can && wake_on_can_cnt==2); tesla(0,12); assert(!wake_on_can && wake_on_can_cnt==0); } static void disabled_case(void) { for (unsigned state=0; state<4; state++) { pair(state); - assert(!wake_on_can); // default and other manufacturers never gain Tesla wake - assert(ignition_can == (state==3)); // stock DRIVE remains independent of opt-in + assert(!wake_on_can); + assert(ignition_can == (state==3)); } } static void checksum_case(void) { CANPacket_t p=packet(0,0x221,8,2,14); - assert(p.data[7]==0x43); // address 0x23 + ACCESSORY 0x40 + counter 0xE0 + assert(p.data[7]==0x43); p.data[7]^=1; ignition_can_hook(&p); - tesla(2,15); assert(!wake_on_can); // bad checksum cannot prime a wake - tesla(2,0); assert(wake_on_can); // two valid consecutive frames, including wrap + tesla(2,15); assert(!wake_on_can); + tesla(2,0); assert(wake_on_can); tesla(0,1); assert(!wake_on_can); - // Corruption in any payload/checksum byte cannot create a wake or prime it. for (unsigned i=0; i<8; i++) { p=packet(0,0x221,8,2,2); p.data[i]^=1; ignition_can_hook(&p); assert(!wake_on_can); @@ -78,8 +75,7 @@ static void checksum_case(void) { p=packet(0,0x221,8,2,8+i); p.data[7]^=1; ignition_can_hook(&p); second(); } - assert(!wake_on_can); // checksum-invalid traffic ages out - // Do not change the stock DRIVE decoder while hardening the extra wake path. + assert(!wake_on_can); p=packet(0,0x221,8,3,12); p.data[7]^=1; ignition_can_hook(&p); assert(ignition_can && !wake_on_can); } @@ -88,9 +84,8 @@ static void stale_case(void) { pair(2); for (unsigned i=1; i<=3; i++) { second(); assert(wake_on_can && wake_on_can_cnt==i); } second(); assert(!wake_on_can && wake_on_can_cnt==4); - // Staleness does not reset the decoder's static previous counter (reference parity). tesla(2,15); assert(!wake_on_can && wake_on_can_cnt==4); - tesla(2,0); assert(wake_on_can && wake_on_can_cnt==0); // wrap after stale + tesla(2,0); assert(wake_on_can && wake_on_can_cnt==0); second(); assert(wake_on_can_cnt==1); tesla(0,1); assert(!wake_on_can && wake_on_can_cnt==0); wake_on_can_cnt=UINT32_MAX; second(); assert(!wake_on_can && wake_on_can_cnt==0); @@ -113,19 +108,19 @@ static void drive_watchdog_case(void) { tesla(3,4); second(); assert(heartbeat_counter==5 && heartbeat_lost); } static void boot_case(void) { - legacy_bootkick(false,false); assert(observed_boot==BOOT_BOOTKICK); // first power-on + legacy_bootkick(false,false); assert(observed_boot==BOOT_BOOTKICK); for (int i=0; i<30; i++) legacy_bootkick(false,false); - assert(!bootkick_reset_triggered); // no first-boot reset + assert(!bootkick_reset_triggered); legacy_bootkick(false,true); assert(observed_boot==BOOT_STANDBY); legacy_bootkick(true,false); assert(observed_boot==BOOT_BOOTKICK); for (int i=0; i<18; i++) { legacy_bootkick(true,false); assert(observed_boot==BOOT_BOOTKICK); } legacy_bootkick(true,false); assert(observed_boot==BOOT_RESET && bootkick_reset_triggered); - assert(gpio_level); // Cuatro RESET is deasserted bootkick, NOT a separate reset pin + assert(gpio_level); for (int i=0; i<4; i++) { legacy_bootkick(true,false); assert(observed_boot==BOOT_RESET); } legacy_bootkick(true,false); assert(observed_boot==BOOT_BOOTKICK && !gpio_level); legacy_bootkick(false,true); legacy_bootkick(true,false); for (int i=0; i<30; i++) legacy_bootkick(true,false); - assert(observed_boot==BOOT_BOOTKICK); // only one reset per MCU boot + assert(observed_boot==BOOT_BOOTKICK); } static void cancel_reset_case(int serial) { legacy_bootkick(false,true); legacy_bootkick(true,false); @@ -135,7 +130,6 @@ static void cancel_reset_case(int serial) { assert(!bootkick_reset_triggered && observed_boot==BOOT_BOOTKICK); } static void existing_case(void) { - // Harness insertion still wakes with ignition false. legacy_bootkick(false,true); harness.status=1; legacy_bootkick(false,false); assert(observed_boot==BOOT_BOOTKICK); legacy_bootkick(false,true); assert(observed_boot==BOOT_STANDBY); @@ -156,17 +150,15 @@ static void other_cars_case(void) { gm_remote_start_boots_comma=true; p=packet(0,0xC9,8,0,0); p.data[6]=0x10; ignition_can_hook(&p); assert(ignition_can); p.data[6]=0; ignition_can_hook(&p); assert(!ignition_can); - // Pre-AP checksum and counter remain active. p=packet(0,0x101,3,0,0); p.data[0]=8; p.data[1]=14; p.data[2]=24; ignition_can_hook(&p); assert(!ignition_can); p.data[1]=15; p.data[2]=25; ignition_can_hook(&p); assert(ignition_can); p.data[0]=0; p.data[1]=0; p.data[2]=99; ignition_can_hook(&p); assert(ignition_can); p.data[1]=1; p.data[2]=3; ignition_can_hook(&p); assert(!ignition_can); - // Rivian sequential modulo-15, then Mazda. p=packet(0,0x152,8,0,0); p.data[1]=14; p.data[7]=0x10; ignition_can_hook(&p); assert(!ignition_can); p.data[1]=0; ignition_can_hook(&p); assert(ignition_can); p=packet(0,0x9E,8,0,0); ignition_can_hook(&p); assert(!ignition_can); p.data[0]=0xC0; ignition_can_hook(&p); assert(ignition_can); - assert(!wake_on_can); // none of these set Tesla-only wake flag + assert(!wake_on_can); #ifdef PANDA_HKG_REMOTE_START ignition_can=false; legacy_bootkick(false,true); p=packet(1,0x384,8,0,0); p.data[3]=1; ignition_can_hook(&p); @@ -177,26 +169,23 @@ static void other_cars_case(void) { #endif } static void drive_edge_case(void) { - // Prolonged awake, SOM shuts down while fresh ACCESSORY CAN continues. pair(2); second(); heartbeat_counter=0; second(); assert(observed_boot==BOOT_STANDBY); for (unsigned i=0; i<3700; i++) { tesla(2,i%16); second(); } assert(wake_on_can && !ignition_can && observed_boot==BOOT_STANDBY); tesla(3,4); second(); assert(ignition_can && wake_on_can); - assert(observed_boot==BOOT_BOOTKICK); // Stock DRIVE edge must survive held wake. + assert(observed_boot==BOOT_BOOTKICK); puts("PASS: fresh ACCESSORY through SOM shutdown preserves later DRIVE bootkick"); tesla(0,5); second(); tesla(2,6); second(); - assert(observed_boot==BOOT_BOOTKICK); // true low->high recovers + assert(observed_boot==BOOT_BOOTKICK); heartbeat_counter=0; second(); assert(observed_boot==BOOT_STANDBY); - // Car sleep / absent or rejected traffic must age wake out and permit re-wake. for (unsigned i=0; i<5; i++) { - tesla(2,6); // repeated counter cannot keep the wake fresh + tesla(2,6); second(); } assert(!wake_on_can && !ignition_can && observed_boot==BOOT_STANDBY); tesla(2,7); second(); assert(wake_on_can && observed_boot==BOOT_BOOTKICK); } static void stock_drive_case(void) { - // This exact physical-input sequence passes both stock and candidate. pair(2); second(); heartbeat_counter=0; second(); for (unsigned i=0; i<3700; i++) { tesla(2,i%16); second(); } assert(!ignition_can && observed_boot==BOOT_STANDBY); @@ -205,25 +194,24 @@ static void stock_drive_case(void) { } static void independent_edges_case(void) { test_bootkick(false,true,false); assert(observed_boot==BOOT_STANDBY); - test_bootkick(false,true,true); assert(observed_boot==BOOT_BOOTKICK); // edge beats heartbeat + test_bootkick(false,true,true); assert(observed_boot==BOOT_BOOTKICK); test_bootkick(false,true,true); assert(observed_boot==BOOT_STANDBY); - test_bootkick(false,false,true); assert(observed_boot==BOOT_STANDBY); // held wake is not level-triggered - test_bootkick(true,true,true); assert(observed_boot==BOOT_BOOTKICK); // independent ignition edge + test_bootkick(false,false,true); assert(observed_boot==BOOT_STANDBY); + test_bootkick(true,true,true); assert(observed_boot==BOOT_BOOTKICK); test_bootkick(true,true,false); assert(observed_boot==BOOT_STANDBY); - test_bootkick(true,false,true); assert(observed_boot==BOOT_STANDBY); // no new wake kick while started - test_bootkick(false,false,true); assert(observed_boot==BOOT_STANDBY); // falling ignition is not wake edge + test_bootkick(true,false,true); assert(observed_boot==BOOT_STANDBY); + test_bootkick(false,false,true); assert(observed_boot==BOOT_STANDBY); test_bootkick(false,false,false); assert(observed_boot==BOOT_STANDBY); test_bootkick(false,false,true); assert(observed_boot==BOOT_BOOTKICK); test_bootkick(false,true,true); assert(observed_boot==BOOT_STANDBY); harness.status=1; - test_bootkick(false,true,true); assert(observed_boot==BOOT_BOOTKICK); // harness still beats heartbeat + test_bootkick(false,true,true); assert(observed_boot==BOOT_BOOTKICK); } static void wake_reset_case(void) { test_bootkick(false,true,false); test_bootkick(false,false,true); for (int i=0; i<18; i++) { test_bootkick(false,false,true); assert(observed_boot==BOOT_BOOTKICK); } test_bootkick(false,false,true); assert(observed_boot==BOOT_RESET && bootkick_reset_triggered); - // Ignition, wake, harness and heartbeat cannot interrupt the active reset. for (int i=0; i<4; i++) { harness.status=(i%2)+1; test_bootkick(i%2,true,i%2); assert(observed_boot==BOOT_RESET); @@ -231,7 +219,7 @@ static void wake_reset_case(void) { test_bootkick(true,false,true); assert(observed_boot==BOOT_BOOTKICK); test_bootkick(false,true,false); test_bootkick(false,false,true); for (int i=0; i<30; i++) test_bootkick(false,false,true); - assert(observed_boot==BOOT_BOOTKICK); // no second reset + assert(observed_boot==BOOT_BOOTKICK); } static void boot_trace_case(void) { uint32_t rng=0x87654321;