mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
Native Safety
Extend native safety and health contracts with explicit AOL state and bounded message handling. Keep vehicle-specific admissions outside the generic layer. Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
This commit is contained in:
@@ -8,3 +8,5 @@ class ALTERNATIVE_EXPERIENCE:
|
||||
DISABLE_STOCK_AEB = 2
|
||||
RAISE_LONGITUDINAL_LIMITS_TO_ISO_MAX = 8
|
||||
ALLOW_AEB = 16
|
||||
TOYOTA_AUTO_HOLD = 128
|
||||
TOYOTA_AEB_HOLD = 256
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "opendbc/safety/can.h"
|
||||
|
||||
void can_set_checksum(CANPacket_t *packet);
|
||||
void can_send(CANPacket_t *to_push, uint8_t bus_number, bool skip_tx_hook);
|
||||
@@ -35,7 +35,9 @@
|
||||
#define SAFETY_RIVIAN 33U
|
||||
#define SAFETY_VOLKSWAGEN_MEB 34U
|
||||
#define SAFETY_BYD 35U
|
||||
#define SAFETY_VOLVO 36U
|
||||
#define SAFETY_MG 38U
|
||||
#define SAFETY_TESLA_PREAP 39U
|
||||
|
||||
#define GET_BIT(msg, b) ((bool)!!(((msg)->data[((b) / 8U)] >> ((b) % 8U)) & 0x1U))
|
||||
#define GET_FLAG(value, mask) (((value) & (mask)) == (mask))
|
||||
@@ -92,6 +94,7 @@ typedef struct {
|
||||
int len;
|
||||
bool check_relay; // if true, trigger relay malfunction if existence on destination bus and block forwarding to destination bus
|
||||
bool disable_static_blocking; // if true, static blocking is disabled so safety mode can dynamically handle it (e.g. selective AEB pass-through)
|
||||
bool check_relay_immediately; // if true, skip the usual transition grace for this exact TX address and bus
|
||||
} CanMsg;
|
||||
|
||||
typedef enum {
|
||||
@@ -175,14 +178,14 @@ typedef struct {
|
||||
} LongitudinalLimits;
|
||||
|
||||
typedef struct {
|
||||
const int addr;
|
||||
const unsigned int bus;
|
||||
const int len;
|
||||
const uint32_t frequency; // expected frequency of the message [Hz]
|
||||
const bool ignore_checksum; // checksum check is not performed when set to true
|
||||
const bool ignore_counter; // counter check is not performed when set to true
|
||||
const uint8_t max_counter; // maximum value of the counter. 0 means that the counter check is skipped
|
||||
const bool ignore_quality_flag; // true if quality flag check is skipped
|
||||
int addr;
|
||||
unsigned int bus;
|
||||
int len;
|
||||
uint32_t frequency; // expected frequency of the message [Hz]
|
||||
bool ignore_checksum; // checksum check is not performed when set to true
|
||||
bool ignore_counter; // counter check is not performed when set to true
|
||||
uint8_t max_counter; // maximum value of the counter. 0 means that the counter check is skipped
|
||||
bool ignore_quality_flag; // true if quality flag check is skipped
|
||||
} CanMsgCheck;
|
||||
|
||||
typedef struct {
|
||||
@@ -199,7 +202,7 @@ typedef struct {
|
||||
|
||||
// params and flags about checksum, counter and frequency checks for each monitored address
|
||||
typedef struct {
|
||||
const CanMsgCheck msg[MAX_ADDR_CHECK_MSGS]; // check either messages (e.g. honda steer)
|
||||
CanMsgCheck msg[MAX_ADDR_CHECK_MSGS]; // check either messages (e.g. honda steer)
|
||||
RxStatus status;
|
||||
} RxCheck;
|
||||
|
||||
@@ -224,6 +227,7 @@ typedef bool (*fwd_hook)(int bus_num, int addr); // returns true if the mes
|
||||
typedef struct {
|
||||
safety_hook_init init;
|
||||
rx_hook rx;
|
||||
rx_hook optional_rx;
|
||||
tx_hook tx;
|
||||
fwd_hook fwd;
|
||||
get_checksum_t get_checksum;
|
||||
@@ -256,6 +260,25 @@ void safety_tick(void);
|
||||
|
||||
// This can be set by the safety hooks
|
||||
extern bool controls_allowed;
|
||||
// Vehicle modes register a policy only for their qualified AOL safety profile.
|
||||
typedef struct {
|
||||
void (*reset)(void);
|
||||
void (*host_request)(uint8_t axis_mask);
|
||||
uint8_t (*request_mask)(void);
|
||||
uint8_t (*permission_mask)(void);
|
||||
void (*rx_invalid)(void);
|
||||
} AolSafetyPolicy;
|
||||
extern const AolSafetyPolicy *aol_policy;
|
||||
extern uint8_t aol_host_axis_mask;
|
||||
extern uint32_t aol_host_request_ts;
|
||||
#define AOL_HOST_REQUEST_TIMEOUT_US 300000U
|
||||
bool aol_rx_healthy(void);
|
||||
bool lateral_controls_allowed(void);
|
||||
bool longitudinal_controls_allowed(void);
|
||||
void aol_set_host_request(uint8_t axis_mask);
|
||||
uint8_t aol_get_permission_mask(void);
|
||||
extern bool aol_optional_release_capable;
|
||||
uint8_t aol_get_request_mask(void);
|
||||
extern bool relay_malfunction;
|
||||
extern bool gas_pressed;
|
||||
extern bool gas_pressed_prev;
|
||||
@@ -321,6 +344,8 @@ extern CurvatureSteeringState curvature_state;
|
||||
|
||||
// This flag allows AEB to be commanded from openpilot.
|
||||
#define ALT_EXP_ALLOW_AEB 16
|
||||
#define ALT_EXP_TOYOTA_AUTO_HOLD 128
|
||||
#define ALT_EXP_TOYOTA_AEB_HOLD 256
|
||||
|
||||
extern int alternative_experience;
|
||||
|
||||
@@ -357,6 +382,8 @@ extern const safety_hooks nissan_hooks;
|
||||
extern const safety_hooks subaru_hooks;
|
||||
extern const safety_hooks subaru_preglobal_hooks;
|
||||
extern const safety_hooks tesla_hooks;
|
||||
extern const safety_hooks tesla_hw1_hooks;
|
||||
extern const safety_hooks tesla_preap_hooks;
|
||||
extern const safety_hooks toyota_hooks;
|
||||
extern const safety_hooks volkswagen_mlb_hooks;
|
||||
extern const safety_hooks volkswagen_mqb_hooks;
|
||||
@@ -366,3 +393,5 @@ extern const safety_hooks rivian_hooks;
|
||||
extern const safety_hooks psa_hooks;
|
||||
extern const safety_hooks byd_hooks;
|
||||
extern const safety_hooks mg_hooks;
|
||||
|
||||
extern const safety_hooks volvo_c1_hooks;
|
||||
|
||||
@@ -61,7 +61,7 @@ bool steer_torque_cmd_checks(int desired_torque, int steer_req, const TorqueStee
|
||||
bool violation = false;
|
||||
uint32_t ts = microsecond_timer_get();
|
||||
|
||||
if (controls_allowed) {
|
||||
if (lateral_controls_allowed()) {
|
||||
// Some safety models support variable torque limit based on vehicle speed
|
||||
int max_torque = limits.max_torque;
|
||||
if (limits.dynamic_max_torque) {
|
||||
@@ -96,7 +96,7 @@ bool steer_torque_cmd_checks(int desired_torque, int steer_req, const TorqueStee
|
||||
}
|
||||
|
||||
bool torque_requested = desired_torque != 0;
|
||||
if (!controls_allowed && torque_requested) {
|
||||
if (!lateral_controls_allowed() && torque_requested) {
|
||||
violation = true;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ bool steer_torque_cmd_checks(int desired_torque, int steer_req, const TorqueStee
|
||||
}
|
||||
|
||||
// reset to 0 if either controls is not allowed or there's a violation
|
||||
if (violation || !controls_allowed) {
|
||||
if (violation || !lateral_controls_allowed()) {
|
||||
valid_steer_req_count = 0;
|
||||
invalid_steer_req_count = 0;
|
||||
desired_torque_last = 0;
|
||||
@@ -204,7 +204,7 @@ static bool steer_angle_cmd_inactive_check(int desired_angle, int max_angle) {
|
||||
bool steer_angle_cmd_checks(int desired_angle, bool steer_control_enabled, const AngleSteeringLimits limits) {
|
||||
bool violation = false;
|
||||
|
||||
if (controls_allowed && steer_control_enabled) {
|
||||
if (lateral_controls_allowed() && steer_control_enabled) {
|
||||
// convert floating point angle rate limits to integers in the scale of the desired angle on CAN,
|
||||
// add 1 to not false trigger the violation. also fudge the speed by 1 m/s so rate limits are
|
||||
// always slightly above openpilot's in case we read an updated speed in between angle commands
|
||||
@@ -229,12 +229,12 @@ bool steer_angle_cmd_checks(int desired_angle, bool steer_control_enabled, const
|
||||
}
|
||||
|
||||
// No angle control allowed when controls are not allowed
|
||||
if (!controls_allowed) {
|
||||
if (!lateral_controls_allowed()) {
|
||||
violation |= steer_control_enabled;
|
||||
}
|
||||
|
||||
// reset to current angle if either controls is not allowed or there's a violation
|
||||
if (violation || !controls_allowed) {
|
||||
if (violation || !lateral_controls_allowed()) {
|
||||
desired_angle_last = SAFETY_CLAMP(angle_meas.values[0], -limits.max_angle, limits.max_angle);
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ bool steer_curvature_cmd_checks(int desired_curvature, int steer_power, bool ste
|
||||
|
||||
speed_mismatch_check((float)vehicle_speed_2.values[0] / VEHICLE_SPEED_FACTOR);
|
||||
|
||||
if (controls_allowed && steer_control_enabled) {
|
||||
if (lateral_controls_allowed() && steer_control_enabled) {
|
||||
// *** absolute curvature cap ***
|
||||
violation |= safety_max_limit_check(desired_curvature, limits.max_curvature, -limits.max_curvature);
|
||||
|
||||
@@ -322,11 +322,11 @@ bool steer_curvature_cmd_checks(int desired_curvature, int steer_power, bool ste
|
||||
if (limits.max_steer_power != 0) {
|
||||
violation |= safety_max_limit_check(steer_power, limits.max_steer_power, 0);
|
||||
violation |= (steer_power != 0) && !steer_control_enabled;
|
||||
violation |= !controls_allowed && (steer_power != 0) && (steer_power >= curvature_state.steer_power_last);
|
||||
violation |= !lateral_controls_allowed() && (steer_power != 0) && (steer_power >= curvature_state.steer_power_last);
|
||||
curvature_state.steer_power_last = steer_power;
|
||||
} else {
|
||||
// No curvature control allowed when controls are not allowed
|
||||
violation |= !controls_allowed && steer_control_enabled;
|
||||
violation |= !lateral_controls_allowed() && steer_control_enabled;
|
||||
}
|
||||
|
||||
if (violation) {
|
||||
@@ -362,7 +362,7 @@ bool steer_angle_cmd_checks_vm(int desired_angle, bool steer_control_enabled, co
|
||||
|
||||
bool violation = false;
|
||||
|
||||
if (controls_allowed && steer_control_enabled) {
|
||||
if (lateral_controls_allowed() && steer_control_enabled) {
|
||||
// *** ISO lateral jerk limit ***
|
||||
// calculate maximum angle rate per second
|
||||
const float max_curvature_rate_sec = MAX_LATERAL_JERK / (fudged_speed * fudged_speed);
|
||||
@@ -396,12 +396,12 @@ bool steer_angle_cmd_checks_vm(int desired_angle, bool steer_control_enabled, co
|
||||
}
|
||||
|
||||
// No angle control allowed when controls are not allowed
|
||||
if (!controls_allowed) {
|
||||
if (!lateral_controls_allowed()) {
|
||||
violation |= steer_control_enabled;
|
||||
}
|
||||
|
||||
// reset to current angle if either controls is not allowed or there's a violation
|
||||
if (violation || !controls_allowed) {
|
||||
if (violation || !lateral_controls_allowed()) {
|
||||
desired_angle_last = SAFETY_CLAMP(angle_meas.values[0], -limits.max_angle, limits.max_angle);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "opendbc/safety/declarations.h"
|
||||
|
||||
bool get_longitudinal_allowed(void) {
|
||||
return controls_allowed && !gas_pressed_prev;
|
||||
return longitudinal_controls_allowed() && !gas_pressed_prev;
|
||||
}
|
||||
|
||||
// Safety checks for longitudinal actuation
|
||||
|
||||
@@ -165,7 +165,7 @@ static safety_config rivian_init(uint16_t param) {
|
||||
|
||||
SAFETY_UNUSED(param);
|
||||
#ifdef ALLOW_DEBUG
|
||||
const int FLAG_RIVIAN_LONG_CONTROL = 1;
|
||||
const uint16_t FLAG_RIVIAN_LONG_CONTROL = 1U;
|
||||
rivian_longitudinal = GET_FLAG(param, FLAG_RIVIAN_LONG_CONTROL);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -55,6 +55,67 @@ const int MAX_WRONG_COUNTERS = 5;
|
||||
|
||||
// This can be set by the safety hooks
|
||||
bool controls_allowed = false;
|
||||
const AolSafetyPolicy *aol_policy = NULL;
|
||||
bool aol_optional_release_capable = false;
|
||||
uint8_t aol_host_axis_mask = 0U;
|
||||
uint32_t aol_host_request_ts = 0U;
|
||||
void aol_set_host_request(uint8_t axis_mask) {
|
||||
if (aol_policy != NULL) {
|
||||
aol_host_axis_mask = axis_mask & 0x3U;
|
||||
aol_host_request_ts = microsecond_timer_get();
|
||||
if (aol_policy->host_request != NULL) {
|
||||
aol_policy->host_request(aol_host_axis_mask | (aol_optional_release_capable ? (axis_mask & 0x4U) : 0U));
|
||||
}
|
||||
} else {
|
||||
aol_host_axis_mask = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t aol_get_request_mask(void) {
|
||||
return ((aol_policy != NULL) && (aol_policy->request_mask != NULL)) ? aol_policy->request_mask() : 0U;
|
||||
}
|
||||
|
||||
bool aol_rx_healthy(void) {
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
bool healthy = (current_safety_config.rx_checks_len > 0) && (current_safety_config.rx_checks != NULL);
|
||||
for (int i = 0; healthy && (i < current_safety_config.rx_checks_len); i++) {
|
||||
const RxCheck *check = ¤t_safety_config.rx_checks[i];
|
||||
if (!check->status.msg_seen || check->status.lagging || !check->status.valid_checksum ||
|
||||
!check->status.valid_quality_flag || (check->status.wrong_counters >= MAX_WRONG_COUNTERS)) {
|
||||
healthy = false;
|
||||
} else {
|
||||
const uint32_t frequency = check->msg[check->status.index].frequency;
|
||||
if (frequency < 10U) {
|
||||
healthy = false;
|
||||
} else {
|
||||
const uint32_t expected_age = 10000000U / frequency;
|
||||
const uint32_t max_age = (expected_age > 100000U) ? expected_age : 100000U;
|
||||
healthy = safety_get_ts_elapsed(now, check->status.last_timestamp) <= max_age;
|
||||
}
|
||||
}
|
||||
}
|
||||
return healthy;
|
||||
}
|
||||
|
||||
uint8_t aol_get_permission_mask(void) {
|
||||
return ((aol_policy != NULL) && (aol_policy->permission_mask != NULL)) ? aol_policy->permission_mask() : 0U;
|
||||
}
|
||||
|
||||
bool lateral_controls_allowed(void) {
|
||||
bool allowed = controls_allowed;
|
||||
if (aol_policy != NULL) {
|
||||
allowed = (aol_get_permission_mask() & 0x1U) != 0U;
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
|
||||
bool longitudinal_controls_allowed(void) {
|
||||
bool allowed = controls_allowed;
|
||||
if (aol_policy != NULL) {
|
||||
allowed = (aol_get_permission_mask() & 0x2U) != 0U;
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
bool relay_malfunction = false;
|
||||
bool gas_pressed = false;
|
||||
bool gas_pressed_prev = false;
|
||||
@@ -107,7 +168,7 @@ static const safety_hooks *current_hooks = &nooutput_hooks;
|
||||
safety_config current_safety_config;
|
||||
|
||||
static void generic_rx_checks(void);
|
||||
static void stock_ecu_check(bool stock_ecu_detected);
|
||||
static void stock_ecu_check(bool stock_ecu_detected, bool immediate);
|
||||
|
||||
static bool is_msg_valid(RxCheck addr_list[], int index) {
|
||||
bool valid = true;
|
||||
@@ -115,6 +176,10 @@ static bool is_msg_valid(RxCheck addr_list[], int index) {
|
||||
if (!addr_list[index].status.valid_checksum || !addr_list[index].status.valid_quality_flag || (addr_list[index].status.wrong_counters >= MAX_WRONG_COUNTERS)) {
|
||||
valid = false;
|
||||
controls_allowed = false;
|
||||
aol_host_axis_mask = 0U;
|
||||
if ((aol_policy != NULL) && (aol_policy->rx_invalid != NULL)) {
|
||||
aol_policy->rx_invalid();
|
||||
}
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
@@ -204,6 +269,11 @@ bool safety_rx_hook(const CANPacket_t *msg) {
|
||||
if (valid && whitelisted) {
|
||||
current_hooks->rx(msg);
|
||||
}
|
||||
if (!whitelisted && (current_hooks->optional_rx != NULL)) {
|
||||
// Optional ingress is interpreted by the exact mode-owned callback. It
|
||||
// cannot satisfy or weaken the required control RX health checks above.
|
||||
current_hooks->optional_rx(msg);
|
||||
}
|
||||
|
||||
// Handles gas, brake, and regen paddle
|
||||
generic_rx_checks();
|
||||
@@ -215,7 +285,7 @@ bool safety_rx_hook(const CANPacket_t *msg) {
|
||||
for (int i = 0; i < current_safety_config.tx_msgs_len; i++) {
|
||||
const CanMsg *m = ¤t_safety_config.tx_msgs[i];
|
||||
if (m->check_relay) {
|
||||
stock_ecu_check((m->addr == addr) && (m->bus == msg->bus));
|
||||
stock_ecu_check((m->addr == addr) && (m->bus == msg->bus), m->check_relay_immediately);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,6 +407,7 @@ void safety_tick(void) {
|
||||
if (lagging || frequency_invalid || !is_msg_valid(current_safety_config.rx_checks, i)) {
|
||||
rx_checks_invalid = true;
|
||||
controls_allowed = false;
|
||||
aol_host_axis_mask = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,12 +440,12 @@ static void generic_rx_checks(void) {
|
||||
steering_disengage_prev = steering_disengage;
|
||||
}
|
||||
|
||||
static void stock_ecu_check(bool stock_ecu_detected) {
|
||||
static void stock_ecu_check(bool stock_ecu_detected, bool immediate) {
|
||||
// allow 1s of transition timeout after relay changes state before assessing malfunctioning
|
||||
const uint32_t RELAY_TRNS_TIMEOUT = 1U;
|
||||
|
||||
// check if stock ECU is on bus broken by car harness
|
||||
if ((safety_mode_cnt > RELAY_TRNS_TIMEOUT) && stock_ecu_detected) {
|
||||
if ((immediate || (safety_mode_cnt > RELAY_TRNS_TIMEOUT)) && stock_ecu_detected) {
|
||||
relay_malfunction_set();
|
||||
}
|
||||
}
|
||||
@@ -456,6 +527,13 @@ int set_safety_hooks(uint16_t mode, uint16_t param) {
|
||||
reset_sample(&angle_meas);
|
||||
|
||||
controls_allowed = false;
|
||||
if ((aol_policy != NULL) && (aol_policy->reset != NULL)) {
|
||||
aol_policy->reset();
|
||||
}
|
||||
aol_policy = NULL;
|
||||
aol_optional_release_capable = false;
|
||||
aol_host_axis_mask = 0U;
|
||||
aol_host_request_ts = 0U;
|
||||
relay_malfunction_reset();
|
||||
safety_rx_checks_invalid = false;
|
||||
|
||||
@@ -465,6 +543,12 @@ int set_safety_hooks(uint16_t mode, uint16_t param) {
|
||||
int hook_config_count = sizeof(safety_hook_registry) / sizeof(safety_hook_config);
|
||||
for (int i = 0; i < hook_config_count; i++) {
|
||||
if (safety_hook_registry[i].id == mode) {
|
||||
if ((mode == SAFETY_TESLA) && ((param & ~1U) != 0U)) {
|
||||
current_hooks = &nooutput_hooks;
|
||||
current_safety_mode = SAFETY_NOOUTPUT;
|
||||
current_safety_param = 0;
|
||||
continue;
|
||||
}
|
||||
current_hooks = safety_hook_registry[i].hooks;
|
||||
current_safety_mode = mode;
|
||||
current_safety_param = param;
|
||||
|
||||
@@ -984,16 +984,14 @@ class SafetyTest(SafetyTestBase):
|
||||
continue
|
||||
if attr.startswith('TestHyundaiCanfd') and current_test.startswith('TestHyundaiCanfd'):
|
||||
continue
|
||||
if {attr, current_test}.issubset({'TestHyundaiLongitudinalSafety', 'TestHyundaiLongitudinalSafetyCameraSCC', 'TestHyundaiSafetyFCEVLong'}):
|
||||
if {attr, current_test}.issubset({'TestHyundaiLongitudinalSafety', 'TestHyundaiLongitudinalSafetyCameraSCC',
|
||||
'TestHyundaiLongitudinalSafetyCameraSCCRefresh',
|
||||
'TestHyundaiLongitudinalSafetyCameraSCCRefreshHybrid', 'TestHyundaiSafetyFCEVLong'}):
|
||||
continue
|
||||
volkswagen_shared = ('TestVolkswagenMqb', 'TestVolkswagenMlb', 'TestVolkswagenMeb')
|
||||
if attr.startswith(volkswagen_shared) and current_test.startswith(volkswagen_shared):
|
||||
continue
|
||||
|
||||
# overlapping TX addrs, but they're not actuating messages for either car
|
||||
if attr == 'TestHyundaiCanfdLKASteeringLongEV' and current_test.startswith('TestToyota'):
|
||||
tx = list(filter(lambda m: m[0] not in [0x160, ], tx))
|
||||
|
||||
# Volkswagen MQB longitudinal actuating message overlaps with the Subaru lateral actuating message
|
||||
if attr == 'TestVolkswagenMqbLongSafety' and current_test.startswith('TestSubaru'):
|
||||
tx = list(filter(lambda m: m[0] not in [0x122, ], tx))
|
||||
@@ -1014,14 +1012,32 @@ class SafetyTest(SafetyTestBase):
|
||||
if attr.startswith('TestHyundaiLongitudinal'):
|
||||
# exceptions for common msgs across different Hyundai CAN platforms
|
||||
tx = list(filter(lambda m: m[0] not in [0x420, 0x50A, 0x389, 0x4A2], tx))
|
||||
classic_hyundai = ('TestHyundaiSafety', 'TestHyundaiLongitudinalSafety', 'TestHyundaiLegacySafety')
|
||||
refresh_hyundai = {'TestHyundaiSafetyCameraSCCRefresh', 'TestHyundaiSafetyCameraSCCRefreshHybrid',
|
||||
'TestHyundaiLongitudinalSafetyCameraSCCRefresh', 'TestHyundaiLongitudinalSafetyCameraSCCRefreshHybrid'}
|
||||
if current_test in refresh_hyundai and attr.startswith(classic_hyundai):
|
||||
# Classic Hyundai modes share 0x485 on bus 0 for LFA/HDA display. The
|
||||
# generic cross-mode probe fabricates eight bytes, which the refresh
|
||||
# variant accepts; exact 4/8-byte and bus checks live in Hyundai tests.
|
||||
tx = list(filter(lambda m: m[:2] != [0x485, 0], tx))
|
||||
all_tx.append([[m[0], m[1], attr] for m in tx])
|
||||
|
||||
# make sure we got all the msgs
|
||||
self.assertTrue(len(all_tx) >= len(test_files)-1)
|
||||
# Independent protocol/whole-caller test files need not declare a SafetyTest
|
||||
# TX table. Count the actual selected cross-mode tables, not Python files;
|
||||
# every selected address below must still be rejected by this safety mode.
|
||||
self.assertTrue(any(all_tx), "No cross-mode safety TX probes were discovered")
|
||||
|
||||
for tx_msgs in all_tx:
|
||||
for addr, bus, test_name in tx_msgs:
|
||||
msg = make_msg(bus, addr)
|
||||
# CCNC display frames are 32 bytes. An eight-byte probe aliases the
|
||||
# unrelated Subaru 0x161 command and falsely reports cross-mode TX.
|
||||
size = 32 if test_name.startswith('TestHyundaiCanfdCCNC') and bus == 0 and addr in (0x161, 0x162) else 8
|
||||
# HDA2 LONG's ADRV warning is 16 bytes. An eight-byte probe instead
|
||||
# constructs Toyota's legitimate 0x160/bus1 display frame. Keep the
|
||||
# foreign command in this test with its actual shape, for both layouts.
|
||||
if test_name in ('TestHyundaiCanfdLKASteeringLongEV', 'TestHyundaiCanfdLKASteeringAltLongEV') and (addr, bus) == (0x160, 1):
|
||||
size = 16
|
||||
msg = make_msg(bus, addr, size)
|
||||
self.safety.set_controls_allowed(1)
|
||||
# TODO: this should be blocked
|
||||
if current_test in ["TestNissanSafety", "TestNissanSafetyAltEpsBus", "TestNissanLeafSafety"] and [addr, bus] in self.TX_MSGS:
|
||||
|
||||
@@ -58,19 +58,46 @@ ffi.cdef(cdef_from_file(safety_dir / "can.h"), packed=True)
|
||||
for path in (safety_dir / "declarations.h", safety_dir / "ignition.h", Path(libsafety_dir) / "safety.c"):
|
||||
ffi.cdef(cdef_from_file(path))
|
||||
|
||||
def _can_packet_alignment() -> int:
|
||||
source = (safety_dir / "can.h").read_text()
|
||||
match = re.search(r"__attribute__\(\(\s*packed\s*,\s*aligned\((\d+)\)\s*\)\)\s*CANPacket_t\s*;", source)
|
||||
if match is None or int(match[1]) != ffi.alignof("unsigned int"):
|
||||
raise RuntimeError("Unsupported native CANPacket_t alignment")
|
||||
return int(match[1])
|
||||
|
||||
|
||||
CAN_PACKET_ALIGNMENT = _can_packet_alignment()
|
||||
|
||||
|
||||
def _allocate_can_packet(size: int):
|
||||
# CFFI keeps the packed field layout but cannot express packed + aligned.
|
||||
# Its allocator retains this owning buffer until the packet is released.
|
||||
words = (size + CAN_PACKET_ALIGNMENT - 1) // CAN_PACKET_ALIGNMENT
|
||||
return ffi.new("unsigned int[]", words)
|
||||
|
||||
|
||||
_can_packet_allocator = ffi.new_allocator(alloc=_allocate_can_packet)
|
||||
|
||||
|
||||
def new_CANPacket():
|
||||
"""Allocate native-sized/aligned storage while preserving packed field offsets."""
|
||||
return _can_packet_allocator("CANPacket_t *")
|
||||
|
||||
|
||||
class CANPacket:
|
||||
pass
|
||||
|
||||
# This function is generated by mutation.py rather than defined in the sources above.
|
||||
ffi.cdef("void mutation_set_active_mutant(int id);")
|
||||
ffi.cdef("void mutation_set_active_mutant(int id); int mutation_get_active_mutant(void);")
|
||||
|
||||
class LibSafety:
|
||||
pass
|
||||
libsafety: LibSafety
|
||||
libpath: str
|
||||
|
||||
def load(path):
|
||||
global libsafety
|
||||
global libsafety, libpath
|
||||
libsafety = ffi.dlopen(str(path))
|
||||
libpath = str(Path(path).resolve())
|
||||
|
||||
def __getattr__(name):
|
||||
if name == "libsafety":
|
||||
@@ -79,7 +106,7 @@ def __getattr__(name):
|
||||
raise AttributeError(name)
|
||||
|
||||
def make_CANPacket(addr: int, bus: int, dat):
|
||||
ret = ffi.new('CANPacket_t *')
|
||||
ret = new_CANPacket()
|
||||
ret[0].extended = 1 if addr >= 0x800 else 0
|
||||
ret[0].addr = addr
|
||||
ret[0].data_len_code = LEN_TO_DLC[len(dat)]
|
||||
|
||||
@@ -13,6 +13,41 @@ uint32_t microsecond_timer_get(void) {
|
||||
#include "opendbc/safety/safety.h"
|
||||
#include "opendbc/safety/ignition.h"
|
||||
|
||||
// Capture checked internally scheduled CAN frames for native safety tests.
|
||||
#define RECORDED_CAN_CAPACITY 32
|
||||
static CANPacket_t recorded_can[RECORDED_CAN_CAPACITY];
|
||||
static unsigned int recorded_can_count = 0U;
|
||||
|
||||
void reset_recorded_can(void) {
|
||||
recorded_can_count = 0U;
|
||||
}
|
||||
|
||||
unsigned int get_recorded_can_count(void) {
|
||||
return recorded_can_count;
|
||||
}
|
||||
|
||||
bool get_recorded_can(unsigned int index, CANPacket_t *out) {
|
||||
if (index >= recorded_can_count || out == NULL) {
|
||||
return false;
|
||||
}
|
||||
*out = recorded_can[index];
|
||||
return true;
|
||||
}
|
||||
|
||||
void can_set_checksum(CANPacket_t *packet) {
|
||||
// This test harness inspects the CAN payload and exercises the TX hook;
|
||||
// Panda packet framing checksums are added by the device transport.
|
||||
packet->checksum = 0U;
|
||||
}
|
||||
|
||||
void can_send(CANPacket_t *to_push, uint8_t bus_number, bool skip_tx_hook) {
|
||||
CANPacket_t packet = *to_push;
|
||||
packet.bus = bus_number;
|
||||
if ((skip_tx_hook || safety_tx_hook(&packet)) && recorded_can_count < RECORDED_CAN_CAPACITY) {
|
||||
recorded_can[recorded_can_count++] = packet;
|
||||
}
|
||||
}
|
||||
|
||||
bool safety_config_valid() {
|
||||
if (current_safety_config.rx_checks_len <= 0) {
|
||||
printf("missing RX checks\n");
|
||||
@@ -30,10 +65,35 @@ bool safety_config_valid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exercise defensive AOL RX guards with configs that production safety init
|
||||
// cannot construct. Restore the active mode's config before returning.
|
||||
bool safety_test_rx_health_fixture(unsigned int variant, bool canfd) {
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
RxCheck check = {
|
||||
.msg = {{.addr = 0x123, .bus = 0U, .len = 8,
|
||||
.frequency = (variant == 2U) ? 9U : 100U,
|
||||
.ignore_checksum = false, .ignore_counter = false,
|
||||
.max_counter = 0U, .ignore_quality_flag = false}, {0}, {0}},
|
||||
.status = {.msg_seen = true, .index = 0, .valid_checksum = true,
|
||||
.valid_quality_flag = true, .last_timestamp = (variant == 3U) ? now - 100001U : now},
|
||||
};
|
||||
const safety_config original = current_safety_config;
|
||||
current_safety_config.rx_checks = (variant == 1U) ? NULL : ✓
|
||||
current_safety_config.rx_checks_len = (variant == 0U) ? 0 : 1;
|
||||
(void)canfd;
|
||||
const bool healthy = aol_rx_healthy();
|
||||
current_safety_config = original;
|
||||
return healthy;
|
||||
}
|
||||
|
||||
void set_controls_allowed(bool c){
|
||||
controls_allowed = c;
|
||||
}
|
||||
|
||||
void set_aol_test_heartbeat(bool engaged) {
|
||||
heartbeat_engaged = engaged;
|
||||
}
|
||||
|
||||
void set_alternative_experience(int mode){
|
||||
alternative_experience = mode;
|
||||
}
|
||||
@@ -144,6 +204,10 @@ void set_desired_torque_last(int t){
|
||||
desired_torque_last = t;
|
||||
}
|
||||
|
||||
int get_desired_angle_last(void){
|
||||
return desired_angle_last;
|
||||
}
|
||||
|
||||
void set_desired_angle_last(int t){
|
||||
desired_angle_last = t;
|
||||
}
|
||||
|
||||
@@ -12,3 +12,6 @@ SAFETY_UNUSED(safety_tx_hook);
|
||||
SAFETY_UNUSED(safety_fwd_hook);
|
||||
SAFETY_UNUSED(safety_tick);
|
||||
SAFETY_UNUSED(set_safety_hooks);
|
||||
// Exported to Panda's USB status handler and the native safety test library.
|
||||
SAFETY_UNUSED(aol_get_request_mask);
|
||||
SAFETY_UNUSED(aol_get_permission_mask);
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -19,6 +24,8 @@ import tree_sitter as ts
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SAFETY_DIR = ROOT / "opendbc" / "safety"
|
||||
BLUEPILOT_SAFETY_DIR = ROOT / "opendbc" / "bluepilot_lateral" / "safety"
|
||||
SAFETY_SOURCE_DIRS = (SAFETY_DIR, BLUEPILOT_SAFETY_DIR)
|
||||
SAFETY_TESTS_DIR = ROOT / "opendbc" / "safety" / "tests"
|
||||
SAFETY_C_REL = Path("opendbc/safety/tests/libsafety/safety.c")
|
||||
|
||||
@@ -74,6 +81,30 @@ class MutantResult:
|
||||
outcome: str # killed | survived | infra_error
|
||||
test_sec: float
|
||||
details: str
|
||||
failure_kind: str | None = None
|
||||
exit_code: int | None = None
|
||||
stderr_tail: str = ""
|
||||
stdout_tail: str = ""
|
||||
unittest_output_tail: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestRun:
|
||||
ran: int
|
||||
skipped: int
|
||||
suppressed: int
|
||||
failures: tuple[str, ...]
|
||||
errors: tuple[str, ...]
|
||||
output_tail: str = ""
|
||||
|
||||
|
||||
class IsolatedRunError(RuntimeError):
|
||||
def __init__(self, kind: str, message: str, *, exit_code: int | None, stderr: str, stdout: str):
|
||||
super().__init__(message)
|
||||
self.kind = kind
|
||||
self.exit_code = exit_code
|
||||
self.stderr_tail = stderr[-32768:]
|
||||
self.stdout_tail = stdout[-8192:]
|
||||
|
||||
|
||||
def colorize(text, color):
|
||||
@@ -105,9 +136,13 @@ def _site_key(site):
|
||||
|
||||
|
||||
def _is_in_constexpr_context(node):
|
||||
"""Check if a node is inside a static or file-scope variable initializer."""
|
||||
"""Find contexts that cannot contain the runtime mutant selector."""
|
||||
current = node.parent
|
||||
while current is not None:
|
||||
if current.type == "call_expression":
|
||||
function = current.child_by_field_name("function")
|
||||
if function is not None and function.text == b"_Static_assert":
|
||||
return True
|
||||
if current.type == "init_declarator":
|
||||
decl = current.parent
|
||||
if decl and decl.type == "declaration":
|
||||
@@ -241,7 +276,7 @@ def enumerate_sites(input_source, preprocessed_file):
|
||||
if mapped is None:
|
||||
continue
|
||||
origin_file, origin_line = mapped
|
||||
if SAFETY_DIR not in origin_file.parents and origin_file != SAFETY_DIR:
|
||||
if not any(origin_file.is_relative_to(source_dir) for source_dir in SAFETY_SOURCE_DIRS):
|
||||
continue
|
||||
site_id = len(out)
|
||||
site = MutationSite(
|
||||
@@ -257,6 +292,138 @@ def enumerate_sites(input_source, preprocessed_file):
|
||||
return out, counts, build_incompatible_site_ids, txt
|
||||
|
||||
|
||||
def equivalent_division_candidates(source, sites, excluded_ids):
|
||||
"""Recognize only closed unsigned-int division used directly in a comparison.
|
||||
|
||||
Replacing one literal may preserve the entire quotient. An unchanged quotient
|
||||
AND C type preserves the comparison for every value of its other operand.
|
||||
This deliberately does not reason about variable expressions or conversions.
|
||||
"""
|
||||
parser = ts.Parser(ts.Language(ts_c.language()))
|
||||
tree = parser.parse(_prepare_for_parsing(source).encode())
|
||||
literals = {}
|
||||
stack = [tree.root_node]
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
if node.type == "number_literal":
|
||||
literals[node.start_byte, node.end_byte] = node
|
||||
stack.extend(node.children)
|
||||
|
||||
def unwrap(node):
|
||||
while node is not None and node.type == "parenthesized_expression" and len(node.named_children) == 1:
|
||||
node = node.named_children[0]
|
||||
return node
|
||||
|
||||
def outer(node):
|
||||
while node.parent is not None and node.parent.type == "parenthesized_expression":
|
||||
node = node.parent
|
||||
return node
|
||||
|
||||
def unsigned_literal(node):
|
||||
node = unwrap(node)
|
||||
if node is None or node.type != "number_literal":
|
||||
return None
|
||||
token = source[node.start_byte:node.end_byte]
|
||||
# Only the U suffix; decimal/hex, with no signed/octal/long/type ambiguity.
|
||||
if re.fullmatch(r"(?:0|[1-9][0-9]*|0[xX][0-9a-fA-F]+)[uU]", token) is None:
|
||||
return None
|
||||
parsed = _parse_int_literal(token)
|
||||
return (node, parsed[0]) if parsed is not None and parsed[0] <= 0xFFFFFFFF else None
|
||||
|
||||
records = []
|
||||
for site in sites:
|
||||
if site.mutator != "boundary" or site.site_id in excluded_ids:
|
||||
continue
|
||||
literal = literals.get((site.op_start, site.op_end))
|
||||
if literal is None:
|
||||
continue
|
||||
division = outer(literal).parent
|
||||
if division is None or division.type != "binary_expression":
|
||||
continue
|
||||
operator = division.child_by_field_name("operator")
|
||||
if operator is None or operator.type != "/":
|
||||
continue
|
||||
left, right = (unsigned_literal(division.child_by_field_name(field)) for field in ("left", "right"))
|
||||
comparison = outer(division).parent
|
||||
if left is None or right is None or comparison is None or comparison.type != "binary_expression":
|
||||
continue
|
||||
comparison_op = comparison.child_by_field_name("operator")
|
||||
if comparison_op is None or comparison_op.type not in COMPARISON_OPERATOR_MAP:
|
||||
continue
|
||||
mutated = unsigned_literal(literal)
|
||||
parsed_mutated = _parse_int_literal(site.mutated_op)
|
||||
if mutated is None or parsed_mutated is None or parsed_mutated[2].lower() != "u" or parsed_mutated[0] > 0xFFFFFFFF:
|
||||
continue
|
||||
numerator, denominator = left[1], right[1]
|
||||
next_numerator = parsed_mutated[0] if left[0] == literal else numerator
|
||||
next_denominator = parsed_mutated[0] if right[0] == literal else denominator
|
||||
if denominator == 0 or next_denominator == 0 or numerator // denominator != next_numerator // next_denominator:
|
||||
continue
|
||||
original = source[division.start_byte:division.end_byte]
|
||||
offset = site.op_start - division.start_byte
|
||||
if original[offset:offset + len(site.original_op)] != site.original_op:
|
||||
raise RuntimeError("equivalence literal no longer matches preprocessed source")
|
||||
changed = original[:offset] + site.mutated_op + original[offset + len(site.original_op):]
|
||||
records.append({"site_id": site.site_id, "source": str(site.origin_file.relative_to(ROOT)),
|
||||
"line": site.origin_line, "mutator": site.mutator, "original_op": site.original_op,
|
||||
"mutated_op": site.mutated_op, "op_start": site.op_start, "op_end": site.op_end,
|
||||
"expression_start": division.start_byte, "expression_end": division.end_byte,
|
||||
"comparison_start": comparison.start_byte, "comparison_end": comparison.end_byte,
|
||||
"comparison_operator": comparison_op.type, "original_expression": original,
|
||||
"mutated_expression": changed, "c_type": "unsigned int", "value": numerator // denominator,
|
||||
"proof_kind": "closed-unsigned-division-v1"})
|
||||
return records
|
||||
|
||||
|
||||
def division_proof_source(records):
|
||||
lines = ["/* Compiler proof of equal closed unsigned-int division expressions. */"]
|
||||
for record in records:
|
||||
original, changed = record["original_expression"], record["mutated_expression"]
|
||||
for expression in (original, changed):
|
||||
lines.append(f'_Static_assert(__builtin_types_compatible_p(__typeof__({expression}), unsigned int), "unsigned int type");')
|
||||
lines.append(f'_Static_assert(({expression}) == {record["value"]}U, "quotient value");')
|
||||
lines.append(f'_Static_assert(({original}) == ({changed}), "equivalent quotient");')
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def prove_equivalent_divisions(source, sites, excluded_ids):
|
||||
records = equivalent_division_candidates(source, sites, excluded_ids)
|
||||
if not records:
|
||||
return []
|
||||
compiler = Path(shutil.which("cc") or "").resolve()
|
||||
if not compiler.is_file():
|
||||
raise RuntimeError("equivalence proof requires the actual C compiler")
|
||||
proof = division_proof_source(records)
|
||||
flags = ["-fPIC", "-g0", "-O0", "-DALLOW_DEBUG", "-Werror", "-std=gnu11", "-fno-builtin", "-fsyntax-only", "-x", "c", "-"]
|
||||
result = subprocess.run([str(compiler), *flags], input=proof, cwd=ROOT, capture_output=True, text=True, timeout=30)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"equivalence compiler proof failed: {result.stderr[-4096:]}")
|
||||
version = subprocess.run([str(compiler), "--version"], cwd=ROOT, capture_output=True, text=True, timeout=30)
|
||||
target = subprocess.run([str(compiler), "-dumpmachine"], cwd=ROOT, capture_output=True, text=True, timeout=30)
|
||||
if version.returncode or target.returncode or not version.stdout.strip() or not re.fullmatch(r"[a-zA-Z0-9_.+-]+", target.stdout.strip()):
|
||||
raise RuntimeError("equivalence compiler identity is unavailable")
|
||||
receipt = {"compiler_argv": [str(compiler), *flags], "compiler_version": version.stdout.strip(),
|
||||
"compiler_target": target.stdout.strip(), "compiler_sha256": hashlib.sha256(compiler.read_bytes()).hexdigest(),
|
||||
"compiler_flags": flags, "proof_source_sha256": hashlib.sha256(proof.encode()).hexdigest()}
|
||||
return [{**record, **receipt} for record in records]
|
||||
|
||||
|
||||
def verify_equivalence_artifact(path):
|
||||
"""Reconstruct proofs from pinned real inputs, rather than trusting the claims."""
|
||||
raw = path.read_bytes()
|
||||
artifact = json.loads(raw)
|
||||
with tempfile.TemporaryDirectory(prefix="mutation-equivalence-verify-") as directory:
|
||||
sites, _, excluded, source = enumerate_sites(ROOT / SAFETY_C_REL, Path(directory) / "source.i")
|
||||
if artifact["discovered"] != len(sites) or artifact["preprocessed_source_sha256"] != hashlib.sha256(source.encode()).hexdigest():
|
||||
raise RuntimeError("equivalence discovery/source identity changed")
|
||||
if {item["site_id"] for item in artifact["pruned_build_incompatible"]} != excluded:
|
||||
raise RuntimeError("constant-context partition changed")
|
||||
proofs = prove_equivalent_divisions(source, sites, excluded)
|
||||
if artifact["proven_equivalent"] != proofs:
|
||||
raise RuntimeError("equivalence partition or compiler proof changed")
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def _build_core_tests(catalog):
|
||||
"""Build test ordering for core (non-mode) files.
|
||||
|
||||
@@ -273,16 +440,15 @@ def _build_core_tests(catalog):
|
||||
method_freq[method] = method_freq.get(method, 0) + 1
|
||||
if method not in method_by_module:
|
||||
method_by_module[method] = {}
|
||||
if name not in method_by_module[method]:
|
||||
method_by_module[method][name] = test_id
|
||||
method_by_module[method].setdefault(name, []).append(test_id)
|
||||
# Round-robin: first instance of each method (by freq), then second, etc.
|
||||
# This ensures diverse early coverage with failfast.
|
||||
sorted_methods = sorted(method_freq, key=lambda m: -method_freq[m])
|
||||
ordered = []
|
||||
max_implementations = max(len(module_map) for module_map in method_by_module.values())
|
||||
max_implementations = max(sum(len(ids) for ids in module_map.values()) for module_map in method_by_module.values())
|
||||
for round_idx in range(max_implementations):
|
||||
for m in sorted_methods:
|
||||
ids = [method_by_module[m][module] for module in sorted(method_by_module[m])]
|
||||
ids = [test_id for module in sorted(method_by_module[m]) for test_id in method_by_module[m][module]]
|
||||
if round_idx < len(ids):
|
||||
ordered.append(ids[round_idx])
|
||||
return ordered
|
||||
@@ -291,16 +457,21 @@ def _build_core_tests(catalog):
|
||||
def build_priority_tests(site, catalog, core_tests):
|
||||
"""Build an ordered list of test IDs for a mutation site.
|
||||
|
||||
For mode files: all tests from the matching test_<mode>.py module.
|
||||
For core files: uses the pre-computed core_tests ordering.
|
||||
For mode files: all tests for the mode's vehicle family, including newer variant suites.
|
||||
For core files: uses the complete pre-computed core_tests ordering.
|
||||
"""
|
||||
src = site.origin_file
|
||||
rel_parts = src.relative_to(ROOT).parts
|
||||
is_mode = len(rel_parts) >= 4 and rel_parts[:3] == ("opendbc", "safety", "modes")
|
||||
is_mode = (len(rel_parts) >= 4 and rel_parts[:3] == ("opendbc", "safety", "modes")) or src.parent == BLUEPILOT_SAFETY_DIR
|
||||
|
||||
if is_mode:
|
||||
mode_file = f"test_{src.stem}.py"
|
||||
return list(catalog.get(mode_file, []))
|
||||
family = src.stem.split("_", 1)[0]
|
||||
family_modules = [name for name in sorted(catalog) if name == f"test_{family}.py" or name.startswith(f"test_{family}_") or
|
||||
(family == "ford" and name == "test_angle_native.py")]
|
||||
if family_modules:
|
||||
return [test_id for name in family_modules for test_id in catalog[name]]
|
||||
# Unmapped mode files still exercise the full safety suite rather than receive zero targets.
|
||||
return [test_id for name in sorted(catalog) for test_id in catalog[name]]
|
||||
return core_tests
|
||||
|
||||
|
||||
@@ -358,7 +529,9 @@ def print_live_status(text, *, final=False):
|
||||
def _discover_test_catalog():
|
||||
loader = unittest.TestLoader()
|
||||
catalog = {}
|
||||
for test_file in sorted(SAFETY_TESTS_DIR.glob("test_*.py")):
|
||||
test_files = [*sorted(SAFETY_TESTS_DIR.glob("test_*.py")),
|
||||
ROOT / "opendbc" / "bluepilot_lateral" / "tests" / "test_angle_native.py"]
|
||||
for test_file in test_files:
|
||||
module_name = ".".join(test_file.relative_to(ROOT).with_suffix("").parts)
|
||||
suite = loader.loadTestsFromName(module_name)
|
||||
catalog[test_file.name] = [t.id() for group in suite for t in group]
|
||||
@@ -381,11 +554,25 @@ def run_unittest(targets, lib_path, mutant_id, verbose):
|
||||
for target in targets:
|
||||
suite.addTests(loader.loadTestsFromName(target))
|
||||
result = runner.run(suite)
|
||||
if result.failures:
|
||||
return result.failures[0][0].id()
|
||||
if result.errors:
|
||||
return result.errors[0][0].id()
|
||||
return None
|
||||
suppressed_indices = set()
|
||||
method_skips = 0
|
||||
for test, _reason in result.skipped:
|
||||
skipped_id = test.id()
|
||||
class_match = re.fullmatch(r"setUpClass \(([^)]+)\)", skipped_id)
|
||||
module_match = re.fullmatch(r"setUpModule \(([^)]+)\)", skipped_id)
|
||||
if class_match or module_match:
|
||||
prefix = (class_match or module_match).group(1) + "."
|
||||
matched = {index for index, target in enumerate(targets) if target.startswith(prefix)}
|
||||
if not matched:
|
||||
raise RuntimeError(f"unmatched unittest skip holder: {skipped_id}")
|
||||
suppressed_indices.update(matched)
|
||||
elif skipped_id in targets:
|
||||
method_skips += 1
|
||||
else:
|
||||
raise RuntimeError(f"unexpected unittest skip holder: {skipped_id}")
|
||||
return TestRun(result.testsRun, method_skips, len(suppressed_indices),
|
||||
tuple(test.id() for test, _ in result.failures),
|
||||
tuple(test.id() for test, _ in result.errors), stream.getvalue()[-32768:])
|
||||
|
||||
|
||||
def _instrument_source(source, sites):
|
||||
@@ -466,16 +653,158 @@ def compile_mutated_library(preprocessed_source, sites, output_so):
|
||||
], cwd=ROOT, check=True)
|
||||
|
||||
|
||||
def eval_mutant(site, targets, lib_path, verbose):
|
||||
def _run_isolated_unittest(site_id, targets, lib_path, timeout_sec):
|
||||
script = """import faulthandler, json, sys
|
||||
from pathlib import Path
|
||||
from dataclasses import asdict
|
||||
from opendbc.safety.tests import mutation
|
||||
faulthandler.enable(all_threads=True)
|
||||
request = json.loads(sys.stdin.read())
|
||||
result = mutation.run_unittest(request['targets'], Path(request['library']), request['site_id'], False)
|
||||
print(json.dumps(asdict(result)), flush=True)
|
||||
"""
|
||||
request = json.dumps({"targets": targets, "library": str(lib_path), "site_id": site_id})
|
||||
child_env = dict(os.environ, PWD=str(ROOT))
|
||||
proc = subprocess.Popen([sys.executable, "-c", script], cwd=ROOT, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, start_new_session=True, env=child_env)
|
||||
try:
|
||||
stdout, stderr = proc.communicate(request, timeout=timeout_sec)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
stdout, stderr = proc.communicate()
|
||||
raise IsolatedRunError("timeout", f"isolated mutant timed out after {timeout_sec:.1f}s",
|
||||
exit_code=proc.returncode, stderr=stderr, stdout=stdout) from exc
|
||||
if proc.returncode != 0:
|
||||
raise IsolatedRunError("signal" if proc.returncode < 0 else "child_exit",
|
||||
f"isolated mutant exit={proc.returncode}", exit_code=proc.returncode,
|
||||
stderr=stderr, stdout=stdout)
|
||||
try:
|
||||
data = json.loads(stdout.splitlines()[-1])
|
||||
return TestRun(**data)
|
||||
except (IndexError, ValueError, TypeError) as exc:
|
||||
raise IsolatedRunError("missing_result", "isolated mutant missing result", exit_code=proc.returncode,
|
||||
stderr=stderr, stdout=stdout) from exc
|
||||
|
||||
|
||||
def eval_mutant(site, targets, lib_path, verbose, timeout_sec):
|
||||
try:
|
||||
t0 = time.perf_counter()
|
||||
failed_test = run_unittest(targets, lib_path, mutant_id=site.site_id, verbose=verbose)
|
||||
run = _run_isolated_unittest(site.site_id, targets, lib_path, timeout_sec)
|
||||
duration = time.perf_counter() - t0
|
||||
if failed_test is not None:
|
||||
return MutantResult(site, "killed", duration, "")
|
||||
if run.errors or ((run.ran + run.suppressed != len(targets)) and not run.failures) or run.ran == run.skipped:
|
||||
return MutantResult(site, "infra_error", duration, f"unittest errors={run.errors} ran={run.ran} skipped={run.skipped} suppressed={run.suppressed}",
|
||||
failure_kind="unittest_error", unittest_output_tail=run.output_tail)
|
||||
if run.failures:
|
||||
return MutantResult(site, "killed", duration, run.failures[0], unittest_output_tail=run.output_tail)
|
||||
return MutantResult(site, "survived", duration, "")
|
||||
except IsolatedRunError as exc:
|
||||
return MutantResult(site, "infra_error", 0.0, str(exc), failure_kind=exc.kind,
|
||||
exit_code=exc.exit_code, stderr_tail=exc.stderr_tail, stdout_tail=exc.stdout_tail)
|
||||
except Exception as exc:
|
||||
return MutantResult(site, "infra_error", 0.0, str(exc))
|
||||
return MutantResult(site, "infra_error", 0.0, str(exc), failure_kind="worker_exception")
|
||||
|
||||
|
||||
def completed_mutant_result(future, site):
|
||||
"""A failed worker is infrastructure loss, not proof that a mutant was killed."""
|
||||
try:
|
||||
return future.result()
|
||||
except Exception as exc:
|
||||
return MutantResult(site, "infra_error", 0.0, f"{type(exc).__name__}: {exc}", failure_kind="pool_exception")
|
||||
|
||||
|
||||
def require_full_baseline(catalog, sites, lib_path, verbose):
|
||||
"""Validate every discovered test before any mutant is submitted to workers."""
|
||||
if not catalog or any(not ids for ids in catalog.values()):
|
||||
raise RuntimeError("empty safety test catalog or module")
|
||||
baseline_ids = [test_id for module in sorted(catalog) for test_id in catalog[module]]
|
||||
baseline = run_unittest(baseline_ids, lib_path, mutant_id=-1, verbose=verbose)
|
||||
if (baseline.failures or baseline.errors or baseline.ran + baseline.suppressed != len(baseline_ids) or
|
||||
baseline.ran == baseline.skipped):
|
||||
raise RuntimeError(f"unmutated full safety baseline failed: {baseline}")
|
||||
|
||||
core_tests = _build_core_tests(catalog)
|
||||
targets = {site.site_id: build_priority_tests(site, catalog, core_tests) for site in sites}
|
||||
missing = [site_id for site_id, ids in targets.items() if not ids]
|
||||
if missing:
|
||||
raise RuntimeError(f"mutants lack test targets: {missing[:20]} ({len(missing)} total)")
|
||||
return targets
|
||||
|
||||
|
||||
def write_results_json(path: Path, *, discovered_sites, pruned_ids, results, site_targets,
|
||||
preprocessed_source: str, baseline_sec: float, proven_equivalent=()) -> None:
|
||||
"""Retain complete bounded diagnostics without changing mutation verdicts."""
|
||||
equivalent_ids = {item["site_id"] for item in proven_equivalent}
|
||||
if (len(equivalent_ids) != len(proven_equivalent) or equivalent_ids & set(pruned_ids) or
|
||||
not equivalent_ids <= {site.site_id for site in discovered_sites}):
|
||||
raise RuntimeError("equivalence partition must contain unique, independently pruned sites")
|
||||
expected_ids = {site.site_id for site in discovered_sites} - set(pruned_ids) - equivalent_ids
|
||||
result_ids = [result.site.site_id for result in results]
|
||||
if len(result_ids) != len(set(result_ids)) or set(result_ids) != expected_ids:
|
||||
raise RuntimeError("mutation result artifact requires one outcome for every executed site")
|
||||
target_sets: list[list[str]] = []
|
||||
target_indexes: dict[tuple[str, ...], int] = {}
|
||||
records = []
|
||||
for result in sorted(results, key=lambda item: item.site.site_id):
|
||||
targets = tuple(site_targets[result.site.site_id])
|
||||
if targets not in target_indexes:
|
||||
target_indexes[targets] = len(target_sets)
|
||||
target_sets.append(list(targets))
|
||||
site = result.site
|
||||
records.append({
|
||||
"site_id": site.site_id,
|
||||
"source": str(site.origin_file.relative_to(ROOT)),
|
||||
"line": site.origin_line,
|
||||
"mutator": site.mutator,
|
||||
"original_op": site.original_op,
|
||||
"mutated_op": site.mutated_op,
|
||||
"selected_test_set": target_indexes[targets],
|
||||
"selected_test_count": len(targets),
|
||||
"outcome": result.outcome,
|
||||
"test_sec": result.test_sec,
|
||||
"details": result.details,
|
||||
"failure_kind": result.failure_kind,
|
||||
"exit_code": result.exit_code,
|
||||
"stderr_tail": result.stderr_tail,
|
||||
"stdout_tail": result.stdout_tail,
|
||||
"unittest_output_tail": result.unittest_output_tail,
|
||||
})
|
||||
sources = {str(site.origin_file.relative_to(ROOT)): hashlib.sha256(site.origin_file.read_bytes()).hexdigest()
|
||||
for site in discovered_sites}
|
||||
artifact = {
|
||||
"schema_version": 2,
|
||||
"proven_equivalent": list(proven_equivalent),
|
||||
"safety_input_sha256": hashlib.sha256((ROOT / SAFETY_C_REL).read_bytes()).hexdigest(),
|
||||
"source_sha256": sources,
|
||||
"preprocessed_source_sha256": hashlib.sha256(preprocessed_source.encode()).hexdigest(),
|
||||
"mutation_runner_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
||||
"baseline_sec": baseline_sec,
|
||||
"discovered": len(discovered_sites),
|
||||
"pruned_build_incompatible": [{
|
||||
"site_id": site.site_id,
|
||||
"source": str(site.origin_file.relative_to(ROOT)),
|
||||
"line": site.origin_line,
|
||||
"mutator": site.mutator,
|
||||
"original_op": site.original_op,
|
||||
"mutated_op": site.mutated_op,
|
||||
} for site in discovered_sites if site.site_id in pruned_ids],
|
||||
"target_sets": target_sets,
|
||||
"results": records,
|
||||
}
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", prefix=f".{path.name}.",
|
||||
suffix=".tmp", dir=path.parent, delete=False) as stream:
|
||||
temporary = Path(stream.name)
|
||||
json.dump(artifact, stream, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, path)
|
||||
temporary = None
|
||||
finally:
|
||||
if temporary is not None:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -484,7 +813,16 @@ def main():
|
||||
parser.add_argument("--max-mutants", type=int, default=0, help="optional limit for debugging (0 means all)")
|
||||
parser.add_argument("--list-only", action="store_true", help="list discovered candidates and exit")
|
||||
parser.add_argument("--verbose", action="store_true", help="print extra debug output")
|
||||
parser.add_argument("--results-json", type=Path, help="optional per-candidate result and diagnostic artifact")
|
||||
parser.add_argument("--verify-equivalence-artifact", type=Path, help=argparse.SUPPRESS)
|
||||
args = parser.parse_args()
|
||||
if args.verify_equivalence_artifact is not None:
|
||||
if args.list_only or args.results_json is not None or args.max_mutants:
|
||||
parser.error("equivalence verification is separate from execution")
|
||||
print(verify_equivalence_artifact(args.verify_equivalence_artifact), flush=True)
|
||||
return 0
|
||||
if args.list_only and args.results_json is not None:
|
||||
parser.error("--results-json requires a full mutation run")
|
||||
|
||||
start = time.perf_counter()
|
||||
|
||||
@@ -495,6 +833,7 @@ def main():
|
||||
|
||||
if args.max_mutants > 0:
|
||||
sites = sites[: args.max_mutants]
|
||||
discovered_sites = sites
|
||||
|
||||
mutator_summary = ", ".join(f"{name} ({c})" for name in MUTATOR_FAMILIES if (c := mutator_counts.get(name, 0)) > 0)
|
||||
print(f"Found {len(sites)} unique candidates: {mutator_summary}", flush=True)
|
||||
@@ -510,9 +849,13 @@ def main():
|
||||
selected_site_ids = {s.site_id for s in sites}
|
||||
build_incompatible_ids &= selected_site_ids
|
||||
pruned_compile_sites = len(build_incompatible_ids)
|
||||
proven_equivalent = prove_equivalent_divisions(preprocessed_source, sites, build_incompatible_ids)
|
||||
equivalent_ids = {record["site_id"] for record in proven_equivalent}
|
||||
sites = [site for site in sites if site.site_id not in equivalent_ids]
|
||||
print(f"Proven equivalent: {len(proven_equivalent)} closed unsigned division mutants", flush=True)
|
||||
if pruned_compile_sites > 0:
|
||||
sites = [s for s in sites if s.site_id not in build_incompatible_ids]
|
||||
print(f"Pruned {pruned_compile_sites} build-incompatible mutants from constant-expression initializers", flush=True)
|
||||
print(f"Pruned {pruned_compile_sites} build-incompatible mutants from constant-expression contexts", flush=True)
|
||||
if not sites:
|
||||
print("Failed to build mutation library: all sites were pruned as build-incompatible", flush=True)
|
||||
return 2
|
||||
@@ -524,49 +867,50 @@ def main():
|
||||
# Forked workers inherit these imports, eliminating per-worker import cost.
|
||||
catalog = _discover_test_catalog()
|
||||
|
||||
# Baseline smoke check
|
||||
baseline_ids = catalog.get("test_defaults.py", [])[:5]
|
||||
baseline_failed = run_unittest(baseline_ids, mutation_lib, mutant_id=-1, verbose=args.verbose)
|
||||
if baseline_failed is not None:
|
||||
print("Baseline smoke failed with mutant_id=-1; aborting to avoid false kill signals.", flush=True)
|
||||
print(f" failed_test: {baseline_failed}", flush=True)
|
||||
# A mutant-induced assertion or exception counts as killed only after this
|
||||
# same complete test catalog has passed against the unmutated library.
|
||||
try:
|
||||
baseline_start = time.perf_counter()
|
||||
site_targets = require_full_baseline(catalog, sites, mutation_lib, args.verbose)
|
||||
baseline_sec = time.perf_counter() - baseline_start
|
||||
except RuntimeError as exc:
|
||||
print(f"Mutation baseline or target selection failed: {exc}", flush=True)
|
||||
return 2
|
||||
|
||||
# Pre-compute test targets per mutation site
|
||||
core_tests = _build_core_tests(catalog)
|
||||
site_targets = {site.site_id: build_priority_tests(site, catalog, core_tests) for site in sites}
|
||||
|
||||
results = []
|
||||
counts = Counter()
|
||||
timeout_sec = max(60.0, min(300.0, baseline_sec * 4.0))
|
||||
print(f"Isolated mutant timeout: {timeout_sec:.1f}s (unmutated full baseline {baseline_sec:.1f}s)", flush=True)
|
||||
|
||||
with ProcessPoolExecutor(max_workers=args.j) as pool:
|
||||
with ProcessPoolExecutor(max_workers=args.j, mp_context=multiprocessing.get_context("spawn")) as pool:
|
||||
future_map = {
|
||||
pool.submit(eval_mutant, site, site_targets[site.site_id], mutation_lib, args.verbose): site for site in sites
|
||||
pool.submit(eval_mutant, site, site_targets[site.site_id], mutation_lib, args.verbose, timeout_sec): site for site in sites
|
||||
}
|
||||
print_live_status(render_progress(0, len(sites), 0, 0, 0, 0.0))
|
||||
try:
|
||||
for fut in as_completed(future_map):
|
||||
try:
|
||||
res = fut.result()
|
||||
except Exception:
|
||||
site = future_map[fut]
|
||||
res = MutantResult(site, "killed", 0.0, "worker process crashed")
|
||||
res = completed_mutant_result(fut, future_map[fut])
|
||||
results.append(res)
|
||||
counts[res.outcome] += 1
|
||||
elapsed_now = time.perf_counter() - start
|
||||
done = len(results) == len(sites)
|
||||
print_live_status(render_progress(len(results), len(sites), counts["killed"], counts["survived"],
|
||||
counts["infra_error"], elapsed_now), final=done)
|
||||
except Exception:
|
||||
# Pool broken — mark all unfinished mutants as killed (crash = behavioral change detected)
|
||||
except Exception as exc:
|
||||
# An interrupted pool cannot establish which mutations changed behavior.
|
||||
completed_ids = {r.site.site_id for r in results}
|
||||
for site in sites:
|
||||
if site.site_id not in completed_ids:
|
||||
results.append(MutantResult(site, "killed", 0.0, "pool broken"))
|
||||
counts["killed"] += 1
|
||||
results.append(MutantResult(site, "infra_error", 0.0, f"pool interrupted: {type(exc).__name__}: {exc}"))
|
||||
counts["infra_error"] += 1
|
||||
elapsed_now = time.perf_counter() - start
|
||||
print_live_status(render_progress(len(results), len(sites), counts["killed"], counts["survived"], counts["infra_error"], elapsed_now), final=True)
|
||||
|
||||
if args.results_json is not None:
|
||||
write_results_json(args.results_json, discovered_sites=discovered_sites,
|
||||
pruned_ids=build_incompatible_ids, results=results, site_targets=site_targets,
|
||||
preprocessed_source=preprocessed_source, baseline_sec=baseline_sec, proven_equivalent=proven_equivalent)
|
||||
|
||||
survivors = sorted((r for r in results if r.outcome == "survived"), key=lambda r: r.site.site_id)
|
||||
if survivors:
|
||||
print("", flush=True)
|
||||
@@ -592,6 +936,7 @@ def main():
|
||||
print(colorize("Mutation summary", ANSI_BOLD), flush=True)
|
||||
print(f" discovered: {discovered_count}", flush=True)
|
||||
print(f" pruned_build_incompatible: {pruned_compile_sites}", flush=True)
|
||||
print(f" proven_equivalent: {len(proven_equivalent)}", flush=True)
|
||||
print(f" total: {len(sites)}", flush=True)
|
||||
print(f" killed: {colorize(str(counts['killed']), ANSI_GREEN)}", flush=True)
|
||||
print(f" survived: {colorize(str(counts['survived']), ANSI_RED)}", flush=True)
|
||||
@@ -604,14 +949,6 @@ def main():
|
||||
if counts["infra_error"] > 0:
|
||||
return 2
|
||||
|
||||
# TODO: fix these surviving mutants and delete this block
|
||||
known_survivors = {
|
||||
("opendbc/safety/lateral.h", 188, "boundary"),
|
||||
("opendbc/safety/lateral.h", 218, "boundary"),
|
||||
("opendbc/safety/lateral.h", 219, "boundary"),
|
||||
}
|
||||
survivors = [r for r in survivors if (str(r.site.origin_file.relative_to(ROOT)), r.site.origin_line, r.site.mutator) not in known_survivors]
|
||||
|
||||
if survivors:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Generic AOL health/default-denial checks before vehicle adapters exist."""
|
||||
import unittest
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
class TestAolDefensiveRxConfiguration(unittest.TestCase):
|
||||
def test_nooutput_has_no_axis_request_or_permission(self):
|
||||
safety = libsafety_py.libsafety
|
||||
self.assertEqual(safety.set_safety_hooks(CarParams.SafetyModel.noOutput, 0), 0)
|
||||
safety.aol_set_host_request(3)
|
||||
self.assertEqual(safety.aol_get_request_mask(), 0)
|
||||
self.assertEqual(safety.aol_get_permission_mask(), 0)
|
||||
|
||||
def test_empty_null_low_frequency_and_stale_configs_deny_health(self):
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_timer(1_000_000)
|
||||
self.assertTrue(safety.safety_test_rx_health_fixture(4, False))
|
||||
for variant in (0, 1, 2, 3):
|
||||
with self.subTest(variant=variant):
|
||||
self.assertFalse(safety.safety_test_rx_health_fixture(variant, False))
|
||||
@@ -0,0 +1,99 @@
|
||||
import gc
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
import weakref
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
class TestCanPacketNativeAllocation(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.directory = tempfile.TemporaryDirectory()
|
||||
cls.addClassCleanup(cls.directory.cleanup)
|
||||
directory = Path(cls.directory.name)
|
||||
source = directory / "probe.c"
|
||||
source.write_text('''#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include "can.h"
|
||||
size_t packet_size(void) { return sizeof(CANPacket_t); }
|
||||
size_t packet_alignment(void) { return _Alignof(CANPacket_t); }
|
||||
size_t packet_data_offset(void) { return offsetof(CANPacket_t, data); }
|
||||
void packet_copy(const CANPacket_t *src, CANPacket_t *out) { *out = *src; }
|
||||
void packet_fill(CANPacket_t *out) {
|
||||
memset(out, 0x92, sizeof(*out));
|
||||
out->addr = 0x123; out->bus = 2; out->data_len_code = 15;
|
||||
for (unsigned int i = 0; i < 64; ++i) out->data[i] = (unsigned char)i;
|
||||
}
|
||||
''')
|
||||
library = directory / "probe.so"
|
||||
subprocess.run(["cc", "-shared", "-fPIC", "-std=gnu11", "-O0", "-Wall", "-Wextra", "-Werror",
|
||||
"-I", str(libsafety_py.safety_dir), str(source), "-o", str(library)], check=True)
|
||||
cls.ffi = libsafety_py.ffi
|
||||
cls.ffi.cdef('''size_t packet_size(void); size_t packet_alignment(void); size_t packet_data_offset(void);
|
||||
void packet_copy(const CANPacket_t *src, CANPacket_t *out); void packet_fill(CANPacket_t *out);''')
|
||||
cls.native = cls.ffi.dlopen(str(library))
|
||||
|
||||
def guarded_packet(self):
|
||||
original_new = self.ffi.new
|
||||
buffers = []
|
||||
native_size = self.native.packet_size()
|
||||
|
||||
def allocate(cdecl, words):
|
||||
self.assertEqual(cdecl, "unsigned int[]")
|
||||
self.assertEqual(words * self.ffi.sizeof("unsigned int"), native_size)
|
||||
storage = original_new(cdecl, words + 4)
|
||||
self.ffi.buffer(storage)[native_size:] = b"\xa5" * 16
|
||||
buffers.append(storage)
|
||||
return storage
|
||||
|
||||
with patch.object(self.ffi, "new", side_effect=allocate):
|
||||
packet = libsafety_py.new_CANPacket()
|
||||
return packet, buffers[0]
|
||||
|
||||
def test_allocation_matches_native_size_alignment_and_packed_data_offset(self):
|
||||
packet, storage = self.guarded_packet()
|
||||
self.assertEqual(self.ffi.sizeof(storage) - 16, self.native.packet_size())
|
||||
self.assertEqual(libsafety_py.CAN_PACKET_ALIGNMENT, self.native.packet_alignment())
|
||||
self.assertEqual(int(self.ffi.cast("uintptr_t", packet)) % self.native.packet_alignment(), 0)
|
||||
self.assertEqual(self.ffi.offsetof("CANPacket_t", "data"), self.native.packet_data_offset())
|
||||
|
||||
def test_native_whole_struct_roundtrip_preserves_output_canary_and_payload(self):
|
||||
source, source_storage = self.guarded_packet()
|
||||
output, output_storage = self.guarded_packet()
|
||||
self.native.packet_fill(source)
|
||||
self.native.packet_copy(source, output)
|
||||
self.assertEqual((output.addr, output.bus, output.data_len_code), (0x123, 2, 15))
|
||||
self.assertEqual(bytes(self.ffi.buffer(output.data)), bytes(range(64)))
|
||||
size = self.native.packet_size()
|
||||
self.assertEqual(bytes(self.ffi.buffer(source_storage)[size:]), b"\xa5" * 16)
|
||||
self.assertEqual(bytes(self.ffi.buffer(output_storage)[size:]), b"\xa5" * 16)
|
||||
|
||||
def test_allocator_keeps_backing_storage_until_packet_is_released(self):
|
||||
packet, storage = self.guarded_packet()
|
||||
reference = weakref.ref(storage)
|
||||
del storage
|
||||
gc.collect()
|
||||
self.assertIsNotNone(reference())
|
||||
self.native.packet_fill(packet)
|
||||
self.assertEqual(packet.data[63], 63)
|
||||
del packet
|
||||
gc.collect()
|
||||
self.assertIsNone(reference())
|
||||
|
||||
def test_unrounded_packed_allocation_boundary_is_overwritten_by_native_copy(self):
|
||||
source = libsafety_py.new_CANPacket()
|
||||
self.native.packet_fill(source)
|
||||
packed_size = self.ffi.sizeof("CANPacket_t")
|
||||
self.assertLess(packed_size, self.native.packet_size())
|
||||
# Allocate enough physical storage: the historical logical boundary is the canary.
|
||||
storage = self.ffi.new("unsigned int[]", (self.native.packet_size() + 16) // 4)
|
||||
self.ffi.buffer(storage)[packed_size:] = b"\xa5" * (self.ffi.sizeof(storage) - packed_size)
|
||||
output = self.ffi.cast("CANPacket_t *", storage)
|
||||
self.native.packet_copy(source, output)
|
||||
self.assertNotEqual(bytes(self.ffi.buffer(storage)[packed_size:self.native.packet_size()]),
|
||||
b"\xa5" * (self.native.packet_size() - packed_size))
|
||||
self.assertEqual(bytes(self.ffi.buffer(storage)[self.native.packet_size():]), b"\xa5" * 16)
|
||||
@@ -0,0 +1,192 @@
|
||||
from concurrent.futures import Future
|
||||
import ctypes
|
||||
from concurrent.futures.process import BrokenProcessPool
|
||||
import importlib.util
|
||||
from collections import Counter
|
||||
import json
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
if importlib.util.find_spec("tree_sitter") and importlib.util.find_spec("tree_sitter_c"):
|
||||
from opendbc.safety.tests import mutation
|
||||
else:
|
||||
mutation = None
|
||||
|
||||
|
||||
class _SkippedClassFixture(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
raise unittest.SkipTest("fixture class unavailable")
|
||||
|
||||
def test_one(self):
|
||||
pass
|
||||
|
||||
def test_two(self):
|
||||
pass
|
||||
|
||||
|
||||
class _PassingFixture(unittest.TestCase):
|
||||
def test_one(self):
|
||||
pass
|
||||
|
||||
|
||||
@unittest.skipIf(mutation is None, "mutation parser dependencies unavailable; full mutation gate requires them")
|
||||
class TestMutationInfrastructureAccounting(unittest.TestCase):
|
||||
@staticmethod
|
||||
def site():
|
||||
return mutation.MutationSite(1, 0, 1, 0, 1, 1, "==", "!=", "comparison", Path("safety.h"), 1)
|
||||
|
||||
def test_static_assert_candidates_remain_accounted_without_runtime_instrumentation(self):
|
||||
source = mutation.SAFETY_DIR / "helpers.h"
|
||||
code = '\n'.join((f'#line 1 "{source}"',
|
||||
'_Static_assert(sizeof(int) == 4, "global word size");',
|
||||
'int permit(int value) {',
|
||||
' _Static_assert(sizeof(int) == 4, "local word size");',
|
||||
' return value <= 3;', '}', ''))
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
original = root / "input.c"
|
||||
original.write_text(code)
|
||||
sites, _counts, pruned, preprocessed = mutation.enumerate_sites(original, root / "input.i")
|
||||
static_sites = {site.site_id for site in sites if site.origin_line in (1, 3)}
|
||||
self.assertTrue(static_sites)
|
||||
self.assertEqual(pruned, static_sites)
|
||||
self.assertEqual({site.mutator for site in sites if site.site_id in pruned}, {"comparison", "boundary"})
|
||||
self.assertEqual({site.site_id for site in sites}, set(range(len(sites))))
|
||||
executable = [site for site in sites if site.site_id not in pruned]
|
||||
self.assertTrue(executable)
|
||||
library = root / "constant-context.so"
|
||||
mutation.compile_mutated_library(preprocessed, executable, library)
|
||||
native = ctypes.CDLL(str(library))
|
||||
native.permit.argtypes, native.permit.restype = [ctypes.c_int], ctypes.c_int
|
||||
native.mutation_set_active_mutant.argtypes = [ctypes.c_int]
|
||||
native.mutation_set_active_mutant(-1)
|
||||
self.assertEqual((native.permit(3), native.permit(4)), (1, 0))
|
||||
comparison = next(site for site in executable if site.mutator == "comparison")
|
||||
native.mutation_set_active_mutant(comparison.site_id)
|
||||
self.assertEqual((native.permit(3), native.permit(4)), (0, 1))
|
||||
|
||||
def test_worker_crash_is_infrastructure_error(self):
|
||||
site = self.site()
|
||||
future = Future()
|
||||
future.set_exception(BrokenProcessPool("worker exited"))
|
||||
result = mutation.completed_mutant_result(future, site)
|
||||
self.assertEqual(result.outcome, "infra_error")
|
||||
self.assertIn("BrokenProcessPool", result.details)
|
||||
|
||||
def test_failed_safety_assertion_remains_killed(self):
|
||||
site = self.site()
|
||||
future = Future()
|
||||
future.set_result(mutation.MutantResult(site, "killed", 0.1, "test assertion"))
|
||||
self.assertEqual(mutation.completed_mutant_result(future, site).outcome, "killed")
|
||||
|
||||
def test_full_baseline_failure_aborts_before_mutant_targets(self):
|
||||
catalog = {"test_defaults.py": ["test_defaults.TestNoOutput.test_tx_hook"]}
|
||||
with patch.object(mutation, "run_unittest", return_value=mutation.TestRun(1, 0, 0, (catalog["test_defaults.py"][0],), ())) as baseline, \
|
||||
patch.object(mutation, "build_priority_tests") as targets:
|
||||
with self.assertRaisesRegex(RuntimeError, "unmutated full safety baseline failed"):
|
||||
mutation.require_full_baseline(catalog, [self.site()], Path("libsafety.so"), False)
|
||||
baseline.assert_called_once_with(catalog["test_defaults.py"], Path("libsafety.so"), mutant_id=-1, verbose=False)
|
||||
targets.assert_not_called()
|
||||
|
||||
def test_empty_target_is_not_treated_as_surviving_mutant(self):
|
||||
catalog = {"test_defaults.py": ["test_defaults.TestNoOutput.test_tx_hook"]}
|
||||
with patch.object(mutation, "run_unittest", return_value=mutation.TestRun(1, 0, 0, (), ())), \
|
||||
patch.object(mutation, "build_priority_tests", return_value=[]):
|
||||
with self.assertRaisesRegex(RuntimeError, "mutants lack test targets"):
|
||||
mutation.require_full_baseline(catalog, [self.site()], Path("libsafety.so"), False)
|
||||
|
||||
def test_unittest_error_does_not_count_as_kill(self):
|
||||
with patch.object(mutation, "_run_isolated_unittest", return_value=mutation.TestRun(1, 0, 0, (), ("fixture.ImportError",))):
|
||||
result = mutation.eval_mutant(self.site(), ["test_defaults.TestNoOutput.test_tx_hook"], Path("libsafety.so"), False, 60)
|
||||
self.assertEqual(result.outcome, "infra_error")
|
||||
|
||||
def test_isolated_worker_timeout_is_infrastructure_error(self):
|
||||
with patch.object(mutation, "_run_isolated_unittest", side_effect=RuntimeError("isolated mutant timed out")):
|
||||
result = mutation.eval_mutant(self.site(), ["test_defaults.TestNoOutput.test_tx_hook"], Path("libsafety.so"), False, 60)
|
||||
self.assertEqual(result.outcome, "infra_error")
|
||||
self.assertIn("timed out", result.details)
|
||||
|
||||
def test_partial_or_all_skipped_baseline_aborts(self):
|
||||
catalog = {"test_defaults.py": ["test.one", "test.two"]}
|
||||
for run in (mutation.TestRun(1, 0, 0, (), ()), mutation.TestRun(2, 2, 0, (), ())):
|
||||
with self.subTest(run=run), patch.object(mutation, "run_unittest", return_value=run):
|
||||
with self.assertRaisesRegex(RuntimeError, "unmutated full safety baseline failed"):
|
||||
mutation.require_full_baseline(catalog, [self.site()], Path("libsafety.so"), False)
|
||||
|
||||
def test_class_level_skip_suppresses_exact_method_ids(self):
|
||||
targets = [f"{__name__}._SkippedClassFixture.test_one", f"{__name__}._SkippedClassFixture.test_two",
|
||||
f"{__name__}._SkippedClassFixture.test_two",
|
||||
f"{__name__}._PassingFixture.test_one"]
|
||||
safety = MagicMock()
|
||||
with patch.object(libsafety_py, "load"), patch.dict(libsafety_py.__dict__, {"libsafety": safety}):
|
||||
result = mutation.run_unittest(targets, Path("unused.so"), -1, False)
|
||||
safety.mutation_set_active_mutant.assert_called_once_with(-1)
|
||||
self.assertEqual((result.ran, result.skipped, result.suppressed, result.failures, result.errors), (1, 0, 3, (), ()))
|
||||
|
||||
def test_family_targets_include_new_ioniq_and_carnival_suites(self):
|
||||
catalog = {"test_hyundai.py": ["base"], "test_hyundai_canfd.py": ["canfd"],
|
||||
"test_hyundai_ioniq6_long.py": ["ioniq_long"], "test_hyundai_canfd_carnival.py": ["carnival"],
|
||||
"test_honda_aol_family.py": ["honda_aol"]}
|
||||
hyundai = mutation.MutationSite(1, 0, 1, 0, 1, 1, "==", "!=", "comparison",
|
||||
mutation.ROOT / "opendbc/safety/modes/hyundai_canfd.h", 1)
|
||||
targets = mutation.build_priority_tests(hyundai, catalog, [])
|
||||
self.assertEqual(set(targets), {"base", "canfd", "ioniq_long", "carnival"})
|
||||
|
||||
def test_core_priority_preserves_duplicate_method_implementations(self):
|
||||
catalog = {"test_one.py": ["A.test_rx", "B.test_rx", "A.test_tx"],
|
||||
"test_two.py": ["C.test_rx", "D.test_tx"]}
|
||||
self.assertEqual(Counter(mutation._build_core_tests(catalog)), Counter(test for ids in catalog.values() for test in ids))
|
||||
|
||||
def test_isolated_signal_keeps_bounded_full_stderr_and_exit_code(self):
|
||||
child = MagicMock()
|
||||
child.returncode = -11
|
||||
child.communicate.return_value = ("partial child output", "first warning\n" + "native backtrace\n" * 100)
|
||||
with patch.object(mutation.subprocess, "Popen", return_value=child) as launch:
|
||||
with self.assertRaises(mutation.IsolatedRunError) as raised:
|
||||
mutation._run_isolated_unittest(1, ["target.test"], Path("libsafety.so"), 60)
|
||||
self.assertEqual(launch.call_args.kwargs["env"]["PWD"], str(mutation.ROOT))
|
||||
self.assertEqual(raised.exception.kind, "signal")
|
||||
self.assertEqual(raised.exception.exit_code, -11)
|
||||
self.assertIn("native backtrace\n", raised.exception.stderr_tail)
|
||||
self.assertEqual(raised.exception.stdout_tail, "partial child output")
|
||||
|
||||
with patch.object(mutation, "_run_isolated_unittest", side_effect=raised.exception):
|
||||
result = mutation.eval_mutant(self.site(), ["target.test"], Path("libsafety.so"), False, 60)
|
||||
self.assertEqual(result.outcome, "infra_error")
|
||||
self.assertEqual((result.failure_kind, result.exit_code), ("signal", -11))
|
||||
self.assertIn("native backtrace\n", result.stderr_tail)
|
||||
|
||||
def test_machine_artifact_preserves_source_targets_and_diagnostics(self):
|
||||
source = mutation.ROOT / "opendbc/safety/safety.h"
|
||||
site = mutation.MutationSite(7, 0, 1, 0, 1, 1, "++", "--", "increment", source, 36)
|
||||
pruned = mutation.MutationSite(8, 0, 1, 0, 1, 1, "0", "1", "boundary", source, 37)
|
||||
result = mutation.MutantResult(site, "infra_error", 0.1, "isolated mutant exit=-11",
|
||||
failure_kind="signal", exit_code=-11,
|
||||
stderr_tail="warning\nfull native backtrace\n", stdout_tail="partial")
|
||||
with TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "nested" / "results.json"
|
||||
mutation.write_results_json(path, discovered_sites=[site, pruned], pruned_ids={8}, results=[result],
|
||||
site_targets={7: ["one.test", "two.test"]},
|
||||
preprocessed_source="frozen preprocessed C", baseline_sec=2.5)
|
||||
artifact = json.loads(path.read_text())
|
||||
self.assertEqual((artifact["discovered"], len(artifact["pruned_build_incompatible"])), (2, 1))
|
||||
self.assertEqual(artifact["target_sets"][artifact["results"][0]["selected_test_set"]], ["one.test", "two.test"])
|
||||
self.assertEqual(artifact["results"][0]["source"], "opendbc/safety/safety.h")
|
||||
self.assertEqual(artifact["results"][0]["exit_code"], -11)
|
||||
self.assertEqual(artifact["results"][0]["stderr_tail"], "warning\nfull native backtrace\n")
|
||||
self.assertEqual(artifact["results"][0]["outcome"], "infra_error")
|
||||
|
||||
with TemporaryDirectory() as directory:
|
||||
with self.assertRaisesRegex(RuntimeError, "one outcome for every executed site"):
|
||||
mutation.write_results_json(Path(directory) / "incomplete.json", discovered_sites=[site],
|
||||
pruned_ids=set(), results=[], site_targets={},
|
||||
preprocessed_source="frozen preprocessed C", baseline_sec=2.5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
// IRQs: CAN1_TX, CAN1_RX0, CAN1_SCE
|
||||
// CAN2_TX, CAN2_RX0, CAN2_SCE
|
||||
// CAN3_TX, CAN3_RX0, CAN3_SCE
|
||||
|
||||
#define CAN_ARRAY_SIZE 3
|
||||
#define CAN_IRQS_ARRAY_SIZE 3
|
||||
extern CAN_TypeDef *cans[CAN_ARRAY_SIZE];
|
||||
extern uint8_t can_irq_number[CAN_IRQS_ARRAY_SIZE][CAN_IRQS_ARRAY_SIZE];
|
||||
|
||||
bool can_set_speed(uint8_t can_number);
|
||||
void can_clear_send(CAN_TypeDef *CANx, uint8_t can_number);
|
||||
void update_can_health_pkt(uint8_t can_number, uint32_t ir_reg);
|
||||
|
||||
// ***************************** CAN *****************************
|
||||
// CANx_TX IRQ Handler
|
||||
// CANx_RX0 IRQ Handler
|
||||
// blink blue when we are receiving CAN messages
|
||||
void can_rx(uint8_t can_number);
|
||||
@@ -3,7 +3,11 @@
|
||||
#include "board/can.h"
|
||||
#include "board/health.h"
|
||||
#include "board/crc.h"
|
||||
#ifdef STM32H7
|
||||
#include "opendbc/safety/can_tx.h"
|
||||
#ifdef STM32F4
|
||||
#include "board/stm32f4/lladc_declarations.h"
|
||||
#include "board/drivers/bxcan_declarations.h"
|
||||
#elif defined(STM32H7)
|
||||
#include "board/stm32h7/lladc_declarations.h"
|
||||
#endif
|
||||
|
||||
@@ -11,7 +15,7 @@
|
||||
|
||||
extern bool bootkick_reset_triggered;
|
||||
|
||||
void bootkick_tick(bool ignition, bool recent_heartbeat);
|
||||
void bootkick_tick(bool ignition, bool recent_heartbeat, bool wake);
|
||||
|
||||
// ******************** can_common ********************
|
||||
|
||||
@@ -43,6 +47,12 @@ extern can_health_t can_health[PANDA_CAN_CNT];
|
||||
// Ignition detected from CAN messages
|
||||
extern bool ignition_can;
|
||||
extern uint32_t ignition_can_cnt;
|
||||
extern bool wake_on_can;
|
||||
extern uint32_t wake_on_can_cnt;
|
||||
#ifdef PANDA_HKG_REMOTE_START
|
||||
extern bool hkg_remote_climate_wake;
|
||||
extern uint32_t hkg_remote_climate_wake_cnt;
|
||||
#endif
|
||||
|
||||
extern bool can_silent;
|
||||
extern bool can_loopback;
|
||||
@@ -71,9 +81,7 @@ extern bus_config_t bus_config[PANDA_CAN_CNT];
|
||||
void can_init_all(void);
|
||||
void can_set_orientation(bool flipped);
|
||||
bool can_tx_check_min_slots_free(uint32_t min);
|
||||
void can_set_checksum(CANPacket_t *packet);
|
||||
bool can_check_checksum(CANPacket_t *packet);
|
||||
void can_send(CANPacket_t *to_push, uint8_t bus_number, bool skip_tx_hook);
|
||||
bool is_speed_valid(uint32_t speed, const uint32_t *all_speeds, uint8_t len);
|
||||
|
||||
// ******************** clock_source ********************
|
||||
@@ -112,6 +120,7 @@ extern FDCAN_GlobalTypeDef *cans[PANDA_CAN_CNT];
|
||||
void can_clear_send(FDCAN_GlobalTypeDef *FDCANx, uint8_t can_number);
|
||||
void update_can_health_pkt(uint8_t can_number, uint32_t ir_reg);
|
||||
|
||||
#endif
|
||||
|
||||
// ******************** harness ********************
|
||||
|
||||
@@ -176,7 +185,6 @@ void handle_interrupt(IRQn_Type irq_type);
|
||||
void interrupt_timer_handler(void);
|
||||
void init_interrupts(bool check_rate_limit);
|
||||
|
||||
#endif // STM32H7
|
||||
|
||||
// ******************** registers ********************
|
||||
|
||||
|
||||
@@ -10,6 +10,18 @@
|
||||
#define HEALTH_FLAG_NMI_RESET (1U << 7)
|
||||
#define HEALTH_FLAG_HARDFAULT_RESET ((uint16_t)1U << 8)
|
||||
|
||||
#define AOL_SAFETY_PROTOCOL_MAGIC 0x314C4F41U // "AOL1", little endian
|
||||
#define AOL_SAFETY_PROTOCOL_VERSION 1U
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic;
|
||||
uint8_t version;
|
||||
uint8_t request_mask;
|
||||
uint8_t permission_mask;
|
||||
uint8_t safety_mode;
|
||||
uint16_t safety_param;
|
||||
uint8_t capability_flags;
|
||||
} aol_safety_health_t;
|
||||
|
||||
struct __attribute__((packed)) health_t {
|
||||
uint32_t uptime_pkt;
|
||||
uint16_t voltage_pkt;
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
static bool panda_ignition_line(void) {
|
||||
#ifdef PANDA_IGNORE_IGNITION_LINE
|
||||
return false;
|
||||
#else
|
||||
return harness_check_ignition();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
extern int _app_start[0xc000]; // Only first 3 sectors of size 0x4000 are used
|
||||
|
||||
// Prototypes
|
||||
@@ -15,7 +24,7 @@ static int get_health_pkt(void *dat) {
|
||||
health->current_pkt = (uint16_t)MIN(current_board->read_current_mA(), 0xFFFFU);
|
||||
|
||||
health->flags_pkt = 0U;
|
||||
health->flags_pkt |= harness_check_ignition() ? HEALTH_FLAG_IGNITION_LINE : 0U;
|
||||
health->flags_pkt |= panda_ignition_line() ? HEALTH_FLAG_IGNITION_LINE : 0U;
|
||||
health->flags_pkt |= ignition_can ? HEALTH_FLAG_IGNITION_CAN : 0U;
|
||||
health->flags_pkt |= controls_allowed ? HEALTH_FLAG_CONTROLS_ALLOWED : 0U;
|
||||
health->flags_pkt |= power_save_enabled ? HEALTH_FLAG_POWER_SAVE_ENABLED : 0U;
|
||||
@@ -48,8 +57,12 @@ static int get_health_pkt(void *dat) {
|
||||
|
||||
health->sound_output_level_pkt = sound_output_level;
|
||||
|
||||
#ifdef STM32H7
|
||||
float temperature_encoded = (CLAMP(dts_get_temperature(), -40.0f, 214.5f) + 40.0f) + 0.5f;
|
||||
health->temperature_pkt = (uint8_t)temperature_encoded;
|
||||
#else
|
||||
health->temperature_pkt = 0U;
|
||||
#endif
|
||||
|
||||
return sizeof(*health);
|
||||
}
|
||||
@@ -65,6 +78,25 @@ int comms_control_handler(ControlPacket_t *req, uint8_t *resp) {
|
||||
uint32_t time;
|
||||
|
||||
switch (req->request) {
|
||||
// Fork-local read-only AOL capability and actual native permissions.
|
||||
// Older firmware returns no matching packet, so the host fails closed.
|
||||
case 0xd5:
|
||||
{
|
||||
aol_safety_health_t status = {
|
||||
.magic = AOL_SAFETY_PROTOCOL_MAGIC,
|
||||
.version = AOL_SAFETY_PROTOCOL_VERSION,
|
||||
.request_mask = aol_get_request_mask(),
|
||||
.permission_mask = aol_get_permission_mask(),
|
||||
.safety_mode = (uint8_t)current_safety_mode,
|
||||
.safety_param = current_safety_param,
|
||||
.capability_flags = (aol_policy != NULL) ? (0x1U | (aol_optional_release_capable ? 0x2U : 0U)) : 0U,
|
||||
};
|
||||
for (unsigned int i = 0U; i < sizeof(status); i++) {
|
||||
resp[i] = ((uint8_t *)&status)[i];
|
||||
}
|
||||
resp_len = sizeof(status);
|
||||
}
|
||||
break;
|
||||
// **** 0xa8: get microsecond timer
|
||||
case 0xa8:
|
||||
time = microsecond_timer_get();
|
||||
@@ -89,7 +121,7 @@ int comms_control_handler(ControlPacket_t *req, uint8_t *resp) {
|
||||
resp_len = 2;
|
||||
break;
|
||||
// **** 0xb5: request deep sleep, wakes on CAN or SBU
|
||||
#ifdef ALLOW_DEBUG
|
||||
#if defined(ALLOW_DEBUG) && defined(STM32H7)
|
||||
case 0xb5:
|
||||
set_safety_mode(SAFETY_SILENT, 0U);
|
||||
set_power_save_state(true);
|
||||
@@ -293,6 +325,11 @@ int comms_control_handler(ControlPacket_t *req, uint8_t *resp) {
|
||||
heartbeat_disabled = true;
|
||||
}
|
||||
break;
|
||||
// A separate short-lived axis request leaves the legacy heartbeat protocol
|
||||
// unchanged for every car without the exact Honda AOL safety parameter.
|
||||
case 0xf5:
|
||||
aol_set_host_request((uint8_t)req->param1);
|
||||
break;
|
||||
// **** 0xf9: set CAN FD data bitrate
|
||||
case 0xf9:
|
||||
if ((req->param1 < PANDA_CAN_CNT) &&
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
typedef enum {
|
||||
SAMPLETIME_3_CYCLES = 0,
|
||||
SAMPLETIME_15_CYCLES = 1,
|
||||
SAMPLETIME_28_CYCLES = 2,
|
||||
SAMPLETIME_56_CYCLES = 3,
|
||||
SAMPLETIME_84_CYCLES = 4,
|
||||
SAMPLETIME_112_CYCLES = 5,
|
||||
SAMPLETIME_144_CYCLES = 6,
|
||||
SAMPLETIME_480_CYCLES = 7
|
||||
} adc_sample_time_t;
|
||||
|
||||
typedef struct {
|
||||
ADC_TypeDef *adc;
|
||||
uint8_t channel;
|
||||
adc_sample_time_t sample_time;
|
||||
} adc_signal_t;
|
||||
|
||||
#define ADC_CHANNEL_DEFAULT(a, c) {.adc = (a), .channel = (c), .sample_time = SAMPLETIME_480_CYCLES}
|
||||
@@ -58,5 +58,8 @@ void fault_recovered(uint32_t fault);
|
||||
// ******************** power_saving ********************
|
||||
|
||||
extern bool power_save_enabled;
|
||||
#if defined(ALLOW_DEBUG) && defined(STM32H7)
|
||||
extern volatile bool stop_mode_requested;
|
||||
#endif
|
||||
|
||||
void set_power_save_state(bool enable);
|
||||
|
||||
Reference in New Issue
Block a user