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:
AngusBell97
2026-09-10 13:48:42 -05:00
committed by firestar5683
parent 01dd14bfae
commit 24b8789c79
44 changed files with 1268 additions and 69 deletions
+4
View File
@@ -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.
+1
View File
@@ -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.
+5
View File
@@ -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",
+4 -3
View File
@@ -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) {
+1 -1
View File
@@ -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);
+20 -1
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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.
+1 -1
View File
@@ -1 +1 @@
DEV-244aa673-DEBUG
DEV-9c92f717-DEBUG
+83
View File
@@ -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 06 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).
+70
View File
@@ -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++; }
+76
View File
@@ -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()
+135
View File
@@ -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()
+285
View File
@@ -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;
}
+100
View File
@@ -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
View File
@@ -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)",
+4 -23
View File
@@ -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}")
+4
View File
@@ -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});
+16 -1
View File
@@ -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()