mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-12 03:03:51 +08:00
Tesla: add Galaxy-controlled CAN wake
Bring in PR #134 for opt-in Tesla wake-on-CAN support. Keep the setting in Galaxy, omit native UI changes, and reject incompatible remote-start firmware selections. Co-authored-by: AngusBell97 <124716116+AngusBell97@users.noreply.github.com>
This commit is contained in:
committed by
firestar5683
parent
01dd14bfae
commit
24b8789c79
@@ -27,6 +27,10 @@ add_panda_targets() {
|
||||
panda_h7_remote_can_ignition_only
|
||||
panda_hkg_remote_can_ignition_only
|
||||
panda_h7_hkg_remote_can_ignition_only
|
||||
panda_tesla_wake
|
||||
panda_h7_tesla_wake
|
||||
panda_tesla_wake_can_ignition_only
|
||||
panda_h7_tesla_wake_can_ignition_only
|
||||
panda_jungle_h7
|
||||
body_h7
|
||||
)
|
||||
|
||||
Binary file not shown.
@@ -362,6 +362,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"IgnoreIgnitionLine", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"LongPitch", {PERSISTENT, BOOL, "1", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"RemoteStartBootsComma", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"TeslaWakeOnCAN", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"RemapCancelToDistance", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"NAPAdaptiveAccel", {PERSISTENT, BOOL, "1", "1", 0, SETTINGS_SIMPLE}},
|
||||
{"NAPFollowDistance", {PERSISTENT, INT, "4", "4"}},
|
||||
|
||||
Binary file not shown.
@@ -181,6 +181,11 @@ build_project("panda_h7_remote_can_ignition_only", base_project_h7, "./board/mai
|
||||
build_project("panda_hkg_remote_can_ignition_only", base_project_f4, "./board/main.c", ["-DPANDA_HKG_REMOTE_START", "-DPANDA_IGNORE_IGNITION_LINE"])
|
||||
build_project("panda_h7_hkg_remote_can_ignition_only", base_project_h7, "./board/main.c", ["-DPANDA_HKG_REMOTE_START", "-DPANDA_IGNORE_IGNITION_LINE"])
|
||||
|
||||
build_project("panda_tesla_wake", base_project_f4, "./board/main.c", ["-DPANDA_TESLA_WAKE_ON_CAN"])
|
||||
build_project("panda_h7_tesla_wake", base_project_h7, "./board/main.c", ["-DPANDA_TESLA_WAKE_ON_CAN"])
|
||||
build_project("panda_tesla_wake_can_ignition_only", base_project_f4, "./board/main.c", ["-DPANDA_TESLA_WAKE_ON_CAN", "-DPANDA_IGNORE_IGNITION_LINE"])
|
||||
build_project("panda_h7_tesla_wake_can_ignition_only", base_project_h7, "./board/main.c", ["-DPANDA_TESLA_WAKE_ON_CAN", "-DPANDA_IGNORE_IGNITION_LINE"])
|
||||
|
||||
# panda jungle fw
|
||||
flags = [
|
||||
"-DPANDA_JUNGLE",
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
bool bootkick_reset_triggered = false;
|
||||
|
||||
void bootkick_tick(bool ignition, bool recent_heartbeat) {
|
||||
void bootkick_tick(bool ignition, bool recent_heartbeat, bool wake) {
|
||||
static uint16_t bootkick_last_serial_ptr = 0;
|
||||
static uint8_t waiting_to_boot_countdown = 0;
|
||||
static uint8_t boot_reset_countdown = 0;
|
||||
static uint8_t bootkick_harness_status_prev = HARNESS_STATUS_NC;
|
||||
static bool bootkick_ign_prev = false;
|
||||
static bool bootkick_wake_prev = false;
|
||||
static BootState boot_state = BOOT_BOOTKICK;
|
||||
BootState boot_state_prev = boot_state;
|
||||
const bool harness_inserted = (harness.status != bootkick_harness_status_prev) && (harness.status != HARNESS_STATUS_NC);
|
||||
|
||||
if ((ignition && !bootkick_ign_prev) || harness_inserted) {
|
||||
// bootkick on rising edge of ignition or harness insertion
|
||||
if ((ignition && !bootkick_ign_prev) || harness_inserted || (wake && !bootkick_wake_prev && !ignition)) {
|
||||
boot_state = BOOT_BOOTKICK;
|
||||
} else if (recent_heartbeat) {
|
||||
// disable bootkick once openpilot is up
|
||||
@@ -56,6 +56,7 @@ void bootkick_tick(bool ignition, bool recent_heartbeat) {
|
||||
|
||||
// update state
|
||||
bootkick_ign_prev = ignition;
|
||||
bootkick_wake_prev = wake;
|
||||
bootkick_harness_status_prev = harness.status;
|
||||
bootkick_last_serial_ptr = uart_ring_som_debug.w_ptr_tx;
|
||||
if (waiting_to_boot_countdown > 0U) {
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
extern bool bootkick_reset_triggered;
|
||||
|
||||
void bootkick_tick(bool ignition, bool recent_heartbeat);
|
||||
void bootkick_tick(bool ignition, bool recent_heartbeat, bool wake);
|
||||
|
||||
@@ -7,7 +7,9 @@ uint32_t rx_buffer_overflow = 0;
|
||||
|
||||
can_health_t can_health[PANDA_CAN_CNT] = {{0}, {0}, {0}};
|
||||
|
||||
// Ignition detected from CAN meessages
|
||||
bool wake_on_can = false;
|
||||
uint32_t wake_on_can_cnt = 0U;
|
||||
|
||||
bool ignition_can = false;
|
||||
uint32_t ignition_can_cnt = 0U;
|
||||
#ifdef PANDA_HKG_REMOTE_START
|
||||
@@ -225,6 +227,23 @@ void ignition_can_hook(CANPacket_t *msg) {
|
||||
ignition_can_cnt = 0U;
|
||||
}
|
||||
prev_counter_tesla = counter;
|
||||
|
||||
#ifdef PANDA_TESLA_WAKE_ON_CAN
|
||||
uint32_t checksum = (msg->addr & 0xFFU) + (msg->addr >> 8U);
|
||||
for (uint8_t i = 0U; i < 7U; i++) {
|
||||
checksum += msg->data[i];
|
||||
}
|
||||
static int prev_counter_tesla_wake = -1;
|
||||
if (!msg->extended && (msg->data[7] == (checksum & 0xFFU))) {
|
||||
if ((prev_counter_tesla_wake != -1) && (counter == ((prev_counter_tesla_wake + 1) % 16))) {
|
||||
wake_on_can = ((msg->data[0] >> 5U) & 0x3U) != 0U;
|
||||
wake_on_can_cnt = 0U;
|
||||
}
|
||||
prev_counter_tesla_wake = counter;
|
||||
} else {
|
||||
prev_counter_tesla_wake = -1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Tesla Model S pre-AP exception
|
||||
|
||||
@@ -28,7 +28,9 @@ extern uint32_t rx_buffer_overflow;
|
||||
|
||||
extern can_health_t can_health[PANDA_CAN_CNT];
|
||||
|
||||
// Ignition detected from CAN meessages
|
||||
extern bool wake_on_can;
|
||||
extern uint32_t wake_on_can_cnt;
|
||||
|
||||
extern bool ignition_can;
|
||||
extern uint32_t ignition_can_cnt;
|
||||
|
||||
|
||||
+5
-1
@@ -192,7 +192,7 @@ static void tick_handler(void) {
|
||||
#ifdef PANDA_HKG_REMOTE_START
|
||||
started = started || hkg_remote_climate_wake;
|
||||
#endif
|
||||
bootkick_tick(started, recent_heartbeat);
|
||||
bootkick_tick(started, recent_heartbeat, wake_on_can);
|
||||
|
||||
// increase heartbeat counter and cap it at the uint32 limit
|
||||
if (heartbeat_counter < UINT32_MAX) {
|
||||
@@ -270,6 +270,9 @@ static void tick_handler(void) {
|
||||
if (ignition_can_cnt > 2U) {
|
||||
ignition_can = false;
|
||||
}
|
||||
if (wake_on_can_cnt > 2U) {
|
||||
wake_on_can = false;
|
||||
}
|
||||
#ifdef PANDA_HKG_REMOTE_START
|
||||
if (hkg_remote_climate_wake_cnt > 2U) {
|
||||
hkg_remote_climate_wake = false;
|
||||
@@ -280,6 +283,7 @@ static void tick_handler(void) {
|
||||
uptime_cnt += 1U;
|
||||
safety_mode_cnt += 1U;
|
||||
ignition_can_cnt += 1U;
|
||||
wake_on_can_cnt += 1U;
|
||||
#ifdef PANDA_HKG_REMOTE_START
|
||||
hkg_remote_climate_wake_cnt += 1U;
|
||||
#endif
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
extern const uint8_t gitversion[19];
|
||||
const uint8_t gitversion[19] = "DEV-244aa673-DEBUG";
|
||||
const uint8_t gitversion[19] = "DEV-9c92f717-DEBUG";
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
DEV-244aa673-DEBUG
|
||||
DEV-9c92f717-DEBUG
|
||||
@@ -0,0 +1,83 @@
|
||||
# Optional Tesla wake-on-CAN
|
||||
|
||||
The Vehicle settings page offers **Wake Comma with Tesla** for detected Tesla Model 3,
|
||||
Model Y and Model X platforms that use `VCFRONT_LVPowerState` on the party bus.
|
||||
It is hidden for other makes, unknown vehicles and pre-AP Tesla Model S.
|
||||
`TeslaWakeOnCAN` is a persistent boolean, default off. Changing it while parked
|
||||
uses the existing confirmed Panda firmware update/reboot flow.
|
||||
|
||||
## Wake and ignition contract
|
||||
|
||||
- Only firmware built with `PANDA_TESLA_WAKE_ON_CAN` enables the extra wake path.
|
||||
Startup and manual update select it only with the toggle enabled and a matching
|
||||
supported Tesla identity. Conflicting selected vehicle or corrupt CarParams
|
||||
disables selection. Other firmware variants never set the extra wake flag.
|
||||
- Accepted wake packets are standard CAN frames on bus 0, ID `0x221`, length 8.
|
||||
Byte 7 must equal the sum of address bytes and payload bytes 0–6 modulo 256.
|
||||
Two checksum-valid frames with consecutive modulo-16 upper-byte-6 counters are
|
||||
required. Invalid checksum or extended frames break that wake counter sequence.
|
||||
- `(data[0] >> 5U) & 0x3U`: OFF=0, CONDITIONING=1, ACCESSORY=2, DRIVE=3.
|
||||
Valid non-OFF states set the independent wake flag. Accepted OFF clears it.
|
||||
Invalid traffic cannot refresh it; the existing tick expiry clears stale wake.
|
||||
- The original DRIVE ignition decoder is unchanged, including its own counter
|
||||
tracking. Extra wake never asserts ignition or enters watchdog/control inputs.
|
||||
- Bootkick tracks ignition and wake edges separately. A fresh wake edge can boot
|
||||
while ignition is false. A held ACCESSORY state cannot mask a later DRIVE edge.
|
||||
Harness, heartbeat priority, reset countdown/cancellation and one-reset-per-MCU-
|
||||
boot behaviour remain intact.
|
||||
- Stock host timeout, battery, voltage, thermal and forced shutdown remain in
|
||||
force. This feature does not keep the host or car awake. Held wake does not
|
||||
repeatedly boot the host; a new wake edge can wake it after shutdown.
|
||||
|
||||
The checksum layout is defined in
|
||||
`opendbc_repo/opendbc/dbc/tesla_model3_party.dbc`; the checksum algorithm is in
|
||||
`opendbc_repo/opendbc/car/tesla/teslacan.py`. Checksum validation is integrity
|
||||
checking, not authentication. Physical early-message availability and vehicle
|
||||
wake behaviour still require hardware validation.
|
||||
|
||||
## Repeatable local checks
|
||||
|
||||
```
|
||||
ulimit -c 0
|
||||
python3 panda/tests/wake_can/run.py --evidence /absolute/evidence/green
|
||||
python3 panda/tests/wake_can/host_policy.py --evidence /absolute/evidence/host
|
||||
```
|
||||
|
||||
The runner compiles verbatim production decoder, full 8Hz tick, bootkick,
|
||||
ignition-line and safety-mode predicates, and Cuatro GPIO callback, with fixture
|
||||
peripherals. Each scenario runs in a fresh process. It covers 48 configurations:
|
||||
Tesla, HKG, GM, IgnoreIgnitionLine, and three DEBUG/ALLOW_DEBUG combinations.
|
||||
There are 600 scenario executions, 960,000 ignition/watchdog parity frames and
|
||||
960,000 no-wake boot/reset/harness/GPIO parity frames against Dom
|
||||
`bb04e935272ccbc7551dd5f46d18197757a35587`. Compilation uses warnings-as-errors and
|
||||
UBSan. `--base REV` selects another stock reference. `--red` verifies that stock
|
||||
lacks the extra non-OFF wake; optional `--regression-ref REV` can test a historical
|
||||
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
|
||||
confirmation, cancellation and firmware preflight. `host_policy.py` exercises
|
||||
actual shutdown methods with synthetic clocks and Params, without device access.
|
||||
|
||||
## Firmware
|
||||
|
||||
SCons supplies F4/H7 Tesla variants, each with and without IgnoreIgnitionLine:
|
||||
|
||||
```
|
||||
cd panda
|
||||
scons --minimal -j4 board/obj/panda_tesla_wake.bin.signed \
|
||||
board/obj/panda_h7_tesla_wake.bin.signed \
|
||||
board/obj/panda_tesla_wake_can_ignition_only.bin.signed \
|
||||
board/obj/panda_h7_tesla_wake_can_ignition_only.bin.signed
|
||||
```
|
||||
|
||||
These use the existing signing configuration. No bootstub, trust key or safety
|
||||
limit changes are required. Comma 3/3X and comma 4 use H7; comma 4 retains its
|
||||
Cuatro GPIO callback. A local signed image is not installed firmware or physical
|
||||
wake/sleep validation. The target Params binding/library must be rebuilt with
|
||||
the new key before installation of the settings UI.
|
||||
|
||||
Initial wake mechanism attribution: dzid26's
|
||||
[commaai/panda PR2393](https://github.com/commaai/panda/pull/2393/files) and
|
||||
[AmyJeanes/sunnypilot](https://github.com/AmyJeanes/sunnypilot/commit/57487d8c48be2907760a1553113f7ae7288025dc).
|
||||
@@ -0,0 +1,70 @@
|
||||
// Host-only fixtures. No CAN transmission, target access, or firmware image.
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "opendbc/safety/can.h"
|
||||
typedef struct {} harness_configuration;
|
||||
typedef struct {} GPIO_TypeDef;
|
||||
#include "board/boards/board_declarations.h"
|
||||
#define HARNESS_STATUS_NC 0U
|
||||
#define HARNESS_STATUS_FLIPPED 2U
|
||||
#define LED_GREEN 1U
|
||||
#define LED_BLUE 2U
|
||||
#define POWER_SAVE_STATUS_ENABLED 1U
|
||||
#define FAULT_RELAY_MALFUNCTION 1U
|
||||
#define SAFETY_SILENT 0U
|
||||
#define SAFETY_NOOUTPUT 19U
|
||||
#define SAFETY_ALLOUTPUT 17U
|
||||
#define SAFETY_ELM327 3U
|
||||
struct { uint32_t SR; } timer;
|
||||
#define TICK_TIMER (&timer)
|
||||
struct { uint8_t status; } harness;
|
||||
struct { uint16_t w_ptr_tx; } uart_ring_som_debug;
|
||||
struct { uint32_t r_ptr, w_ptr; } can_rx_q, can_tx1_q, can_tx2_q, can_tx3_q;
|
||||
bool line, som_gpio, gm_remote_start_boots_comma;
|
||||
bool siren_enabled, relay_malfunction, controls_allowed, heartbeat_engaged;
|
||||
bool heartbeat_disabled, heartbeat_lost;
|
||||
uint32_t heartbeat_counter, heartbeat_engaged_mismatches, uptime_cnt, safety_mode_cnt;
|
||||
uint16_t current_safety_mode = 42U, current_safety_param;
|
||||
uint8_t power_save_status;
|
||||
int current_safety_config;
|
||||
unsigned watchdog_kicks, safety_ticks, silent_calls, ir_calls, fan_power, register_checks;
|
||||
BootState observed_boot;
|
||||
unsigned gpio_calls;
|
||||
bool gpio_level;
|
||||
#define GPIOA ((GPIO_TypeDef *)1)
|
||||
void set_gpio_output(GPIO_TypeDef *port, unsigned pin, bool value) {
|
||||
assert(port == GPIOA && pin == 0U); gpio_calls++; gpio_level = value;
|
||||
}
|
||||
static void cuatro_set_bootkick(BootState state);
|
||||
void boot_output(BootState state) { observed_boot = state; cuatro_set_bootkick(state); }
|
||||
bool read_som(void) { return som_gpio; }
|
||||
void bool_sink(bool x) { (void)x; }
|
||||
void ir_output(uint8_t x) { assert(x == 0U); ir_calls++; }
|
||||
struct board fixture_board = {.set_bootkick=boot_output, .read_som_gpio=read_som,
|
||||
.set_siren=bool_sink, .set_ir_power=ir_output};
|
||||
struct board *current_board = &fixture_board;
|
||||
bool harness_check_ignition(void) { return line; }
|
||||
void fan_tick(void) {}
|
||||
void harness_tick(void) {}
|
||||
void sound_tick(void) {}
|
||||
void simple_watchdog_kick(void) { watchdog_kicks++; }
|
||||
void fault_occurred(unsigned x) { (void)x; }
|
||||
void fault_recovered(unsigned x) { (void)x; }
|
||||
void can_set_orientation(bool x) { (void)x; }
|
||||
void can_init_all(void) {}
|
||||
void set_safety_mode(uint16_t mode, uint16_t param) {
|
||||
current_safety_mode = mode; current_safety_param = param;
|
||||
if (mode == SAFETY_SILENT) silent_calls++;
|
||||
}
|
||||
void set_power_save_state(uint8_t x) { power_save_status = x; }
|
||||
void led_set(unsigned x, bool y) { (void)x; (void)y; }
|
||||
void print(const char *x) { (void)x; }
|
||||
void puth(unsigned x) { (void)x; }
|
||||
void puth4(unsigned x) { (void)x; }
|
||||
void fan_set_power(unsigned x) { fan_power = x; }
|
||||
void check_registers(void) { register_checks++; }
|
||||
void safety_tick(int *x) { (void)x; safety_ticks++; }
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute actual host shutdown methods with an explicit clock and in-memory Params.
|
||||
Diagnostic only: no imports of the live application, real Params, or hardware.
|
||||
Stock timeout, voltage, battery and forced shutdown remain authoritative.
|
||||
"""
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--evidence', type=Path, required=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
args.evidence.mkdir(parents=True, exist_ok=True)
|
||||
path = ROOT/'system/hardware/power_monitoring.py'
|
||||
source = path.read_text()
|
||||
tree = ast.parse(source)
|
||||
cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name=='PowerMonitoring')
|
||||
methods = [n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name in ['shutdown_reason','should_shutdown']]
|
||||
assert len(methods)==2
|
||||
clock = SimpleNamespace(now=0.)
|
||||
ns = {'time':SimpleNamespace(monotonic=lambda:clock.now),'SimpleNamespace':SimpleNamespace}
|
||||
nodes: list[ast.stmt] = [n for n in tree.body if isinstance(n, ast.Assign)]
|
||||
nodes.extend(methods)
|
||||
exec(compile(ast.Module(body=nodes,type_ignores=[]),str(path)+':actual-methods','exec'), ns)
|
||||
values = {'DisablePowerDown':False,'ForcePowerDown':False}
|
||||
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 = {}
|
||||
for t in range(2,3703):
|
||||
clock.now=float(t)
|
||||
result = reason()
|
||||
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()
|
||||
pm.car_battery_capacity_uWh=30e6; pm.car_voltage_mV=11000
|
||||
assert reason() is None
|
||||
clock.now+=29; assert reason() is None
|
||||
clock.now+=1; assert reason()=='low_voltage'
|
||||
observed['low_voltage']=reason()
|
||||
values['ForcePowerDown']=True; assert reason()=='forced_power_down'
|
||||
observed['forced']=reason()
|
||||
values['ForcePowerDown']=False; pm.car_voltage_mV=14000
|
||||
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
|
||||
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.'
|
||||
(args.evidence/'host-policy.json').write_text(json.dumps(observed,indent=2)+'\n')
|
||||
print(json.dumps(observed,indent=2))
|
||||
print('PASS stock shutdown policy assertions; no live Params or hardware accessed.')
|
||||
|
||||
|
||||
if __name__ == '__main__': main()
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile verbatim current C functions, not a Python decoder/power-policy model.
|
||||
No SCons execution, firmware build, artifact signing, or device access.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
BASE = 'bb04e935272ccbc7551dd5f46d18197757a35587'
|
||||
FILES = ['panda/board/drivers/can_common.h', 'panda/board/main.c',
|
||||
'panda/board/drivers/bootkick.h', 'panda/board/boards/cuatro.h']
|
||||
|
||||
|
||||
def between(text, start, end):
|
||||
assert text.count(start) == 1, start
|
||||
tail = text[text.index(start):]
|
||||
assert end in tail, end
|
||||
return tail[:tail.index(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:
|
||||
globals_ = 'bool wake_on_can=false; uint32_t wake_on_can_cnt=0;\n' + globals_
|
||||
chunks = [
|
||||
('can globals', globals_),
|
||||
('decoder', between(can, 'void ignition_can_hook(CANPacket_t *msg) {', '\nbool can_tx_check_min_slots_free')),
|
||||
('ignition line', between(main, 'static bool panda_ignition_line(void) {', '\n\n// ********************* Serial')),
|
||||
('car safety predicate', between(main, 'bool is_car_safety_mode(uint16_t mode) {', '\n// ***************************** main')),
|
||||
('cuatro GPIO callback', between(cuatro, 'static void cuatro_set_bootkick(BootState state) {', '\nstatic void cuatro_set_amp_enabled')),
|
||||
('bootkick', boot),
|
||||
('entire 8Hz tick', between(main, '#define HEARTBEAT_IGNITION_CNT_ON', '\nint main(void) {')),
|
||||
]
|
||||
call = 'bootkick_tick(ign, hb, wake);' if independent_wake else '(void)wake; bootkick_tick(ign, hb);'
|
||||
adapter = '\nstatic void test_bootkick(bool ign, bool hb, bool wake) { ' + call + ' }\n'
|
||||
adapter += 'static void legacy_bootkick(bool ign, bool hb) { test_bootkick(ign, hb, false); }\n'
|
||||
output = '#include "fixture.h"\n' + '\n'.join(x[1] for x in chunks) + adapter + '\n#include "scenarios.h"\n'
|
||||
return output, {name: hashlib.sha256(text.encode()).hexdigest() for name, text in chunks}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--evidence', type=Path, required=True)
|
||||
parser.add_argument('--red', action='store_true', help='Expect stock states assertion to fail')
|
||||
parser.add_argument('--base', default=BASE, help='Stock Git revision for parity checks')
|
||||
parser.add_argument('--regression-ref', help='Optional historical pre-fix Git revision; expect DRIVE wake failure')
|
||||
args = parser.parse_args()
|
||||
args.evidence.mkdir(parents=True, exist_ok=True)
|
||||
source, hashes = generate(lambda p: (ROOT / p).read_text())
|
||||
if args.regression_ref:
|
||||
source, hashes = generate(lambda p: subprocess.check_output(['git', 'show', f'{args.regression_ref}:{p}'], cwd=ROOT, text=True))
|
||||
stock, stock_hashes = generate(lambda p: subprocess.check_output(['git', 'show', f'{args.base}:{p}'], cwd=ROOT, text=True))
|
||||
(args.evidence / 'candidate.c').write_text(source)
|
||||
(args.evidence / 'stock.c').write_text(stock)
|
||||
tested_files = FILES + ['panda/board/drivers/can_common_declarations.h',
|
||||
'panda/board/drivers/bootkick_declarations.h',
|
||||
'panda/board/boards/board_declarations.h',
|
||||
'opendbc_repo/opendbc/safety/can.h']
|
||||
tested_files += [str(p.relative_to(ROOT)) for p in sorted(HERE.iterdir()) if p.suffix in {'.py', '.h', '.md'}]
|
||||
manifest = {'base': args.base, 'source_sha256': {p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest() for p in tested_files},
|
||||
'extracted_sha256': hashes, 'stock_extracted_sha256': stock_hashes, 'runs': []}
|
||||
scenarios = ['states','invalid','stale','watchdog','drive_watchdog','boot','serial','gpio','existing','other_cars','drive_edge',
|
||||
'independent_edges','wake_reset','stock_drive']
|
||||
for tesla, hkg, gm, ignore in itertools.product([False, True], repeat=4):
|
||||
for debug, allow_debug in [(False,False), (True,True), (False,True)]:
|
||||
variant = f'tesla{int(tesla)}-hkg{int(hkg)}-gm{int(gm)}-ignore{int(ignore)}-debug{int(debug)}-allow{int(allow_debug)}'
|
||||
flags = [f'-D{x}' for x, yes in [('PANDA_TESLA_WAKE_ON_CAN',tesla),('PANDA_HKG_REMOTE_START',hkg),('PANDA_GM_REMOTE_START_C9',gm),
|
||||
('PANDA_IGNORE_IGNITION_LINE',ignore),('DEBUG',debug),('ALLOW_DEBUG',allow_debug)] if yes]
|
||||
binaries = {}
|
||||
for name in ['candidate', 'stock']:
|
||||
binary = args.evidence / f'{name}-{variant}'
|
||||
command = [os.environ.get('CC','cc'), '-std=gnu11', '-Wall','-Wextra','-Werror',
|
||||
'-Wno-sign-compare', '-O2','-g','-fsanitize=undefined','-fno-sanitize-recover=all',
|
||||
*flags, '-I'+str(HERE), '-I'+str(ROOT/'panda'), '-I'+str(ROOT/'panda/board/drivers'),
|
||||
'-I'+str(ROOT/'opendbc_repo'), str(args.evidence/f'{name}.c'), '-o', str(binary)]
|
||||
subprocess.run(command, check=True)
|
||||
binaries[name] = binary
|
||||
manifest['runs'].append({'compile': command})
|
||||
if args.regression_ref:
|
||||
run = subprocess.run([str(binaries['candidate']), 'drive_edge'], text=True, capture_output=True)
|
||||
assert run.returncode != 0 and 'observed_boot==BOOT_BOOTKICK' in run.stderr, run.stderr
|
||||
manifest['runs'].append({'variant':variant,'regression_red_rc':run.returncode,'stderr':run.stderr})
|
||||
(args.evidence/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||||
print('RED verified: pre-fix candidate masks DRIVE:', run.stderr.strip())
|
||||
return
|
||||
if args.red:
|
||||
run = subprocess.run([str(binaries['stock']), 'states'], text=True, capture_output=True)
|
||||
assert run.returncode != 0 and 'wake_on_can == (state != 0)' in run.stderr, run.stderr
|
||||
print('RED verified: stock fails non-OFF wake assertion:', run.stderr.strip())
|
||||
return
|
||||
selected = scenarios + ['checksum'] if tesla else [
|
||||
'disabled', 'drive_watchdog', 'boot', 'serial', 'gpio', 'existing', 'other_cars',
|
||||
'stock_drive', 'independent_edges', 'wake_reset']
|
||||
for scenario in selected:
|
||||
run = subprocess.run([str(binaries['candidate']),scenario], text=True,capture_output=True)
|
||||
manifest['runs'].append({'variant':variant,'scenario':scenario,'rc':run.returncode,'stdout':run.stdout,'stderr':run.stderr})
|
||||
if run.returncode:
|
||||
print(variant, scenario, run.stdout, run.stderr)
|
||||
raise SystemExit(run.returncode)
|
||||
subprocess.run([str(binaries['stock']), 'stock_drive'], check=True)
|
||||
boot_traces = [subprocess.check_output([str(binaries[name]),'boot_trace']) for name in ['candidate','stock']]
|
||||
assert boot_traces[0] == boot_traces[1], f'Boot/reset/harness parity failed: {variant}'
|
||||
(args.evidence / f'boot-parity-{variant}.txt').write_bytes(boot_traces[0])
|
||||
manifest['runs'].append({'variant':variant,'boot_parity_frames':20000,'sha256':hashlib.sha256(boot_traces[0]).hexdigest()})
|
||||
traces = [subprocess.check_output([str(binaries[name]),'trace']) for name in ['candidate','stock']]
|
||||
assert traces[0] == traces[1], f'Ignition/watchdog parity failed: {variant}'
|
||||
trace_hash = hashlib.sha256(traces[0]).hexdigest()
|
||||
(args.evidence / f'parity-{variant}.txt').write_bytes(traces[0])
|
||||
manifest['runs'].append({'variant':variant,'parity_frames':20000,'sha256':trace_hash})
|
||||
print(f'PASS {variant}: {len(selected)} scenarios; 20000-frame ignition/watchdog and boot/reset stock parity; DRIVE edge preserved')
|
||||
(args.evidence/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||||
summary = {'configurations':sum('parity_frames' in r for r in manifest['runs']),
|
||||
'scenario_runs':sum('scenario' in r for r in manifest['runs']),
|
||||
'stock_parity_frames':sum(r.get('parity_frames',0) for r in manifest['runs']),
|
||||
'boot_parity_frames':sum(r.get('boot_parity_frames',0) for r in manifest['runs'])}
|
||||
(args.evidence/'summary.json').write_text(json.dumps(summary,indent=2)+'\n')
|
||||
print('PASS:', summary, 'NOT hardware/release clearance.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,285 @@
|
||||
// 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;
|
||||
return p;
|
||||
}
|
||||
static void tesla(unsigned state, unsigned counter) {
|
||||
CANPacket_t p=packet(0,0x221,8,state,counter); ignition_can_hook(&p);
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
static void invalid_case(void) {
|
||||
pair(2); assert(wake_on_can); wake_on_can_cnt=2;
|
||||
for (unsigned bus=1; bus<8; bus++) {
|
||||
CANPacket_t p=packet(bus,0x221,8,0,0); ignition_can_hook(&p);
|
||||
assert(wake_on_can && wake_on_can_cnt == 2);
|
||||
}
|
||||
for (unsigned dlc=0; dlc<16; dlc++) {
|
||||
if (dlc==8) continue;
|
||||
CANPacket_t p=packet(0,0x221,dlc,0,0); ignition_can_hook(&p);
|
||||
assert(wake_on_can && wake_on_can_cnt==2);
|
||||
}
|
||||
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(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
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
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(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);
|
||||
tesla(2,3); assert(!wake_on_can);
|
||||
tesla(0,4); assert(!wake_on_can);
|
||||
}
|
||||
p=packet(0,0x221,8,2,5); p.extended=1; ignition_can_hook(&p);
|
||||
assert(!wake_on_can);
|
||||
tesla(2,6); assert(!wake_on_can);
|
||||
tesla(2,7); assert(wake_on_can);
|
||||
wake_on_can_cnt=2;
|
||||
for (unsigned i=0; i<4; i++) {
|
||||
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.
|
||||
p=packet(0,0x221,8,3,12); p.data[7]^=1; ignition_can_hook(&p);
|
||||
assert(ignition_can && !wake_on_can);
|
||||
}
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
static void watchdog_case(void) {
|
||||
pair(2); assert(wake_on_can && !ignition_can);
|
||||
heartbeat_disabled=true; controls_allowed=true; heartbeat_engaged=true; som_gpio=true;
|
||||
second(); assert(heartbeat_counter==1 && !heartbeat_disabled && !heartbeat_lost);
|
||||
second(); assert(heartbeat_counter==2 && heartbeat_lost && !heartbeat_engaged);
|
||||
assert(current_safety_mode==SAFETY_SILENT && silent_calls==1);
|
||||
assert(power_save_status==POWER_SAVE_STATUS_ENABLED && ir_calls==1 && fan_power==30);
|
||||
assert(watchdog_kicks==16 && safety_ticks==2 && register_checks==2);
|
||||
heartbeat_counter=UINT32_MAX; second(); assert(heartbeat_counter==UINT32_MAX);
|
||||
}
|
||||
static void drive_watchdog_case(void) {
|
||||
pair(3);
|
||||
for (unsigned i=1; i<5; i++) {
|
||||
tesla(3,(15+i)%16); second(); assert(!heartbeat_lost && heartbeat_counter==i);
|
||||
}
|
||||
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
|
||||
for (int i=0; i<30; i++) legacy_bootkick(false,false);
|
||||
assert(!bootkick_reset_triggered); // no first-boot reset
|
||||
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
|
||||
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
|
||||
}
|
||||
static void cancel_reset_case(int serial) {
|
||||
legacy_bootkick(false,true); legacy_bootkick(true,false);
|
||||
if (serial) uart_ring_som_debug.w_ptr_tx++; else som_gpio=true;
|
||||
legacy_bootkick(true,false); som_gpio=false;
|
||||
for (int i=0; i<30; i++) legacy_bootkick(true,false);
|
||||
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);
|
||||
line=true; heartbeat_counter=1; second();
|
||||
#ifdef PANDA_IGNORE_IGNITION_LINE
|
||||
assert(observed_boot==BOOT_STANDBY);
|
||||
#else
|
||||
assert(observed_boot==BOOT_BOOTKICK);
|
||||
#endif
|
||||
}
|
||||
static void other_cars_case(void) {
|
||||
CANPacket_t p=packet(0,0x1F1,8,0,0); p.data[0]=2; ignition_can_hook(&p);
|
||||
#ifdef PANDA_GM_REMOTE_START_C9
|
||||
assert(!ignition_can);
|
||||
#else
|
||||
assert(ignition_can);
|
||||
#endif
|
||||
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
|
||||
#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);
|
||||
assert(hkg_remote_climate_wake && !ignition_can && !wake_on_can);
|
||||
heartbeat_counter=1; second(); assert(observed_boot==BOOT_BOOTKICK && !heartbeat_lost);
|
||||
for (int i=0; i<3; i++) second();
|
||||
assert(!hkg_remote_climate_wake && hkg_remote_climate_wake_cnt==4);
|
||||
#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.
|
||||
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
|
||||
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
|
||||
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);
|
||||
tesla(3,4); second();
|
||||
assert(ignition_can && observed_boot==BOOT_BOOTKICK);
|
||||
}
|
||||
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_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(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(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
|
||||
}
|
||||
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);
|
||||
}
|
||||
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
|
||||
}
|
||||
static void boot_trace_case(void) {
|
||||
uint32_t rng=0x87654321;
|
||||
for (unsigned i=0; i<20000; i++) {
|
||||
rng=rng*1664525U+1013904223U;
|
||||
if (i%37==0) harness.status=(rng>>20)%3;
|
||||
if (i%53==0) uart_ring_som_debug.w_ptr_tx++;
|
||||
som_gpio=(rng%97)==0;
|
||||
legacy_bootkick((rng>>30)!=0,(rng%29)==0);
|
||||
printf("%u %u %u %u %u\n",i,observed_boot,bootkick_reset_triggered,gpio_level,gpio_calls);
|
||||
}
|
||||
}
|
||||
static void trace_case(void) {
|
||||
uint32_t rng=0x12345678;
|
||||
const unsigned ids[]={0x221,0x101,0x152,0x1F1,0xC9,0x9E,0x384,0x220};
|
||||
for (unsigned i=0; i<20000; i++) {
|
||||
rng=rng*1664525U+1013904223U;
|
||||
CANPacket_t p=packet((rng>>29)%3,ids[(rng>>20)%8],(rng>>8)%16,(rng>>2)%4,i%16);
|
||||
for (unsigned b=0;b<8;b++) p.data[b]=(rng>>(b*3))&255;
|
||||
if ((i%3)==0) p=packet(0,0x221,8,i%4,i%16);
|
||||
line=(rng&255)==0; gm_remote_start_boots_comma=(rng&256)!=0;
|
||||
if ((i%17)==0) { heartbeat_counter=0; current_safety_mode=42; controls_allowed=true; heartbeat_engaged=true; }
|
||||
ignition_can_hook(&p); tick();
|
||||
printf("%u %u %u %u %u %u %u %u %u %u %u %u %u\n",i,ignition_can,ignition_can_cnt,heartbeat_counter,
|
||||
current_safety_mode,heartbeat_lost,heartbeat_engaged,controls_allowed,heartbeat_engaged_mismatches,
|
||||
power_save_status,watchdog_kicks,safety_ticks,ir_calls);
|
||||
}
|
||||
}
|
||||
int main(int argc,char **argv) {
|
||||
assert(argc==2);
|
||||
if (!strcmp(argv[1],"disabled")) disabled_case();
|
||||
else if (!strcmp(argv[1],"checksum")) checksum_case();
|
||||
else if (!strcmp(argv[1],"states")) wake_case();
|
||||
else if (!strcmp(argv[1],"invalid")) invalid_case();
|
||||
else if (!strcmp(argv[1],"stale")) stale_case();
|
||||
else if (!strcmp(argv[1],"watchdog")) watchdog_case();
|
||||
else if (!strcmp(argv[1],"drive_watchdog")) drive_watchdog_case();
|
||||
else if (!strcmp(argv[1],"boot")) boot_case();
|
||||
else if (!strcmp(argv[1],"serial")) cancel_reset_case(1);
|
||||
else if (!strcmp(argv[1],"gpio")) cancel_reset_case(0);
|
||||
else if (!strcmp(argv[1],"existing")) existing_case();
|
||||
else if (!strcmp(argv[1],"other_cars")) other_cars_case();
|
||||
else if (!strcmp(argv[1],"drive_edge")) drive_edge_case();
|
||||
else if (!strcmp(argv[1],"stock_drive")) stock_drive_case();
|
||||
else if (!strcmp(argv[1],"independent_edges")) independent_edges_case();
|
||||
else if (!strcmp(argv[1],"wake_reset")) wake_reset_case();
|
||||
else if (!strcmp(argv[1],"boot_trace")) boot_trace_case();
|
||||
else if (!strcmp(argv[1],"trace")) trace_case();
|
||||
else return 2;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Firmware selection shared by pandad and the parked updater."""
|
||||
|
||||
import os
|
||||
|
||||
from cereal import car
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params, UnknownKeyName
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
|
||||
FW_PATH = os.path.join(BASEDIR, "panda", "board", "obj")
|
||||
|
||||
TESLA_CAN_WAKE_PLATFORMS = {"TESLA_MODEL_3", "TESLA_MODEL_Y", "TESLA_MODEL_X"}
|
||||
|
||||
|
||||
def supports_tesla_can_wake(params: Params) -> bool:
|
||||
try:
|
||||
selected_make = params.get("CarMake") or ""
|
||||
selected_model = params.get("CarModel") or ""
|
||||
if isinstance(selected_make, bytes):
|
||||
selected_make = selected_make.decode("utf-8")
|
||||
if isinstance(selected_model, bytes):
|
||||
selected_model = selected_model.decode("utf-8")
|
||||
if selected_make and selected_make.strip().lower() != "tesla":
|
||||
return False
|
||||
cp_bytes = params.get("CarParams")
|
||||
if cp_bytes is None:
|
||||
cp_bytes = params.get("CarParamsPersistent")
|
||||
if not cp_bytes:
|
||||
return False
|
||||
with car.CarParams.from_bytes(cp_bytes) as CP:
|
||||
return (CP.brand == "tesla" and CP.carFingerprint in TESLA_CAN_WAKE_PLATFORMS and
|
||||
(not selected_model or selected_model == CP.carFingerprint))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_tesla_wake_on_can(params: Params) -> bool:
|
||||
try:
|
||||
return params.get_bool("TeslaWakeOnCAN") and supports_tesla_can_wake(params)
|
||||
except UnknownKeyName:
|
||||
return False
|
||||
|
||||
|
||||
def firmware_flags_conflict(params: Params, key: str, enabled: bool) -> bool:
|
||||
if not enabled:
|
||||
return False
|
||||
try:
|
||||
if key == "TeslaWakeOnCAN":
|
||||
return params.get_bool("RemoteStartBootsComma") or params.get_bool("HKGRemoteStartBootsComma")
|
||||
if key in {"RemoteStartBootsComma", "HKGRemoteStartBootsComma"}:
|
||||
return params.get_bool("TeslaWakeOnCAN")
|
||||
except UnknownKeyName:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def get_selected_firmware_name(app_fn: str, remote_start: bool, hkg_remote_start: bool, ignore_ignition_line: bool,
|
||||
tesla_wake: bool = False) -> str:
|
||||
if tesla_wake and (remote_start or hkg_remote_start):
|
||||
raise ValueError("Tesla wake firmware cannot be combined with remote-start firmware")
|
||||
if not remote_start and not hkg_remote_start and not ignore_ignition_line and not tesla_wake:
|
||||
return app_fn
|
||||
|
||||
name_parts = ["panda_h7" if app_fn == "panda_h7.bin.signed" else "panda"]
|
||||
if tesla_wake:
|
||||
name_parts.extend(["tesla", "wake"])
|
||||
elif hkg_remote_start:
|
||||
name_parts.extend(["hkg", "remote"])
|
||||
elif remote_start:
|
||||
name_parts.append("remote")
|
||||
if ignore_ignition_line:
|
||||
name_parts.append("can_ignition_only")
|
||||
return "_".join(name_parts) + ".bin.signed"
|
||||
|
||||
|
||||
def get_firmware_path(fw_path: str, app_fn: str, remote_start: bool, hkg_remote_start: bool, ignore_ignition_line: bool,
|
||||
tesla_wake: bool = False) -> str:
|
||||
selected_fn = get_selected_firmware_name(app_fn, remote_start, hkg_remote_start, ignore_ignition_line, tesla_wake)
|
||||
selected_path = os.path.join(fw_path, selected_fn)
|
||||
if selected_fn != app_fn and not os.path.isfile(selected_path):
|
||||
if tesla_wake:
|
||||
raise FileNotFoundError(f"Tesla wake firmware not found: {selected_path}")
|
||||
cloudlog.warning(f"Selected panda firmware not found: {selected_path}, falling back to default")
|
||||
return os.path.join(fw_path, app_fn)
|
||||
return selected_path
|
||||
|
||||
|
||||
def validate_tesla_can_wake_firmware(params: Params, enabled: bool) -> None:
|
||||
"""Verify both Tesla wake images before changing the setting."""
|
||||
remote_start = params.get_bool("RemoteStartBootsComma")
|
||||
hkg_remote_start = params.get_bool("HKGRemoteStartBootsComma")
|
||||
ignore_ignition_line = params.get_bool("IgnoreIgnitionLine")
|
||||
if enabled and (remote_start or hkg_remote_start):
|
||||
raise RuntimeError("Tesla wake cannot be enabled while remote-start firmware is enabled")
|
||||
for app_fn in ("panda.bin.signed", "panda_h7.bin.signed"):
|
||||
selected_fn = get_selected_firmware_name(app_fn, remote_start, hkg_remote_start, ignore_ignition_line, enabled)
|
||||
selected_path = os.path.join(FW_PATH, selected_fn)
|
||||
if not os.path.isfile(selected_path) or os.path.getsize(selected_path) == 0:
|
||||
raise RuntimeError(f"Required Panda firmware is missing or empty: {selected_fn}. Update the device firmware package before changing Wake on CAN.")
|
||||
+13
-31
@@ -12,37 +12,18 @@ from openpilot.common.params import Params, UnknownKeyName
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.pandad.rivian_long_flasher import prepare_rivian_bridge
|
||||
from openpilot.selfdrive.pandad.panda_firmware import get_firmware_path, get_tesla_wake_on_can
|
||||
from openpilot.selfdrive.pandad.panda_firmware import get_selected_firmware_name as get_selected_firmware_name
|
||||
|
||||
|
||||
def get_selected_firmware_name(app_fn: str, remote_start: bool, hkg_remote_start: bool, ignore_ignition_line: bool) -> str:
|
||||
if not remote_start and not hkg_remote_start and not ignore_ignition_line:
|
||||
return app_fn
|
||||
|
||||
h7 = app_fn == "panda_h7.bin.signed"
|
||||
name_parts = ["panda_h7" if h7 else "panda"]
|
||||
if hkg_remote_start:
|
||||
name_parts.extend(["hkg", "remote"])
|
||||
elif remote_start:
|
||||
name_parts.append("remote")
|
||||
if ignore_ignition_line:
|
||||
name_parts.append("can_ignition_only")
|
||||
return "_".join(name_parts) + ".bin.signed"
|
||||
def get_expected_firmware_path(panda: Panda, remote_start: bool, hkg_remote_start: bool, ignore_ignition_line: bool,
|
||||
tesla_wake: bool = False) -> str:
|
||||
return get_firmware_path(FW_PATH, panda.get_mcu_type().config.app_fn, remote_start, hkg_remote_start, ignore_ignition_line, tesla_wake)
|
||||
|
||||
|
||||
def get_expected_firmware_path(panda: Panda, remote_start: bool, hkg_remote_start: bool, ignore_ignition_line: bool) -> str:
|
||||
app_fn = panda.get_mcu_type().config.app_fn
|
||||
selected_fn = get_selected_firmware_name(app_fn, remote_start, hkg_remote_start, ignore_ignition_line)
|
||||
if selected_fn != app_fn:
|
||||
selected_path = os.path.join(FW_PATH, selected_fn)
|
||||
if os.path.isfile(selected_path):
|
||||
return selected_path
|
||||
cloudlog.warning(f"Selected panda firmware not found: {selected_path}, falling back to default")
|
||||
return os.path.join(FW_PATH, app_fn)
|
||||
|
||||
|
||||
def get_expected_signature(panda: Panda, remote_start: bool, hkg_remote_start: bool, ignore_ignition_line: bool) -> bytes:
|
||||
def get_expected_signature(panda: Panda, remote_start: bool, hkg_remote_start: bool, ignore_ignition_line: bool, tesla_wake: bool = False) -> bytes:
|
||||
try:
|
||||
fn = get_expected_firmware_path(panda, remote_start, hkg_remote_start, ignore_ignition_line)
|
||||
fn = get_expected_firmware_path(panda, remote_start, hkg_remote_start, ignore_ignition_line, tesla_wake)
|
||||
return Panda.get_signature_from_firmware(fn)
|
||||
except Exception:
|
||||
cloudlog.exception("Error computing expected signature")
|
||||
@@ -70,7 +51,7 @@ def get_ignore_ignition_line(params: Params) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def flash_panda(panda_serial: str, remote_start: bool, hkg_remote_start: bool, ignore_ignition_line: bool) -> Panda:
|
||||
def flash_panda(panda_serial: str, remote_start: bool, hkg_remote_start: bool, ignore_ignition_line: bool, tesla_wake: bool = False) -> Panda:
|
||||
try:
|
||||
panda = Panda(panda_serial)
|
||||
except PandaProtocolMismatch:
|
||||
@@ -78,8 +59,8 @@ def flash_panda(panda_serial: str, remote_start: bool, hkg_remote_start: bool, i
|
||||
HARDWARE.recover_internal_panda()
|
||||
raise
|
||||
|
||||
fw_path = get_expected_firmware_path(panda, remote_start, hkg_remote_start, ignore_ignition_line)
|
||||
fw_signature = get_expected_signature(panda, remote_start, hkg_remote_start, ignore_ignition_line)
|
||||
fw_path = get_expected_firmware_path(panda, remote_start, hkg_remote_start, ignore_ignition_line, tesla_wake)
|
||||
fw_signature = get_expected_signature(panda, remote_start, hkg_remote_start, ignore_ignition_line, tesla_wake)
|
||||
internal_panda = panda.is_internal()
|
||||
|
||||
panda_version = "bootstub" if panda.bootstub else panda.get_version()
|
||||
@@ -172,8 +153,9 @@ def main() -> None:
|
||||
remote_start = get_remote_start_boots_comma(params)
|
||||
hkg_remote_start = get_hkg_remote_start_boots_comma(params)
|
||||
ignore_ignition_line = get_ignore_ignition_line(params)
|
||||
tesla_wake = get_tesla_wake_on_can(params)
|
||||
for serial in panda_serials:
|
||||
pandas.append(flash_panda(serial, remote_start, hkg_remote_start, ignore_ignition_line))
|
||||
pandas.append(flash_panda(serial, remote_start, hkg_remote_start, ignore_ignition_line, tesla_wake))
|
||||
|
||||
# Ensure internal panda is present if expected
|
||||
internal_pandas = [panda for panda in pandas if panda.is_internal()]
|
||||
@@ -225,7 +207,7 @@ def main() -> None:
|
||||
first_run = False
|
||||
|
||||
# run pandad with all connected serials as arguments
|
||||
if get_remote_start_boots_comma(params) or get_hkg_remote_start_boots_comma(params) or get_ignore_ignition_line(params):
|
||||
if remote_start or hkg_remote_start or ignore_ignition_line or tesla_wake:
|
||||
os.environ["BOARDD_SKIP_FW_CHECK"] = "1"
|
||||
else:
|
||||
os.environ.pop("BOARDD_SKIP_FW_CHECK", None)
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
from itertools import product
|
||||
from types import SimpleNamespace
|
||||
|
||||
from cereal import car
|
||||
import pytest
|
||||
|
||||
from openpilot.selfdrive.pandad import panda_firmware
|
||||
from openpilot.selfdrive.pandad import pandad
|
||||
|
||||
|
||||
class VehicleParams:
|
||||
def __init__(self, **values):
|
||||
self.values = values
|
||||
|
||||
def get(self, key):
|
||||
return self.values.get(key)
|
||||
|
||||
def get_bool(self, key):
|
||||
return bool(self.get(key))
|
||||
|
||||
|
||||
def car_params(brand="tesla", fingerprint="TESLA_MODEL_3"):
|
||||
return car.CarParams.new_message(brand=brand, carFingerprint=fingerprint).to_bytes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("h7,ignore", list(product([False, True], repeat=2)))
|
||||
def test_tesla_variant_uses_ignore_ignition_line(h7, ignore):
|
||||
app_fn = "panda_h7.bin.signed" if h7 else "panda.bin.signed"
|
||||
expected = {
|
||||
(False, False): "panda_tesla_wake.bin.signed",
|
||||
(False, True): "panda_tesla_wake_can_ignition_only.bin.signed",
|
||||
(True, False): "panda_h7_tesla_wake.bin.signed",
|
||||
(True, True): "panda_h7_tesla_wake_can_ignition_only.bin.signed",
|
||||
}[h7, ignore]
|
||||
assert pandad.get_selected_firmware_name(app_fn, False, False, ignore, tesla_wake=True) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("gm,hkg,ignore,expected", [
|
||||
(False, False, False, "panda.bin.signed"),
|
||||
(False, False, True, "panda_can_ignition_only.bin.signed"),
|
||||
(True, False, False, "panda_remote.bin.signed"),
|
||||
(True, False, True, "panda_remote_can_ignition_only.bin.signed"),
|
||||
(False, True, False, "panda_hkg_remote.bin.signed"),
|
||||
(False, True, True, "panda_hkg_remote_can_ignition_only.bin.signed"),
|
||||
(True, True, False, "panda_hkg_remote.bin.signed"),
|
||||
(True, True, True, "panda_hkg_remote_can_ignition_only.bin.signed"),
|
||||
])
|
||||
@pytest.mark.parametrize("h7", [False, True])
|
||||
def test_existing_firmware_selection_unchanged(gm, hkg, ignore, expected, h7):
|
||||
app_fn = "panda_h7.bin.signed" if h7 else "panda.bin.signed"
|
||||
if h7:
|
||||
expected = expected.replace("panda", "panda_h7", 1)
|
||||
assert pandad.get_selected_firmware_name(app_fn, gm, hkg, ignore) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("remote_start,hkg_remote_start", [(True, False), (False, True), (True, True)])
|
||||
def test_tesla_wake_rejects_remote_start_firmware(remote_start, hkg_remote_start):
|
||||
with pytest.raises(ValueError, match="cannot be combined"):
|
||||
pandad.get_selected_firmware_name("panda_h7.bin.signed", remote_start, hkg_remote_start, False, tesla_wake=True)
|
||||
|
||||
|
||||
def test_tesla_wake_preflight_rejects_remote_start_firmware():
|
||||
params = VehicleParams(RemoteStartBootsComma=True, HKGRemoteStartBootsComma=False, IgnoreIgnitionLine=False)
|
||||
with pytest.raises(RuntimeError, match="cannot be enabled"):
|
||||
panda_firmware.validate_tesla_can_wake_firmware(params, True)
|
||||
|
||||
|
||||
def test_firmware_toggle_conflicts_are_symmetric():
|
||||
params = VehicleParams(TeslaWakeOnCAN=True, RemoteStartBootsComma=False, HKGRemoteStartBootsComma=False)
|
||||
assert panda_firmware.firmware_flags_conflict(params, "RemoteStartBootsComma", True)
|
||||
assert not panda_firmware.firmware_flags_conflict(params, "RemoteStartBootsComma", False)
|
||||
params.values["RemoteStartBootsComma"] = True
|
||||
assert panda_firmware.firmware_flags_conflict(params, "TeslaWakeOnCAN", True)
|
||||
assert not panda_firmware.firmware_flags_conflict(params, "TeslaWakeOnCAN", False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("values,expected", [
|
||||
({}, False),
|
||||
({"TeslaWakeOnCAN": True}, False),
|
||||
({"TeslaWakeOnCAN": True, "CarMake": "Tesla"}, False),
|
||||
({"CarParamsPersistent": car_params()}, False),
|
||||
({"TeslaWakeOnCAN": True, "CarParamsPersistent": car_params()}, True),
|
||||
({"TeslaWakeOnCAN": True, "CarMake": b"Tesla", "CarParamsPersistent": car_params(fingerprint="TESLA_MODEL_Y")}, True),
|
||||
({"TeslaWakeOnCAN": True, "CarMake": "Toyota", "CarParamsPersistent": car_params()}, False),
|
||||
({"TeslaWakeOnCAN": True, "CarModel": "TESLA_MODEL_X", "CarParamsPersistent": car_params()}, False),
|
||||
({"TeslaWakeOnCAN": True, "CarParams": car_params("toyota", "TOYOTA_PRIUS"), "CarParamsPersistent": car_params()}, False),
|
||||
({"TeslaWakeOnCAN": True, "CarParams": b"corrupt", "CarParamsPersistent": car_params()}, False),
|
||||
({"TeslaWakeOnCAN": True, "CarParamsPersistent": b"corrupt"}, False),
|
||||
({"TeslaWakeOnCAN": True, "CarParamsPersistent": car_params(fingerprint="TESLA_MODEL_X")}, True),
|
||||
({"TeslaWakeOnCAN": True, "CarParamsPersistent": car_params(fingerprint="TESLA_MODEL_S_PREAP")}, False),
|
||||
({"TeslaWakeOnCAN": True, "CarParamsPersistent": car_params(fingerprint="")}, False),
|
||||
({"TeslaWakeOnCAN": True, "CarParamsPersistent": car_params("hyundai", "HYUNDAI_SONATA")}, False),
|
||||
({"TeslaWakeOnCAN": True, "CarParamsCache": car_params(), "CarParamsPrevRoute": car_params()}, False),
|
||||
])
|
||||
def test_tesla_wake_requires_enabled_supported_vehicle_without_conflicting_identity(values, expected):
|
||||
assert hasattr(pandad, "get_tesla_wake_on_can"), "Tesla wake parameter reader is missing"
|
||||
assert pandad.get_tesla_wake_on_can(VehicleParams(**values)) is expected
|
||||
|
||||
|
||||
def test_unregistered_tesla_param_defaults_off():
|
||||
class OldParams(VehicleParams):
|
||||
def get_bool(self, key):
|
||||
raise pandad.UnknownKeyName(key)
|
||||
|
||||
assert hasattr(pandad, "get_tesla_wake_on_can"), "Tesla wake parameter reader is missing"
|
||||
assert pandad.get_tesla_wake_on_can(OldParams()) is False
|
||||
|
||||
|
||||
def test_missing_tesla_firmware_never_falls_back_to_stock(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(pandad, "FW_PATH", str(tmp_path))
|
||||
panda = SimpleNamespace(get_mcu_type=lambda: SimpleNamespace(config=SimpleNamespace(app_fn="panda_h7.bin.signed")))
|
||||
with pytest.raises(FileNotFoundError, match="panda_h7_tesla_wake"):
|
||||
pandad.get_expected_firmware_path(panda, False, False, False, tesla_wake=True)
|
||||
|
||||
|
||||
def test_selected_tesla_firmware_path_used(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(pandad, "FW_PATH", str(tmp_path))
|
||||
firmware = tmp_path / "panda_h7_tesla_wake_can_ignition_only.bin.signed"
|
||||
firmware.touch()
|
||||
panda = SimpleNamespace(get_mcu_type=lambda: SimpleNamespace(config=SimpleNamespace(app_fn="panda_h7.bin.signed")))
|
||||
assert pandad.get_expected_firmware_path(panda, False, False, True, tesla_wake=True) == str(firmware)
|
||||
|
||||
|
||||
class FirmwarePanda:
|
||||
get_signature_from_firmware = staticmethod(pandad.Panda.get_signature_from_firmware)
|
||||
|
||||
def __init__(self, app_fn="panda_h7.bin.signed"):
|
||||
self.app_fn = app_fn
|
||||
self.bootstub = False
|
||||
self.signature = b"old firmware"
|
||||
self.flashed = []
|
||||
|
||||
def get_mcu_type(self):
|
||||
return SimpleNamespace(config=SimpleNamespace(app_fn=self.app_fn))
|
||||
|
||||
def is_internal(self):
|
||||
return True
|
||||
|
||||
def get_version(self):
|
||||
return "test"
|
||||
|
||||
def get_signature(self):
|
||||
return self.signature
|
||||
|
||||
def flash(self, fn):
|
||||
self.flashed.append(fn)
|
||||
self.signature = self.get_signature_from_firmware(fn)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled,filename", [
|
||||
(True, "panda_h7_tesla_wake.bin.signed"),
|
||||
(False, "panda_h7.bin.signed"),
|
||||
])
|
||||
def test_startup_flashes_and_verifies_selected_signature(monkeypatch, tmp_path, enabled, filename):
|
||||
device = FirmwarePanda()
|
||||
firmware = tmp_path / filename
|
||||
firmware.write_bytes(b"s" * 128)
|
||||
monkeypatch.setattr(pandad, "FW_PATH", str(tmp_path))
|
||||
class PandaFactory:
|
||||
get_signature_from_firmware = staticmethod(FirmwarePanda.get_signature_from_firmware)
|
||||
|
||||
def __new__(cls, serial):
|
||||
assert serial == "test-panda"
|
||||
return device
|
||||
|
||||
monkeypatch.setattr(pandad, "Panda", PandaFactory)
|
||||
assert pandad.flash_panda("test-panda", False, False, False, tesla_wake=enabled) is device
|
||||
assert device.flashed == [str(firmware)]
|
||||
assert device.signature == b"s" * 128
|
||||
pandad.flash_panda("test-panda", False, False, False, tesla_wake=enabled)
|
||||
assert device.flashed == [str(firmware)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("available", [False, True])
|
||||
def test_manual_updater_uses_tesla_image_and_never_flashes_stock_when_missing(monkeypatch, tmp_path, available):
|
||||
from openpilot.starpilot.common import starpilot_utilities as utilities
|
||||
from openpilot.selfdrive.pandad import rivian_long_flasher
|
||||
|
||||
device = FirmwarePanda()
|
||||
firmware = tmp_path / "panda_h7_tesla_wake.bin.signed"
|
||||
if available:
|
||||
firmware.write_bytes(b"m" * 128)
|
||||
params = VehicleParams(TeslaWakeOnCAN=True, CarParamsPersistent=car_params())
|
||||
class PandaFactory:
|
||||
@staticmethod
|
||||
def list():
|
||||
return ["test-panda"]
|
||||
|
||||
@staticmethod
|
||||
def usb_list():
|
||||
return []
|
||||
|
||||
def __new__(cls, serial):
|
||||
assert serial == "test-panda"
|
||||
return device
|
||||
|
||||
monkeypatch.setattr(utilities, "Panda", PandaFactory)
|
||||
monkeypatch.setattr(utilities, "Params", lambda: params)
|
||||
monkeypatch.setattr(utilities, "FW_PATH", str(tmp_path))
|
||||
monkeypatch.setattr(rivian_long_flasher, "is_rivian_vehicle", lambda: False)
|
||||
errors = []
|
||||
monkeypatch.setattr(utilities, "capture_exception", errors.append)
|
||||
removed = []
|
||||
utilities.flash_panda(SimpleNamespace(remove=removed.append))
|
||||
assert device.flashed == ([str(firmware)] if available else [])
|
||||
assert bool(errors) is (not available)
|
||||
if errors:
|
||||
assert isinstance(errors[0], FileNotFoundError)
|
||||
assert removed == ["FlashPanda"]
|
||||
|
||||
|
||||
def test_other_variants_keep_existing_missing_image_fallback(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(pandad, "FW_PATH", str(tmp_path))
|
||||
assert pandad.get_expected_firmware_path(FirmwarePanda(), True, True, True) == str(tmp_path / "panda_h7.bin.signed")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [False, True])
|
||||
def test_main_skips_stock_cpp_signature_check_only_for_selected_variant(monkeypatch, enabled):
|
||||
device = FirmwarePanda()
|
||||
device.get_usb_serial = lambda: "test-panda"
|
||||
device.get_type = lambda: b"type"
|
||||
device.health = lambda: {"heartbeat_lost": False, "som_reset_triggered": False}
|
||||
device.reset = lambda **kwargs: None
|
||||
device.close = lambda: None
|
||||
params = VehicleParams(TeslaWakeOnCAN=enabled, CarParamsPersistent=car_params())
|
||||
params.remove = lambda key: params.values.pop(key, None)
|
||||
params.put = lambda key, value: params.values.update({key: value})
|
||||
monkeypatch.setattr(pandad, "Params", lambda: params)
|
||||
monkeypatch.setattr(pandad, "Panda", SimpleNamespace(list=lambda: ["test-panda"]))
|
||||
monkeypatch.setattr(pandad, "PandaDFU", SimpleNamespace(list=lambda: []))
|
||||
monkeypatch.setattr(pandad, "HARDWARE", SimpleNamespace(has_internal_panda=lambda: True))
|
||||
monkeypatch.setattr(pandad, "prepare_rivian_bridge", lambda serials: set())
|
||||
selections = []
|
||||
|
||||
def flash(serial, gm, hkg, ignore, tesla):
|
||||
selections.append((serial, gm, hkg, ignore, tesla))
|
||||
return device
|
||||
|
||||
monkeypatch.setattr(pandad, "flash_panda", flash)
|
||||
handlers = []
|
||||
monkeypatch.setattr(pandad.signal, "signal", lambda signum, handler: handlers.append(handler))
|
||||
environments = []
|
||||
|
||||
def start_process(args, cwd):
|
||||
environments.append(pandad.os.environ.get("BOARDD_SKIP_FW_CHECK"))
|
||||
return SimpleNamespace(wait=lambda: handlers[0](2, None), send_signal=lambda signum: None)
|
||||
|
||||
monkeypatch.setattr(pandad.subprocess, "Popen", start_process)
|
||||
monkeypatch.setenv("BOARDD_SKIP_FW_CHECK", "stale")
|
||||
pandad.main()
|
||||
assert selections == [("test-panda", False, False, False, enabled)]
|
||||
assert environments == (["1"] if enabled else [None])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled,ignore", list(product([False, True], repeat=2)))
|
||||
def test_ui_preflight_requires_both_board_images_without_writing(monkeypatch, tmp_path, enabled, ignore):
|
||||
from openpilot.selfdrive.pandad import panda_firmware as firmware
|
||||
monkeypatch.setattr(firmware, "FW_PATH", str(tmp_path), raising=False)
|
||||
params = VehicleParams(TeslaWakeOnCAN=not enabled, IgnoreIgnitionLine=ignore)
|
||||
before = dict(params.values)
|
||||
preflight = getattr(firmware, "validate_tesla_can_wake_firmware", None)
|
||||
assert callable(preflight), "firmware preflight must reject missing images before settings are saved"
|
||||
base = "tesla_wake" if enabled else ""
|
||||
suffix = "_".join(part for part in (base, "can_ignition_only" if ignore else "") if part)
|
||||
suffix = "_" + suffix if suffix else ""
|
||||
names = [f"panda{suffix}.bin.signed", f"panda_h7{suffix}.bin.signed"]
|
||||
for index, name in enumerate(names):
|
||||
with pytest.raises(RuntimeError, match="missing"):
|
||||
preflight(params, enabled)
|
||||
(tmp_path / name).write_bytes(b"firmware")
|
||||
preflight(params, enabled)
|
||||
assert params.values == before
|
||||
@@ -3580,6 +3580,20 @@
|
||||
"ui_type": "toggle",
|
||||
"settings_tier": "simple"
|
||||
},
|
||||
{
|
||||
"key": "TeslaWakeOnCAN",
|
||||
"label": "Wake Comma with Tesla",
|
||||
"description": "When enabled, your comma can wake up when your Tesla wakes up. When disabled, it normally powers on when you press the brake.",
|
||||
"picker_description": "Allow your comma to wake up with your Tesla.",
|
||||
"data_type": "bool",
|
||||
"ui_type": "toggle",
|
||||
"settings_tier": "simple",
|
||||
"vehicle_makes": [
|
||||
"Tesla"
|
||||
],
|
||||
"requires_capability": "TeslaCANWakeAvailable",
|
||||
"requires_offroad": true
|
||||
},
|
||||
{
|
||||
"key": "HKGRemoteStartBootsComma",
|
||||
"label": "Remote Start Boots Comma (HKG)",
|
||||
|
||||
@@ -22,6 +22,7 @@ from openpilot.common.realtime import DT_DMON, DT_HW
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.version import get_build_metadata
|
||||
from panda import Panda, FW_PATH
|
||||
from openpilot.selfdrive.pandad.panda_firmware import get_firmware_path, get_tesla_wake_on_can
|
||||
|
||||
from openpilot.starpilot.common.starpilot_variables import EARTH_RADIUS, STARPILOT_API, KONIK_PATH
|
||||
|
||||
@@ -198,21 +199,6 @@ def extract_zip(zip_file, extract_path):
|
||||
print(f"Extraction completed!")
|
||||
|
||||
|
||||
def get_selected_panda_firmware_name(app_fn, remote_start, hkg_remote_start, ignore_ignition_line):
|
||||
if not remote_start and not hkg_remote_start and not ignore_ignition_line:
|
||||
return app_fn
|
||||
|
||||
h7 = app_fn == "panda_h7.bin.signed"
|
||||
name_parts = ["panda_h7" if h7 else "panda"]
|
||||
if hkg_remote_start:
|
||||
name_parts.extend(["hkg", "remote"])
|
||||
elif remote_start:
|
||||
name_parts.append("remote")
|
||||
if ignore_ignition_line:
|
||||
name_parts.append("can_ignition_only")
|
||||
return "_".join(name_parts) + ".bin.signed"
|
||||
|
||||
|
||||
def flash_panda(params_memory):
|
||||
from openpilot.selfdrive.pandad.rivian_long_flasher import is_rivian_bridge_panda, is_rivian_vehicle
|
||||
|
||||
@@ -230,6 +216,8 @@ def flash_panda(params_memory):
|
||||
except Exception:
|
||||
ignore_ignition_line = False
|
||||
|
||||
tesla_wake = get_tesla_wake_on_can(params)
|
||||
|
||||
rivian = is_rivian_vehicle()
|
||||
usb_serials = set(Panda.usb_list())
|
||||
for serial in Panda.list():
|
||||
@@ -246,15 +234,8 @@ def flash_panda(params_memory):
|
||||
print(f"Skipping unverified external Black Panda on Rivian {serial}")
|
||||
continue
|
||||
print(f"Flashing Panda {serial}")
|
||||
flash_fn = None
|
||||
app_fn = panda.get_mcu_type().config.app_fn
|
||||
selected_fn = get_selected_panda_firmware_name(app_fn, remote_start, hkg_remote_start, ignore_ignition_line)
|
||||
if selected_fn != app_fn:
|
||||
candidate = os.path.join(FW_PATH, selected_fn)
|
||||
if os.path.isfile(candidate):
|
||||
flash_fn = candidate
|
||||
else:
|
||||
print(f"Selected panda firmware missing: {candidate}. Falling back to default firmware.")
|
||||
flash_fn = get_firmware_path(FW_PATH, app_fn, remote_start, hkg_remote_start, ignore_ignition_line, tesla_wake)
|
||||
panda.flash(fn=flash_fn)
|
||||
except Exception as exception:
|
||||
print(f"Failed to flash Panda {serial}: {exception}")
|
||||
|
||||
@@ -1599,6 +1599,10 @@ class StarPilotVariables:
|
||||
"TeslaCoopSteering",
|
||||
condition=toggle.car_make == "tesla" and toggle.car_model == TESLA_CAR.TESLA_MODEL_3,
|
||||
)
|
||||
toggle.tesla_wake_on_can = self.get_value(
|
||||
"TeslaWakeOnCAN",
|
||||
condition=toggle.car_make == "tesla" and toggle.car_model in {TESLA_CAR.TESLA_MODEL_3, TESLA_CAR.TESLA_MODEL_Y, TESLA_CAR.TESLA_MODEL_X},
|
||||
)
|
||||
toggle.rivian_angle_control = self.get_value("RivianAngleControl", condition=toggle.car_make == "rivian")
|
||||
|
||||
toggle.tethering_config = self.get_value("TetheringEnabled", cast=float)
|
||||
|
||||
@@ -62,8 +62,9 @@ export const api = {
|
||||
return res.ok ? parse(res) : {}
|
||||
},
|
||||
|
||||
updateParam({ key, value, label }) {
|
||||
updateParam({ key, value, label, confirmedPandaFirmwareFlash }) {
|
||||
const data = { key, value }
|
||||
if (confirmedPandaFirmwareFlash === true) data.confirmedPandaFirmwareFlash = true
|
||||
if (label) data.label = label
|
||||
return request("/api/params", { method: "PUT", data })
|
||||
},
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
import { FavoritesEditor } from "./FavoritesEditor.js"
|
||||
import { t } from "../i18n.js"
|
||||
|
||||
const PANDA_FIRMWARE_TOGGLE_KEYS = new Set(["IgnoreIgnitionLine", "RemoteStartBootsComma", "HKGRemoteStartBootsComma", "TeslaWakeOnCAN"])
|
||||
|
||||
export const GalaxyToggleCard = {
|
||||
name: "GalaxyToggleCard",
|
||||
components: { FavoritesEditor },
|
||||
@@ -76,12 +78,18 @@ export const GalaxyToggleCard = {
|
||||
rollback(prev) { this.$emit("change", { key: this.param.key, value: prev }) },
|
||||
async commit(nextValue) {
|
||||
if (this.locked || this.updating) return
|
||||
const firmwareToggle = PANDA_FIRMWARE_TOGGLE_KEYS.has(this.param.key)
|
||||
if (firmwareToggle && this.values.IsOnroad) return
|
||||
if (firmwareToggle && !window.confirm(`${this.param.label} requires a Panda firmware update and device reboot.\n\n${nextValue ? "Enable" : "Disable"} ${this.param.label} and flash the Panda now?`)) {
|
||||
this.rollback(this.value)
|
||||
return
|
||||
}
|
||||
const prev = this.value
|
||||
const label = this.lastLabel || ""
|
||||
this.$emit("change", { key: this.param.key, value: nextValue })
|
||||
this.updating = true
|
||||
try {
|
||||
const data = await api.updateParam({ key: this.param.key, value: nextValue, label })
|
||||
const data = await api.updateParam({ key: this.param.key, value: nextValue, label, ...(firmwareToggle ? { confirmedPandaFirmwareFlash: true } : {}) })
|
||||
const updated = data?.updated && typeof data.updated === "object" ? data.updated : {}
|
||||
if (Object.prototype.hasOwnProperty.call(updated, this.param.key)) {
|
||||
this.$emit("change", { key: this.param.key, value: updated[this.param.key], ...updated })
|
||||
@@ -94,9 +102,9 @@ export const GalaxyToggleCard = {
|
||||
this.updating = false
|
||||
}
|
||||
},
|
||||
onSwitch(e) {
|
||||
if (!this.locked) this.commit(!!e.target.checked)
|
||||
else e.target.checked = !!this.value
|
||||
async onSwitch(e) {
|
||||
if (!this.locked) await this.commit(!!e.target.checked)
|
||||
e.target.checked = !!this.value
|
||||
},
|
||||
onSelect(e) {
|
||||
if (this.locked) { e.target.value = String(this.value ?? "") ; return }
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from types import SimpleNamespace
|
||||
import pytest
|
||||
from test_navigation_params import _params_client, the_galaxy
|
||||
|
||||
|
||||
def client_for(monkeypatch, supported=True, onroad=False):
|
||||
client, params = _params_client(monkeypatch, {"IsOnroad": onroad, "TeslaWakeOnCAN": False}, "pc")
|
||||
monkeypatch.setattr(the_galaxy, "_get_param_type_info", lambda: ({"TeslaWakeOnCAN"}, {"TeslaWakeOnCAN": bool}))
|
||||
monkeypatch.setattr(the_galaxy, "supports_tesla_can_wake", lambda _: supported, raising=False)
|
||||
monkeypatch.setattr(the_galaxy, "update_starpilot_toggles", lambda: None)
|
||||
monkeypatch.setattr(the_galaxy, "validate_tesla_can_wake_firmware", lambda *_: None, raising=False)
|
||||
launches = []
|
||||
monkeypatch.setattr(the_galaxy.threading, "Thread", lambda **kw: SimpleNamespace(start=lambda: launches.append(kw["target"])))
|
||||
return client, params, launches
|
||||
|
||||
|
||||
@pytest.mark.parametrize("supported,onroad,confirmed,status", [
|
||||
(False, False, True, 403), (True, True, True, 403), (True, False, False, 409),
|
||||
])
|
||||
def test_tesla_firmware_rejects_unsupported_onroad_or_unconfirmed_write(monkeypatch, supported, onroad, confirmed, status):
|
||||
client, params, launches = client_for(monkeypatch, supported, onroad)
|
||||
response = client.put("/api/params", json={"key": "TeslaWakeOnCAN", "value": True, "confirmedPandaFirmwareFlash": confirmed})
|
||||
assert response.status_code == status
|
||||
assert params.values["TeslaWakeOnCAN"] is False
|
||||
assert not launches
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [False, True])
|
||||
def test_tesla_firmware_confirmed_offroad_write_launches_flash(monkeypatch, enabled):
|
||||
client, params, launches = client_for(monkeypatch)
|
||||
response = client.put("/api/params", json={"key": "TeslaWakeOnCAN", "value": enabled, "confirmedPandaFirmwareFlash": True})
|
||||
assert response.status_code == 200
|
||||
assert params.get_bool("TeslaWakeOnCAN") is enabled
|
||||
assert launches == [the_galaxy._flash_panda_then_reboot]
|
||||
|
||||
|
||||
def test_missing_tesla_firmware_does_not_save_or_flash(monkeypatch):
|
||||
client, params, launches = client_for(monkeypatch)
|
||||
def unavailable(*_):
|
||||
raise RuntimeError("Tesla firmware is missing")
|
||||
monkeypatch.setattr(the_galaxy, "validate_tesla_can_wake_firmware", unavailable)
|
||||
response = client.put("/api/params", json={"key": "TeslaWakeOnCAN", "value": True, "confirmedPandaFirmwareFlash": True})
|
||||
assert response.status_code == 409
|
||||
assert "missing" in response.get_json()["error"]
|
||||
assert params.values["TeslaWakeOnCAN"] is False
|
||||
assert not launches
|
||||
|
||||
|
||||
def test_tesla_wake_rejects_conflicting_remote_start(monkeypatch):
|
||||
client, params, launches = client_for(monkeypatch)
|
||||
params.values["RemoteStartBootsComma"] = True
|
||||
response = client.put("/api/params", json={"key": "TeslaWakeOnCAN", "value": True, "confirmedPandaFirmwareFlash": True})
|
||||
assert response.status_code == 409
|
||||
assert "remote-start" in response.get_json()["error"]
|
||||
assert params.values["TeslaWakeOnCAN"] is False
|
||||
assert not launches
|
||||
|
||||
|
||||
def test_remote_start_rejects_enabled_tesla_wake(monkeypatch):
|
||||
client, params, launches = client_for(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
the_galaxy,
|
||||
"_get_param_type_info",
|
||||
lambda: ({"TeslaWakeOnCAN", "RemoteStartBootsComma"}, {"TeslaWakeOnCAN": bool, "RemoteStartBootsComma": bool}),
|
||||
)
|
||||
params.values["TeslaWakeOnCAN"] = True
|
||||
response = client.put("/api/params", json={"key": "RemoteStartBootsComma", "value": True, "confirmedPandaFirmwareFlash": True})
|
||||
assert response.status_code == 409
|
||||
assert "remote-start" in response.get_json()["error"]
|
||||
assert params.values.get("RemoteStartBootsComma", False) is False
|
||||
assert not launches
|
||||
|
||||
|
||||
@pytest.mark.parametrize("supported", [False, True])
|
||||
def test_params_payload_reports_tesla_capability_and_off_default(monkeypatch, supported):
|
||||
client, params, _ = client_for(monkeypatch, supported=supported)
|
||||
params.values.pop("TeslaWakeOnCAN")
|
||||
monkeypatch.setattr(the_galaxy, "_get_default_param_values", lambda: {"TeslaWakeOnCAN": "0"})
|
||||
response = client.get("/api/params/all")
|
||||
assert response.status_code == 200
|
||||
assert response.get_json()["TeslaCANWakeAvailable"] is supported
|
||||
assert response.get_json()["TeslaWakeOnCAN"] is False
|
||||
@@ -0,0 +1,49 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const root = path.resolve(__dirname, '../../../..');
|
||||
const base = path.join(root, 'starpilot/system/the_galaxy/assets/mobile/js');
|
||||
const layout = JSON.parse(fs.readFileSync(path.join(root, 'starpilot/common/assets/device_settings_layout.json')));
|
||||
const section = layout.find(s => s.name === 'Vehicle');
|
||||
const param = section.params.find(p => p.key === 'TeslaWakeOnCAN');
|
||||
assert.ok(param, 'Wake on CAN must appear in Vehicle settings');
|
||||
const ctx = { console, FavoritesEditor: {}, window: { confirm: () => false }, api: {}, showSnackbar: () => {} };
|
||||
vm.createContext(ctx);
|
||||
function load(file, expose) {
|
||||
const src = fs.readFileSync(path.join(base, file), 'utf8').replace(/^import[\s\S]*?from [^\n]+\n/gm, '').replace(/export /g, '');
|
||||
vm.runInContext(src + '\n' + expose, ctx);
|
||||
}
|
||||
load('params.js', 'this.visible = isSettingVisible');
|
||||
for (const [make, supported, expected] of [['Tesla', true, true], ['Tesla', false, false], ['Toyota', true, false], ['', false, false]]) {
|
||||
assert.equal(ctx.visible(section, param, {CarMake:make, TeslaCANWakeAvailable:supported}), expected);
|
||||
}
|
||||
load('components/GalaxyToggleCard.js', 'this.card = GalaxyToggleCard');
|
||||
(async () => {
|
||||
const writes = [], changes = [];
|
||||
ctx.api.updateParam = async data => {writes.push(data); return {}};
|
||||
const card = {param, value:false, values:{IsOnroad:false}, locked:false, updating:false,
|
||||
$emit: (...args) => changes.push(args), rollback: () => {}};
|
||||
await ctx.card.methods.commit.call(card, true);
|
||||
assert.equal(writes.length, 0, 'cancelled firmware confirmation must not write');
|
||||
ctx.window.confirm = () => true;
|
||||
card.values.IsOnroad = true;
|
||||
await ctx.card.methods.commit.call(card, true);
|
||||
assert.equal(writes.length, 0, 'onroad firmware writes must be blocked');
|
||||
card.values.IsOnroad = false;
|
||||
await ctx.card.methods.commit.call(card, true);
|
||||
assert.equal(writes.length, 1);
|
||||
assert.equal(writes[0].confirmedPandaFirmwareFlash, true);
|
||||
assert.equal(writes[0].value, true);
|
||||
const switchEvent = {target:{checked:true}};
|
||||
const cancelledCard = {locked:false, value:false, commit: async () => {}};
|
||||
await ctx.card.methods.onSwitch.call(cancelledCard, switchEvent);
|
||||
assert.equal(switchEvent.target.checked, false, 'cancelled switch must restore its displayed state');
|
||||
const network = [];
|
||||
const apiCtx = {fetch:async (url, opts) => {network.push(JSON.parse(opts.body)); return {ok:true,json:async()=>({})}}};
|
||||
vm.createContext(apiCtx);
|
||||
vm.runInContext(fs.readFileSync(path.join(base,'api.js'),'utf8').replace(/export /g,'') + '\nthis.client=api', apiCtx);
|
||||
await apiCtx.client.updateParam({key:'TeslaWakeOnCAN',value:true,confirmedPandaFirmwareFlash:true});
|
||||
assert.equal(network[0].confirmedPandaFirmwareFlash, true);
|
||||
console.log('Tesla New Galaxy visibility, cancellation, offroad and confirmation passed');
|
||||
})().catch(e => {console.error(e); process.exitCode=1});
|
||||
@@ -46,6 +46,7 @@ from openpilot.common.params import ParamKeyFlag, ParamKeyType, Params
|
||||
from openpilot.common.realtime import DT_HW
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.selfdrive.pandad.panda_firmware import firmware_flags_conflict, supports_tesla_can_wake, validate_tesla_can_wake_firmware
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_VALUE, PRESERVE_COUNT
|
||||
@@ -268,7 +269,7 @@ _TESTING_GROUND_CUSTOM_RESERVED_INTERVAL_S = 15.0
|
||||
_TESTING_GROUND_CUSTOM_RESERVED_PM = None
|
||||
_TESTING_GROUND_CUSTOM_RESERVED_LOCK = threading.Lock()
|
||||
_TESTING_GROUND_CUSTOM_RESERVED_LAST_PUBLISH_MONO = 0.0
|
||||
PANDA_FIRMWARE_TOGGLE_KEYS = {"IgnoreIgnitionLine", "RemoteStartBootsComma", "HKGRemoteStartBootsComma"}
|
||||
PANDA_FIRMWARE_TOGGLE_KEYS = {"IgnoreIgnitionLine", "RemoteStartBootsComma", "HKGRemoteStartBootsComma", "TeslaWakeOnCAN"}
|
||||
PANDA_FIRMWARE_CONFIRMATION_FIELD = "confirmedPandaFirmwareFlash"
|
||||
_PANDA_FLASH_REBOOT_LOCK = threading.Lock()
|
||||
|
||||
@@ -6223,11 +6224,24 @@ def setup(app):
|
||||
if params.get_bool("IsOnroad"):
|
||||
return jsonify({"error": "Cannot change PiP Side Camera configuration while driving."}), 403
|
||||
|
||||
if key == "TeslaWakeOnCAN" and not supports_tesla_can_wake(params):
|
||||
return jsonify({"error": "Wake on CAN is available only for a detected Tesla Model 3, Y or X."}), 403
|
||||
|
||||
if key in PANDA_FIRMWARE_TOGGLE_KEYS and params.get_bool("IsOnroad"):
|
||||
return jsonify({"error": "Cannot flash Panda firmware while driving."}), 403
|
||||
if key in PANDA_FIRMWARE_TOGGLE_KEYS and data.get(PANDA_FIRMWARE_CONFIRMATION_FIELD) is not True:
|
||||
return jsonify({"error": "Panda firmware changes require confirmation before flashing."}), 409
|
||||
|
||||
enabled = str_val.strip() in ("1", "true", "True")
|
||||
if key in PANDA_FIRMWARE_TOGGLE_KEYS and firmware_flags_conflict(params, key, enabled):
|
||||
return jsonify({"error": "Tesla wake cannot be combined with remote-start firmware."}), 409
|
||||
|
||||
if key == "TeslaWakeOnCAN":
|
||||
try:
|
||||
validate_tesla_can_wake_firmware(params, enabled)
|
||||
except RuntimeError as exc:
|
||||
return jsonify({"error": str(exc)}), 409
|
||||
|
||||
if key in {"LeadIndicator", "HideLeadMarker"}:
|
||||
enabled = str_val.strip() in ("1", "true", "True")
|
||||
if key == "LeadIndicator":
|
||||
@@ -6568,6 +6582,7 @@ def setup(app):
|
||||
except Exception:
|
||||
result[key] = None
|
||||
|
||||
result["TeslaCANWakeAvailable"] = supports_tesla_can_wake(params)
|
||||
result["HasRadar"] = _get_has_radar()
|
||||
result["VehicleParked"] = _get_vehicle_parked()
|
||||
result["AlphaLongitudinalAvailable"] = _get_alpha_longitudinal_available()
|
||||
|
||||
Reference in New Issue
Block a user