Add openpilot tests
This commit is contained in:
2
panda/drivers/linux/test/Makefile
Normal file
2
panda/drivers/linux/test/Makefile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
all:
|
||||||
|
gcc main.c -o cantest -pthread -lpthread
|
||||||
120
panda/drivers/linux/test/main.c
Normal file
120
panda/drivers/linux/test/main.c
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
|
#include <net/if.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
|
||||||
|
#include <linux/can.h>
|
||||||
|
#include <linux/can/raw.h>
|
||||||
|
|
||||||
|
const char *ifname = "can0";
|
||||||
|
|
||||||
|
static unsigned char payload[] = {0xAA, 0xAA, 0xAA, 0xAA, 0x07, 0x00, 0x00, 0x00};
|
||||||
|
int packet_len = 8;
|
||||||
|
int dir = 0;
|
||||||
|
|
||||||
|
void *write_thread( void *dat ){
|
||||||
|
int nbytes;
|
||||||
|
struct can_frame frame;
|
||||||
|
int s = *((int*) dat);
|
||||||
|
|
||||||
|
while(1){
|
||||||
|
for(int i = 0; i < 1; i ++){
|
||||||
|
if(packet_len % 2){
|
||||||
|
frame.can_id = 0x8AA | CAN_EFF_FLAG;
|
||||||
|
}else{
|
||||||
|
frame.can_id = 0xAA;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.can_dlc = packet_len;
|
||||||
|
memcpy(frame.data, payload, frame.can_dlc);
|
||||||
|
|
||||||
|
nbytes = write(s, &frame, sizeof(struct can_frame));
|
||||||
|
|
||||||
|
printf("Wrote %d bytes; addr: %lx; datlen: %d\n", nbytes, frame.can_id, frame.can_dlc);
|
||||||
|
|
||||||
|
if(dir){
|
||||||
|
packet_len++;
|
||||||
|
if(packet_len >= 8)
|
||||||
|
dir = 0;
|
||||||
|
}else{
|
||||||
|
packet_len--;
|
||||||
|
if(packet_len <= 0)
|
||||||
|
dir = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
pthread_t sndthread;
|
||||||
|
int err, s, nbytes;
|
||||||
|
struct sockaddr_can addr;
|
||||||
|
struct can_frame frame;
|
||||||
|
struct ifreq ifr;
|
||||||
|
|
||||||
|
if((s = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0) {
|
||||||
|
perror("Error while opening socket");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
strcpy(ifr.ifr_name, ifname);
|
||||||
|
ioctl(s, SIOCGIFINDEX, &ifr);
|
||||||
|
|
||||||
|
addr.can_family = AF_CAN;
|
||||||
|
addr.can_ifindex = ifr.ifr_ifindex;
|
||||||
|
|
||||||
|
printf("%s at index %d\n", ifname, ifr.ifr_ifindex);
|
||||||
|
|
||||||
|
if(bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||||
|
perror("Error in socket bind");
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/////// Create Write Thread
|
||||||
|
|
||||||
|
err = pthread_create( &sndthread, NULL, write_thread, (void*) &s);
|
||||||
|
if(err){
|
||||||
|
fprintf(stderr,"Error - pthread_create() return code: %d\n", err);
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////// Listen to socket
|
||||||
|
while (1) {
|
||||||
|
struct can_frame framein;
|
||||||
|
|
||||||
|
// Read in a CAN frame
|
||||||
|
int numBytes = read(s, &framein, CANFD_MTU);
|
||||||
|
switch (numBytes) {
|
||||||
|
case CAN_MTU:
|
||||||
|
if(framein.can_id & 0x80000000)
|
||||||
|
printf("Received %u byte payload; canid 0x%lx (EXT)\n",
|
||||||
|
framein.can_dlc, framein.can_id & 0x7FFFFFFF);
|
||||||
|
else
|
||||||
|
printf("Received %u byte payload; canid 0x%lx\n", framein.can_dlc, framein.can_id);
|
||||||
|
break;
|
||||||
|
case CANFD_MTU:
|
||||||
|
// TODO: Should make an example for CAN FD
|
||||||
|
break;
|
||||||
|
case -1:
|
||||||
|
// Check the signal value on interrupt
|
||||||
|
//if (EINTR == errno)
|
||||||
|
// continue;
|
||||||
|
|
||||||
|
// Delay before continuing
|
||||||
|
sleep(1);
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
4
panda/drivers/linux/test/run.sh
Normal file
4
panda/drivers/linux/test/run.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
sudo ifconfig can0 up
|
||||||
|
make
|
||||||
|
./cantest
|
||||||
0
panda/tests/__init__.py
Normal file
0
panda/tests/__init__.py
Normal file
43
panda/tests/benchmark.py
Normal file
43
panda/tests/benchmark.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import time
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from panda import Panda, PandaDFU
|
||||||
|
from panda.tests.hitl.helpers import get_random_can_messages
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def print_time(desc):
|
||||||
|
start = time.perf_counter()
|
||||||
|
yield
|
||||||
|
end = time.perf_counter()
|
||||||
|
print(f"{end - start:.2f}s - {desc}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with print_time("Panda()"):
|
||||||
|
p = Panda()
|
||||||
|
|
||||||
|
with print_time("PandaDFU.list()"):
|
||||||
|
PandaDFU.list()
|
||||||
|
|
||||||
|
fxn = [
|
||||||
|
'reset',
|
||||||
|
'reconnect',
|
||||||
|
'up_to_date',
|
||||||
|
'health',
|
||||||
|
#'flash',
|
||||||
|
]
|
||||||
|
for f in fxn:
|
||||||
|
with print_time(f"Panda.{f}()"):
|
||||||
|
getattr(p, f)()
|
||||||
|
|
||||||
|
p.set_can_loopback(True)
|
||||||
|
|
||||||
|
for n in range(6):
|
||||||
|
msgs = get_random_can_messages(int(10**n))
|
||||||
|
with print_time(f"Panda.can_send_many() - {len(msgs)} msgs"):
|
||||||
|
p.can_send_many(msgs)
|
||||||
|
|
||||||
|
with print_time("Panda.can_recv()"):
|
||||||
|
m = p.can_recv()
|
||||||
156
panda/tests/black_white_loopback_test.py
Normal file
156
panda/tests/black_white_loopback_test.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Loopback test between black panda (+ harness and power) and white/grey panda
|
||||||
|
# Tests all buses, including OBD CAN, which is on the same bus as CAN0 in this test.
|
||||||
|
# To be sure, the test should be run with both harness orientations
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import argparse
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
def get_test_string():
|
||||||
|
return b"test" + os.urandom(10)
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
nonzero_bus_errors = 0
|
||||||
|
zero_bus_errors = 0
|
||||||
|
content_errors = 0
|
||||||
|
|
||||||
|
def run_test(sleep_duration):
|
||||||
|
global counter
|
||||||
|
|
||||||
|
pandas = Panda.list()
|
||||||
|
print(pandas)
|
||||||
|
|
||||||
|
# make sure two pandas are connected
|
||||||
|
if len(pandas) != 2:
|
||||||
|
raise Exception("Connect white/grey and black panda to run this test!")
|
||||||
|
|
||||||
|
# connect
|
||||||
|
pandas[0] = Panda(pandas[0])
|
||||||
|
pandas[1] = Panda(pandas[1])
|
||||||
|
|
||||||
|
black_panda = None
|
||||||
|
other_panda = None
|
||||||
|
|
||||||
|
# find out which one is black
|
||||||
|
if pandas[0].is_black() and not pandas[1].is_black():
|
||||||
|
black_panda = pandas[0]
|
||||||
|
other_panda = pandas[1]
|
||||||
|
elif not pandas[0].is_black() and pandas[1].is_black():
|
||||||
|
black_panda = pandas[1]
|
||||||
|
other_panda = pandas[0]
|
||||||
|
else:
|
||||||
|
raise Exception("Connect white/grey and black panda to run this test!")
|
||||||
|
|
||||||
|
# disable safety modes
|
||||||
|
black_panda.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
other_panda.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
# test health packet
|
||||||
|
print("black panda health", black_panda.health())
|
||||||
|
print("other panda health", other_panda.health())
|
||||||
|
|
||||||
|
# test black -> other
|
||||||
|
while True:
|
||||||
|
test_buses(black_panda, other_panda, True, [(0, False, [0]), (1, False, [1]), (2, False, [2]), (1, True, [0])], sleep_duration)
|
||||||
|
test_buses(black_panda, other_panda, False, [(0, False, [0]), (1, False, [1]), (2, False, [2]), (0, True, [0, 1])], sleep_duration)
|
||||||
|
counter += 1
|
||||||
|
print("Number of cycles:", counter, "Non-zero bus errors:", nonzero_bus_errors, "Zero bus errors:", zero_bus_errors, "Content errors:", content_errors)
|
||||||
|
|
||||||
|
# Toggle relay
|
||||||
|
black_panda.set_safety_mode(Panda.SAFETY_SILENT)
|
||||||
|
time.sleep(1)
|
||||||
|
black_panda.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_buses(black_panda, other_panda, direction, test_array, sleep_duration):
|
||||||
|
global nonzero_bus_errors, zero_bus_errors, content_errors
|
||||||
|
|
||||||
|
if direction:
|
||||||
|
print("***************** TESTING (BLACK --> OTHER) *****************")
|
||||||
|
else:
|
||||||
|
print("***************** TESTING (OTHER --> BLACK) *****************")
|
||||||
|
|
||||||
|
for send_bus, obd, recv_buses in test_array:
|
||||||
|
black_panda.send_heartbeat()
|
||||||
|
other_panda.send_heartbeat()
|
||||||
|
print("\ntest can: ", send_bus, " OBD: ", obd)
|
||||||
|
|
||||||
|
# set OBD on black panda
|
||||||
|
black_panda.set_gmlan(True if obd else None)
|
||||||
|
|
||||||
|
# clear and flush
|
||||||
|
if direction:
|
||||||
|
black_panda.can_clear(send_bus)
|
||||||
|
else:
|
||||||
|
other_panda.can_clear(send_bus)
|
||||||
|
|
||||||
|
for recv_bus in recv_buses:
|
||||||
|
if direction:
|
||||||
|
other_panda.can_clear(recv_bus)
|
||||||
|
else:
|
||||||
|
black_panda.can_clear(recv_bus)
|
||||||
|
|
||||||
|
black_panda.can_recv()
|
||||||
|
other_panda.can_recv()
|
||||||
|
|
||||||
|
# send the characters
|
||||||
|
at = random.randint(1, 2000)
|
||||||
|
st = get_test_string()[0:8]
|
||||||
|
if direction:
|
||||||
|
black_panda.can_send(at, st, send_bus)
|
||||||
|
else:
|
||||||
|
other_panda.can_send(at, st, send_bus)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# check for receive
|
||||||
|
if direction:
|
||||||
|
_ = black_panda.can_recv() # can echo
|
||||||
|
cans_loop = other_panda.can_recv()
|
||||||
|
else:
|
||||||
|
_ = other_panda.can_recv() # can echo
|
||||||
|
cans_loop = black_panda.can_recv()
|
||||||
|
|
||||||
|
loop_buses = []
|
||||||
|
for loop in cans_loop:
|
||||||
|
if (loop[0] != at) or (loop[2] != st):
|
||||||
|
content_errors += 1
|
||||||
|
|
||||||
|
print(" Loop on bus", str(loop[3]))
|
||||||
|
loop_buses.append(loop[3])
|
||||||
|
if len(cans_loop) == 0:
|
||||||
|
print(" No loop")
|
||||||
|
assert not os.getenv("NOASSERT")
|
||||||
|
|
||||||
|
# test loop buses
|
||||||
|
recv_buses.sort()
|
||||||
|
loop_buses.sort()
|
||||||
|
if(recv_buses != loop_buses):
|
||||||
|
if len(loop_buses) == 0:
|
||||||
|
zero_bus_errors += 1
|
||||||
|
else:
|
||||||
|
nonzero_bus_errors += 1
|
||||||
|
assert not os.getenv("NOASSERT")
|
||||||
|
else:
|
||||||
|
print(" TEST PASSED")
|
||||||
|
|
||||||
|
time.sleep(sleep_duration)
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-n", type=int, help="Number of test iterations to run")
|
||||||
|
parser.add_argument("-sleep", type=int, help="Sleep time between tests", default=0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.n is None:
|
||||||
|
while True:
|
||||||
|
run_test(sleep_duration=args.sleep)
|
||||||
|
else:
|
||||||
|
for _ in range(args.n):
|
||||||
|
run_test(sleep_duration=args.sleep)
|
||||||
164
panda/tests/black_white_relay_endurance.py
Normal file
164
panda/tests/black_white_relay_endurance.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Loopback test between black panda (+ harness and power) and white/grey panda
|
||||||
|
# Tests all buses, including OBD CAN, which is on the same bus as CAN0 in this test.
|
||||||
|
# To be sure, the test should be run with both harness orientations
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
def get_test_string():
|
||||||
|
return b"test" + os.urandom(10)
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
nonzero_bus_errors = 0
|
||||||
|
zero_bus_errors = 0
|
||||||
|
content_errors = 0
|
||||||
|
|
||||||
|
def run_test(sleep_duration):
|
||||||
|
global counter
|
||||||
|
|
||||||
|
pandas = Panda.list()
|
||||||
|
print(pandas)
|
||||||
|
|
||||||
|
# make sure two pandas are connected
|
||||||
|
if len(pandas) != 2:
|
||||||
|
raise Exception("Connect white/grey and black panda to run this test!")
|
||||||
|
|
||||||
|
# connect
|
||||||
|
pandas[0] = Panda(pandas[0])
|
||||||
|
pandas[1] = Panda(pandas[1])
|
||||||
|
|
||||||
|
black_panda = None
|
||||||
|
other_panda = None
|
||||||
|
|
||||||
|
# find out which one is black
|
||||||
|
if pandas[0].is_black() and not pandas[1].is_black():
|
||||||
|
black_panda = pandas[0]
|
||||||
|
other_panda = pandas[1]
|
||||||
|
elif not pandas[0].is_black() and pandas[1].is_black():
|
||||||
|
black_panda = pandas[1]
|
||||||
|
other_panda = pandas[0]
|
||||||
|
else:
|
||||||
|
raise Exception("Connect white/grey and black panda to run this test!")
|
||||||
|
|
||||||
|
# disable safety modes
|
||||||
|
black_panda.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
other_panda.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
# test health packet
|
||||||
|
print("black panda health", black_panda.health())
|
||||||
|
print("other panda health", other_panda.health())
|
||||||
|
|
||||||
|
# test black -> other
|
||||||
|
start_time = time.time()
|
||||||
|
temp_start_time = start_time
|
||||||
|
while True:
|
||||||
|
test_buses(black_panda, other_panda, True, [(0, False, [0]), (1, False, [1]), (2, False, [2]), (1, True, [0])], sleep_duration)
|
||||||
|
test_buses(black_panda, other_panda, False, [(0, False, [0]), (1, False, [1]), (2, False, [2]), (0, True, [0, 1])], sleep_duration)
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
runtime = time.time() - start_time
|
||||||
|
print("Number of cycles:", counter, "Non-zero bus errors:", nonzero_bus_errors, "Zero bus errors:", zero_bus_errors,
|
||||||
|
"Content errors:", content_errors, "Runtime: ", runtime)
|
||||||
|
|
||||||
|
if (time.time() - temp_start_time) > 3600 * 6:
|
||||||
|
# Toggle relay
|
||||||
|
black_panda.set_safety_mode(Panda.SAFETY_SILENT)
|
||||||
|
time.sleep(1)
|
||||||
|
black_panda.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
time.sleep(1)
|
||||||
|
temp_start_time = time.time()
|
||||||
|
|
||||||
|
|
||||||
|
def test_buses(black_panda, other_panda, direction, test_array, sleep_duration):
|
||||||
|
global nonzero_bus_errors, zero_bus_errors, content_errors
|
||||||
|
|
||||||
|
if direction:
|
||||||
|
print("***************** TESTING (BLACK --> OTHER) *****************")
|
||||||
|
else:
|
||||||
|
print("***************** TESTING (OTHER --> BLACK) *****************")
|
||||||
|
|
||||||
|
for send_bus, obd, recv_buses in test_array:
|
||||||
|
black_panda.send_heartbeat()
|
||||||
|
other_panda.send_heartbeat()
|
||||||
|
print("\ntest can: ", send_bus, " OBD: ", obd)
|
||||||
|
|
||||||
|
# set OBD on black panda
|
||||||
|
black_panda.set_gmlan(True if obd else None)
|
||||||
|
|
||||||
|
# clear and flush
|
||||||
|
if direction:
|
||||||
|
black_panda.can_clear(send_bus)
|
||||||
|
else:
|
||||||
|
other_panda.can_clear(send_bus)
|
||||||
|
|
||||||
|
for recv_bus in recv_buses:
|
||||||
|
if direction:
|
||||||
|
other_panda.can_clear(recv_bus)
|
||||||
|
else:
|
||||||
|
black_panda.can_clear(recv_bus)
|
||||||
|
|
||||||
|
black_panda.can_recv()
|
||||||
|
other_panda.can_recv()
|
||||||
|
|
||||||
|
# send the characters
|
||||||
|
at = random.randint(1, 2000)
|
||||||
|
st = get_test_string()[0:8]
|
||||||
|
if direction:
|
||||||
|
black_panda.can_send(at, st, send_bus)
|
||||||
|
else:
|
||||||
|
other_panda.can_send(at, st, send_bus)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# check for receive
|
||||||
|
if direction:
|
||||||
|
_ = black_panda.can_recv() # cans echo
|
||||||
|
cans_loop = other_panda.can_recv()
|
||||||
|
else:
|
||||||
|
_ = other_panda.can_recv() # cans echo
|
||||||
|
cans_loop = black_panda.can_recv()
|
||||||
|
|
||||||
|
loop_buses = []
|
||||||
|
for loop in cans_loop:
|
||||||
|
if (loop[0] != at) or (loop[2] != st):
|
||||||
|
content_errors += 1
|
||||||
|
|
||||||
|
print(" Loop on bus", str(loop[3]))
|
||||||
|
loop_buses.append(loop[3])
|
||||||
|
if len(cans_loop) == 0:
|
||||||
|
print(" No loop")
|
||||||
|
assert os.getenv("NOASSERT")
|
||||||
|
|
||||||
|
# test loop buses
|
||||||
|
recv_buses.sort()
|
||||||
|
loop_buses.sort()
|
||||||
|
if(recv_buses != loop_buses):
|
||||||
|
if len(loop_buses) == 0:
|
||||||
|
zero_bus_errors += 1
|
||||||
|
else:
|
||||||
|
nonzero_bus_errors += 1
|
||||||
|
assert os.getenv("NOASSERT")
|
||||||
|
else:
|
||||||
|
print(" TEST PASSED")
|
||||||
|
|
||||||
|
time.sleep(sleep_duration)
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-n", type=int, help="Number of test iterations to run")
|
||||||
|
parser.add_argument("-sleep", type=int, help="Sleep time between tests", default=0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.n is None:
|
||||||
|
while True:
|
||||||
|
run_test(sleep_duration=args.sleep)
|
||||||
|
else:
|
||||||
|
for _ in range(args.n):
|
||||||
|
run_test(sleep_duration=args.sleep)
|
||||||
135
panda/tests/black_white_relay_test.py
Normal file
135
panda/tests/black_white_relay_test.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Relay test with loopback between black panda (+ harness and power) and white/grey panda
|
||||||
|
# Tests the relay switching multiple times / second by looking at the buses on which loop occurs.
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
def get_test_string():
|
||||||
|
return b"test" + os.urandom(10)
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
open_errors = 0
|
||||||
|
closed_errors = 0
|
||||||
|
content_errors = 0
|
||||||
|
|
||||||
|
def run_test(sleep_duration):
|
||||||
|
global counter, open_errors, closed_errors
|
||||||
|
|
||||||
|
pandas = Panda.list()
|
||||||
|
print(pandas)
|
||||||
|
|
||||||
|
# make sure two pandas are connected
|
||||||
|
if len(pandas) != 2:
|
||||||
|
raise Exception("Connect white/grey and black panda to run this test!")
|
||||||
|
|
||||||
|
# connect
|
||||||
|
pandas[0] = Panda(pandas[0])
|
||||||
|
pandas[1] = Panda(pandas[1])
|
||||||
|
|
||||||
|
# find out which one is black
|
||||||
|
type0 = pandas[0].get_type()
|
||||||
|
type1 = pandas[1].get_type()
|
||||||
|
|
||||||
|
black_panda = None
|
||||||
|
other_panda = None
|
||||||
|
|
||||||
|
if type0 == "\x03" and type1 != "\x03":
|
||||||
|
black_panda = pandas[0]
|
||||||
|
other_panda = pandas[1]
|
||||||
|
elif type0 != "\x03" and type1 == "\x03":
|
||||||
|
black_panda = pandas[1]
|
||||||
|
other_panda = pandas[0]
|
||||||
|
else:
|
||||||
|
raise Exception("Connect white/grey and black panda to run this test!")
|
||||||
|
|
||||||
|
# disable safety modes
|
||||||
|
black_panda.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
other_panda.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
# test health packet
|
||||||
|
print("black panda health", black_panda.health())
|
||||||
|
print("other panda health", other_panda.health())
|
||||||
|
|
||||||
|
# test black -> other
|
||||||
|
while True:
|
||||||
|
# Switch on relay
|
||||||
|
black_panda.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
if not test_buses(black_panda, other_panda, (0, False, [0])):
|
||||||
|
open_errors += 1
|
||||||
|
raise Exception("Open error")
|
||||||
|
|
||||||
|
# Switch off relay
|
||||||
|
black_panda.set_safety_mode(Panda.SAFETY_SILENT)
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
if not test_buses(black_panda, other_panda, (0, False, [0, 2])):
|
||||||
|
closed_errors += 1
|
||||||
|
raise Exception("Close error")
|
||||||
|
|
||||||
|
counter += 1
|
||||||
|
print("Number of cycles:", counter, "Open errors:", open_errors, "Closed errors:", closed_errors, "Content errors:", content_errors)
|
||||||
|
|
||||||
|
def test_buses(black_panda, other_panda, test_obj):
|
||||||
|
global content_errors
|
||||||
|
send_bus, obd, recv_buses = test_obj
|
||||||
|
|
||||||
|
black_panda.send_heartbeat()
|
||||||
|
other_panda.send_heartbeat()
|
||||||
|
|
||||||
|
# Set OBD on send panda
|
||||||
|
other_panda.set_gmlan(True if obd else None)
|
||||||
|
|
||||||
|
# clear and flush
|
||||||
|
other_panda.can_clear(send_bus)
|
||||||
|
|
||||||
|
for recv_bus in recv_buses:
|
||||||
|
black_panda.can_clear(recv_bus)
|
||||||
|
|
||||||
|
black_panda.can_recv()
|
||||||
|
other_panda.can_recv()
|
||||||
|
|
||||||
|
# send the characters
|
||||||
|
at = random.randint(1, 2000)
|
||||||
|
st = get_test_string()[0:8]
|
||||||
|
other_panda.can_send(at, st, send_bus)
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
# check for receive
|
||||||
|
_ = other_panda.can_recv() # can echo
|
||||||
|
cans_loop = black_panda.can_recv()
|
||||||
|
|
||||||
|
loop_buses = []
|
||||||
|
for loop in cans_loop:
|
||||||
|
if (loop[0] != at) or (loop[2] != st):
|
||||||
|
content_errors += 1
|
||||||
|
loop_buses.append(loop[3])
|
||||||
|
|
||||||
|
# test loop buses
|
||||||
|
recv_buses.sort()
|
||||||
|
loop_buses.sort()
|
||||||
|
if(recv_buses != loop_buses):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-n", type=int, help="Number of test iterations to run")
|
||||||
|
parser.add_argument("-sleep", type=int, help="Sleep time between tests", default=0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.n is None:
|
||||||
|
while True:
|
||||||
|
run_test(sleep_duration=args.sleep)
|
||||||
|
else:
|
||||||
|
for _ in range(args.n):
|
||||||
|
run_test(sleep_duration=args.sleep)
|
||||||
48
panda/tests/bulk_write_test.py
Normal file
48
panda/tests/bulk_write_test.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
JUNGLE = "JUNGLE" in os.environ
|
||||||
|
if JUNGLE:
|
||||||
|
from panda import PandaJungle
|
||||||
|
|
||||||
|
# The TX buffers on pandas is 0x100 in length.
|
||||||
|
NUM_MESSAGES_PER_BUS = 10000
|
||||||
|
|
||||||
|
def flood_tx(panda):
|
||||||
|
print('Sending!')
|
||||||
|
msg = b"\xaa" * 4
|
||||||
|
packet = [[0xaa, None, msg, 0], [0xaa, None, msg, 1], [0xaa, None, msg, 2]] * NUM_MESSAGES_PER_BUS
|
||||||
|
panda.can_send_many(packet, timeout=10000)
|
||||||
|
print(f"Done sending {3*NUM_MESSAGES_PER_BUS} messages!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
serials = Panda.list()
|
||||||
|
if JUNGLE:
|
||||||
|
sender = Panda()
|
||||||
|
receiver = PandaJungle()
|
||||||
|
else:
|
||||||
|
if len(serials) != 2:
|
||||||
|
raise Exception("Connect two pandas to perform this test!")
|
||||||
|
sender = Panda(serials[0])
|
||||||
|
receiver = Panda(serials[1]) # type: ignore
|
||||||
|
receiver.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
sender.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
# Start transmisson
|
||||||
|
threading.Thread(target=flood_tx, args=(sender,)).start()
|
||||||
|
|
||||||
|
# Receive as much as we can in a few second time period
|
||||||
|
rx: list[Any] = []
|
||||||
|
old_len = 0
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < 3 or len(rx) > old_len:
|
||||||
|
old_len = len(rx)
|
||||||
|
print(old_len)
|
||||||
|
rx.extend(receiver.can_recv())
|
||||||
|
print(f"Received {len(rx)} messages")
|
||||||
36
panda/tests/can_printer.py
Normal file
36
panda/tests/can_printer.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
# fake
|
||||||
|
def sec_since_boot():
|
||||||
|
return time.time()
|
||||||
|
|
||||||
|
def can_printer():
|
||||||
|
p = Panda()
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
start = sec_since_boot()
|
||||||
|
lp = sec_since_boot()
|
||||||
|
msgs = defaultdict(list)
|
||||||
|
canbus = int(os.getenv("CAN", "0"))
|
||||||
|
while True:
|
||||||
|
can_recv = p.can_recv()
|
||||||
|
for address, _, dat, src in can_recv:
|
||||||
|
if src == canbus:
|
||||||
|
msgs[address].append(dat)
|
||||||
|
|
||||||
|
if sec_since_boot() - lp > 0.1:
|
||||||
|
dd = chr(27) + "[2J"
|
||||||
|
dd += "%5.2f\n" % (sec_since_boot() - start)
|
||||||
|
for k, v in sorted(zip(list(msgs.keys()), [binascii.hexlify(x[-1]) for x in list(msgs.values())], strict=True)):
|
||||||
|
dd += "%s(%6d) %s\n" % ("%04X(%4d)" % (k, k), len(msgs[k]), v)
|
||||||
|
print(dd)
|
||||||
|
lp = sec_since_boot()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
can_printer()
|
||||||
152
panda/tests/canfd/test_canfd.py
Normal file
152
panda/tests/canfd/test_canfd.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
from collections import defaultdict
|
||||||
|
from panda import Panda, calculate_checksum, DLC_TO_LEN
|
||||||
|
from panda import PandaJungle
|
||||||
|
from panda.tests.hitl.helpers import time_many_sends
|
||||||
|
|
||||||
|
H7_HW_TYPES = [Panda.HW_TYPE_RED_PANDA, Panda.HW_TYPE_RED_PANDA_V2]
|
||||||
|
JUNGLE_SERIAL = os.getenv("JUNGLE")
|
||||||
|
H7_PANDAS_EXCLUDE = [] # type: ignore
|
||||||
|
if os.getenv("H7_PANDAS_EXCLUDE"):
|
||||||
|
H7_PANDAS_EXCLUDE = os.getenv("H7_PANDAS_EXCLUDE").strip().split(" ") # type: ignore
|
||||||
|
|
||||||
|
def panda_reset():
|
||||||
|
panda_serials = []
|
||||||
|
|
||||||
|
panda_jungle = PandaJungle(JUNGLE_SERIAL)
|
||||||
|
panda_jungle.set_can_silent(True)
|
||||||
|
panda_jungle.set_panda_power(False)
|
||||||
|
time.sleep(1)
|
||||||
|
panda_jungle.set_panda_power(True)
|
||||||
|
time.sleep(4)
|
||||||
|
|
||||||
|
for serial in Panda.list():
|
||||||
|
if serial not in H7_PANDAS_EXCLUDE:
|
||||||
|
with Panda(serial=serial) as p:
|
||||||
|
if p.get_type() in H7_HW_TYPES:
|
||||||
|
p.reset()
|
||||||
|
panda_serials.append(serial)
|
||||||
|
|
||||||
|
print("test pandas", panda_serials)
|
||||||
|
assert len(panda_serials) == 2, "Two H7 pandas required"
|
||||||
|
|
||||||
|
return panda_serials
|
||||||
|
|
||||||
|
def panda_init(serial, enable_canfd=False, enable_non_iso=False):
|
||||||
|
p = Panda(serial=serial)
|
||||||
|
p.set_power_save(False)
|
||||||
|
for bus in range(3):
|
||||||
|
p.set_can_speed_kbps(0, 500)
|
||||||
|
if enable_canfd:
|
||||||
|
p.set_can_data_speed_kbps(bus, 2000)
|
||||||
|
if enable_non_iso:
|
||||||
|
p.set_canfd_non_iso(bus, True)
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
return p
|
||||||
|
|
||||||
|
def test_canfd_throughput(p, p_recv=None):
|
||||||
|
two_pandas = p_recv is not None
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
if two_pandas:
|
||||||
|
p_recv.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
# enable output mode
|
||||||
|
else:
|
||||||
|
p.set_can_loopback(True)
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
[500, 1000, 2000], # speeds
|
||||||
|
[93, 87, 78], # saturation thresholds
|
||||||
|
]
|
||||||
|
|
||||||
|
for i in range(len(tests[0])):
|
||||||
|
# set bus 0 data speed to speed
|
||||||
|
p.set_can_data_speed_kbps(0, tests[0][i])
|
||||||
|
if p_recv is not None:
|
||||||
|
p_recv.set_can_data_speed_kbps(0, tests[0][i])
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
comp_kbps = time_many_sends(p, 0, p_recv=p_recv, msg_count=400, two_pandas=two_pandas, msg_len=64)
|
||||||
|
|
||||||
|
# bit count from https://en.wikipedia.org/wiki/CAN_bus
|
||||||
|
saturation_pct = (comp_kbps / tests[0][i]) * 100.0
|
||||||
|
assert saturation_pct > tests[1][i]
|
||||||
|
assert saturation_pct < 100
|
||||||
|
|
||||||
|
def canfd_test(p_send, p_recv):
|
||||||
|
for n in range(100):
|
||||||
|
sent_msgs = defaultdict(set)
|
||||||
|
to_send = []
|
||||||
|
for _ in range(200):
|
||||||
|
bus = random.randrange(3)
|
||||||
|
for dlc in range(len(DLC_TO_LEN)):
|
||||||
|
address = random.randrange(1, 1<<29)
|
||||||
|
data = bytearray(random.getrandbits(8) for _ in range(DLC_TO_LEN[dlc]))
|
||||||
|
if len(data) >= 2:
|
||||||
|
data[0] = calculate_checksum(data[1:] + bytes(str(address), encoding="utf-8"))
|
||||||
|
to_send.append([address, 0, data, bus])
|
||||||
|
sent_msgs[bus].add((address, bytes(data)))
|
||||||
|
|
||||||
|
p_send.can_send_many(to_send, timeout=0)
|
||||||
|
|
||||||
|
start_time = time.monotonic()
|
||||||
|
while (time.monotonic() - start_time < 1) and any(len(x) > 0 for x in sent_msgs.values()):
|
||||||
|
incoming = p_recv.can_recv()
|
||||||
|
for msg in incoming:
|
||||||
|
address, _, data, bus = msg
|
||||||
|
if len(data) >= 2:
|
||||||
|
assert calculate_checksum(data[1:] + bytes(str(address), encoding="utf-8")) == data[0]
|
||||||
|
k = (address, bytes(data))
|
||||||
|
assert k in sent_msgs[bus], f"message {k} was never sent on bus {bus}"
|
||||||
|
sent_msgs[bus].discard(k)
|
||||||
|
|
||||||
|
for bus in range(3):
|
||||||
|
assert not len(sent_msgs[bus]), f"loop {n}: bus {bus} missing {len(sent_msgs[bus])} messages"
|
||||||
|
|
||||||
|
def setup_test(enable_non_iso=False):
|
||||||
|
panda_serials = panda_reset()
|
||||||
|
|
||||||
|
p_send = panda_init(panda_serials[0], enable_canfd=False, enable_non_iso=enable_non_iso)
|
||||||
|
p_recv = panda_init(panda_serials[1], enable_canfd=True, enable_non_iso=enable_non_iso)
|
||||||
|
|
||||||
|
# Check that sending panda CAN FD and BRS are turned off
|
||||||
|
for bus in range(3):
|
||||||
|
health = p_send.can_health(bus)
|
||||||
|
assert not health["canfd_enabled"]
|
||||||
|
assert not health["brs_enabled"]
|
||||||
|
assert health["canfd_non_iso"] == enable_non_iso
|
||||||
|
|
||||||
|
# Receiving panda sends dummy CAN FD message that should enable CAN FD on sender side
|
||||||
|
for bus in range(3):
|
||||||
|
p_recv.can_send(0x200, b"dummymessage", bus)
|
||||||
|
p_recv.can_recv()
|
||||||
|
p_send.can_recv()
|
||||||
|
|
||||||
|
# Check if all tested buses on sending panda have swithed to CAN FD with BRS
|
||||||
|
for bus in range(3):
|
||||||
|
health = p_send.can_health(bus)
|
||||||
|
assert health["canfd_enabled"]
|
||||||
|
assert health["brs_enabled"]
|
||||||
|
assert health["canfd_non_iso"] == enable_non_iso
|
||||||
|
|
||||||
|
return p_send, p_recv
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("[TEST CAN-FD]")
|
||||||
|
p_send, p_recv = setup_test()
|
||||||
|
canfd_test(p_send, p_recv)
|
||||||
|
|
||||||
|
print("[TEST CAN-FD non-ISO]")
|
||||||
|
p_send, p_recv = setup_test(enable_non_iso=True)
|
||||||
|
canfd_test(p_send, p_recv)
|
||||||
|
|
||||||
|
print("[TEST CAN-FD THROUGHPUT]")
|
||||||
|
panda_serials = panda_reset()
|
||||||
|
p_send = panda_init(panda_serials[0], enable_canfd=True)
|
||||||
|
p_recv = panda_init(panda_serials[1], enable_canfd=True)
|
||||||
|
test_canfd_throughput(p_send, p_recv)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
97
panda/tests/check_fw_size.py
Normal file
97
panda/tests/check_fw_size.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import subprocess
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
def check_space(file, mcu):
|
||||||
|
MCUS = {
|
||||||
|
"H7": {
|
||||||
|
".flash": 1024*1024, # FLASH
|
||||||
|
".dtcmram": 128*1024, # DTCMRAM
|
||||||
|
".itcmram": 64*1024, # ITCMRAM
|
||||||
|
".axisram": 320*1024, # AXI SRAM
|
||||||
|
".sram12": 32*1024, # SRAM1(16kb) + SRAM2(16kb)
|
||||||
|
".sram4": 16*1024, # SRAM4
|
||||||
|
".backup_sram": 4*1024, # SRAM4
|
||||||
|
},
|
||||||
|
"F4": {
|
||||||
|
".flash": 1024*1024, # FLASH
|
||||||
|
".dtcmram": 256*1024, # RAM
|
||||||
|
".ram_d1": 64*1024, # RAM2
|
||||||
|
},
|
||||||
|
}
|
||||||
|
IGNORE_LIST = [
|
||||||
|
".ARM.attributes",
|
||||||
|
".comment",
|
||||||
|
".debug_line",
|
||||||
|
".debug_info",
|
||||||
|
".debug_abbrev",
|
||||||
|
".debug_aranges",
|
||||||
|
".debug_str",
|
||||||
|
".debug_ranges",
|
||||||
|
".debug_loc",
|
||||||
|
".debug_frame",
|
||||||
|
".debug_line_str",
|
||||||
|
".debug_rnglists",
|
||||||
|
".debug_loclists",
|
||||||
|
]
|
||||||
|
FLASH = [
|
||||||
|
".isr_vector",
|
||||||
|
".text",
|
||||||
|
".rodata",
|
||||||
|
".data"
|
||||||
|
]
|
||||||
|
RAM = [
|
||||||
|
".data",
|
||||||
|
".bss",
|
||||||
|
"._user_heap_stack" # _user_heap_stack considered free?
|
||||||
|
]
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
calcs = defaultdict(int)
|
||||||
|
|
||||||
|
output = str(subprocess.check_output(f"arm-none-eabi-size -x --format=sysv {file}", shell=True), 'utf-8')
|
||||||
|
|
||||||
|
for row in output.split('\n'):
|
||||||
|
pop = False
|
||||||
|
line = row.split()
|
||||||
|
if len(line) == 3 and line[0].startswith('.'):
|
||||||
|
if line[0] in IGNORE_LIST:
|
||||||
|
continue
|
||||||
|
result[line[0]] = [line[1], line[2]]
|
||||||
|
if line[0] in FLASH:
|
||||||
|
calcs[".flash"] += int(line[1], 16)
|
||||||
|
pop = True
|
||||||
|
if line[0] in RAM:
|
||||||
|
calcs[".dtcmram"] += int(line[1], 16)
|
||||||
|
pop = True
|
||||||
|
if pop:
|
||||||
|
result.pop(line[0])
|
||||||
|
|
||||||
|
if len(result):
|
||||||
|
for line in result:
|
||||||
|
calcs[line] += int(result[line][0], 16)
|
||||||
|
|
||||||
|
print(f"=======SUMMARY FOR {mcu} FILE {file}=======")
|
||||||
|
for line in calcs:
|
||||||
|
if line in MCUS[mcu]:
|
||||||
|
used_percent = (100 - (MCUS[mcu][line] - calcs[line]) / MCUS[mcu][line] * 100)
|
||||||
|
print(f"SECTION: {line} size: {MCUS[mcu][line]} USED: {calcs[line]}({used_percent:.2f}%) FREE: {MCUS[mcu][line] - calcs[line]}")
|
||||||
|
else:
|
||||||
|
print(line, calcs[line])
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# red panda
|
||||||
|
check_space("../board/obj/bootstub.panda_h7.elf", "H7")
|
||||||
|
check_space("../board/obj/panda_h7.elf", "H7")
|
||||||
|
# black panda
|
||||||
|
check_space("../board/obj/bootstub.panda.elf", "F4")
|
||||||
|
check_space("../board/obj/panda.elf", "F4")
|
||||||
|
# jungle v1
|
||||||
|
check_space("../board/jungle/obj/bootstub.panda_jungle.elf", "F4")
|
||||||
|
check_space("../board/jungle/obj/panda_jungle.elf", "F4")
|
||||||
|
# jungle v2
|
||||||
|
check_space("../board/jungle/obj/bootstub.panda_jungle_h7.elf", "H7")
|
||||||
|
check_space("../board/jungle/obj/panda_jungle_h7.elf", "H7")
|
||||||
20
panda/tests/ci_shell.sh
Normal file
20
panda/tests/ci_shell.sh
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||||
|
OP_ROOT="$DIR/../../"
|
||||||
|
PANDA_ROOT="$DIR/../"
|
||||||
|
|
||||||
|
if [ -z "$BUILD" ]; then
|
||||||
|
docker pull docker.io/commaai/panda:latest
|
||||||
|
else
|
||||||
|
docker build --cache-from docker.io/commaai/panda:latest -t docker.io/commaai/panda:latest -f $PANDA_ROOT/Dockerfile $PANDA_ROOT
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run \
|
||||||
|
-it \
|
||||||
|
--rm \
|
||||||
|
--volume $OP_ROOT:$OP_ROOT \
|
||||||
|
--workdir $PWD \
|
||||||
|
--env PYTHONPATH=$OP_ROOT \
|
||||||
|
docker.io/commaai/panda:latest \
|
||||||
|
/bin/bash
|
||||||
62
panda/tests/debug_console.py
Normal file
62
panda/tests/debug_console.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import select
|
||||||
|
import codecs
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
setcolor = ["\033[1;32;40m", "\033[1;31;40m"]
|
||||||
|
unsetcolor = "\033[00m"
|
||||||
|
|
||||||
|
port_number = int(os.getenv("PORT", "0"))
|
||||||
|
claim = os.getenv("CLAIM") is not None
|
||||||
|
no_color = os.getenv("NO_COLOR") is not None
|
||||||
|
no_reconnect = os.getenv("NO_RECONNECT") is not None
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
serials = Panda.list()
|
||||||
|
if os.getenv("SERIAL"):
|
||||||
|
serials = [x for x in serials if x == os.getenv("SERIAL")]
|
||||||
|
|
||||||
|
pandas = [Panda(x, claim=claim) for x in serials]
|
||||||
|
decoders = [codecs.getincrementaldecoder('utf-8')() for _ in pandas]
|
||||||
|
|
||||||
|
if not len(pandas):
|
||||||
|
print("no pandas found")
|
||||||
|
if no_reconnect:
|
||||||
|
sys.exit(0)
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if os.getenv("BAUD") is not None:
|
||||||
|
for panda in pandas:
|
||||||
|
panda.set_uart_baud(port_number, int(os.getenv("BAUD"))) # type: ignore
|
||||||
|
|
||||||
|
while True:
|
||||||
|
for i, panda in enumerate(pandas):
|
||||||
|
while True:
|
||||||
|
ret = panda.serial_read(port_number)
|
||||||
|
if len(ret) > 0:
|
||||||
|
decoded = decoders[i].decode(ret)
|
||||||
|
if no_color:
|
||||||
|
sys.stdout.write(decoded)
|
||||||
|
else:
|
||||||
|
sys.stdout.write(setcolor[i] + decoded + unsetcolor)
|
||||||
|
sys.stdout.flush()
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
|
||||||
|
ln = sys.stdin.readline()
|
||||||
|
if claim:
|
||||||
|
panda.serial_write(port_number, ln)
|
||||||
|
time.sleep(0.01)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
print("panda disconnected!")
|
||||||
|
time.sleep(0.5)
|
||||||
50
panda/tests/development/register_hashmap_spread.py
Normal file
50
panda/tests/development/register_hashmap_spread.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import matplotlib.pyplot as plt # pylint: disable=import-error
|
||||||
|
|
||||||
|
HASHING_PRIME = 23
|
||||||
|
REGISTER_MAP_SIZE = 0x3FF
|
||||||
|
BYTES_PER_REG = 4
|
||||||
|
|
||||||
|
# From ST32F413 datasheet
|
||||||
|
REGISTER_ADDRESS_REGIONS = [
|
||||||
|
(0x40000000, 0x40007FFF),
|
||||||
|
(0x40010000, 0x400107FF),
|
||||||
|
(0x40011000, 0x400123FF),
|
||||||
|
(0x40012C00, 0x40014BFF),
|
||||||
|
(0x40015000, 0x400153FF),
|
||||||
|
(0x40015800, 0x40015BFF),
|
||||||
|
(0x40016000, 0x400167FF),
|
||||||
|
(0x40020000, 0x40021FFF),
|
||||||
|
(0x40023000, 0x400233FF),
|
||||||
|
(0x40023800, 0x40023FFF),
|
||||||
|
(0x40026000, 0x400267FF),
|
||||||
|
(0x50000000, 0x5003FFFF),
|
||||||
|
(0x50060000, 0x500603FF),
|
||||||
|
(0x50060800, 0x50060BFF),
|
||||||
|
(0x50060800, 0x50060BFF),
|
||||||
|
(0xE0000000, 0xE00FFFFF)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _hash(reg_addr):
|
||||||
|
return (((reg_addr >> 16) ^ ((((reg_addr + 1) & 0xFFFF) * HASHING_PRIME) & 0xFFFF)) & REGISTER_MAP_SIZE)
|
||||||
|
|
||||||
|
# Calculate hash for each address
|
||||||
|
hashes = []
|
||||||
|
double_hashes = []
|
||||||
|
for (start_addr, stop_addr) in REGISTER_ADDRESS_REGIONS:
|
||||||
|
for addr in range(start_addr, stop_addr + 1, BYTES_PER_REG):
|
||||||
|
h = _hash(addr)
|
||||||
|
hashes.append(h)
|
||||||
|
double_hashes.append(_hash(h))
|
||||||
|
|
||||||
|
# Make histograms
|
||||||
|
plt.subplot(2, 1, 1)
|
||||||
|
plt.hist(hashes, bins=REGISTER_MAP_SIZE)
|
||||||
|
plt.title("Number of collisions per _hash")
|
||||||
|
plt.xlabel("Address")
|
||||||
|
|
||||||
|
plt.subplot(2, 1, 2)
|
||||||
|
plt.hist(double_hashes, bins=REGISTER_MAP_SIZE)
|
||||||
|
plt.title("Number of collisions per double _hash")
|
||||||
|
plt.xlabel("Address")
|
||||||
|
plt.show()
|
||||||
16
panda/tests/echo.py
Normal file
16
panda/tests/echo.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
# This script is intended to be used in conjunction with the echo_loopback_test.py test script from panda jungle.
|
||||||
|
# It sends a reversed response back for every message received containing b"test".
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = Panda()
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
p.set_power_save(False)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
incoming = p.can_recv()
|
||||||
|
for message in incoming:
|
||||||
|
address, notused, data, bus = message
|
||||||
|
if b'test' in data:
|
||||||
|
p.can_send(address, data[::-1], bus)
|
||||||
232
panda/tests/elm_car_simulator.py
Normal file
232
panda/tests/elm_car_simulator.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""Used to Reverse/Test ELM protocol auto detect and OBD message response without a car."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import struct
|
||||||
|
import binascii
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
def lin_checksum(dat):
|
||||||
|
return sum(dat) % 0x100
|
||||||
|
|
||||||
|
class ELMCarSimulator():
|
||||||
|
def __init__(self, sn, silent=False, can_kbaud=500,
|
||||||
|
can=True, can11b=True, can29b=True,
|
||||||
|
lin=True):
|
||||||
|
self.__p = Panda(sn if sn else Panda.list()[0])
|
||||||
|
self.__on = True
|
||||||
|
self.__stop = False
|
||||||
|
self.__silent = silent
|
||||||
|
|
||||||
|
self.__lin_timer = None
|
||||||
|
self.__lin_active = False
|
||||||
|
self.__lin_enable = lin
|
||||||
|
self.__lin_monitor_thread = threading.Thread(target=self.__lin_monitor)
|
||||||
|
|
||||||
|
self.__can_multipart_data = None
|
||||||
|
self.__can_kbaud = can_kbaud
|
||||||
|
self.__can_extra_noise_msgs = deque()
|
||||||
|
self.__can_enable = can
|
||||||
|
self.__can11b = can11b
|
||||||
|
self.__can29b = can29b
|
||||||
|
self.__can_monitor_thread = threading.Thread(target=self.__can_monitor)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def panda(self):
|
||||||
|
return self.__p
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self.__lin_timer:
|
||||||
|
self.__lin_timer.cancel()
|
||||||
|
self.__lin_timeout_handler()
|
||||||
|
|
||||||
|
self.__stop = True
|
||||||
|
|
||||||
|
def join(self):
|
||||||
|
if self.__lin_monitor_thread.is_alive():
|
||||||
|
self.__lin_monitor_thread.join()
|
||||||
|
if self.__can_monitor_thread.is_alive():
|
||||||
|
self.__can_monitor_thread.join()
|
||||||
|
if self.__p:
|
||||||
|
print("closing handle")
|
||||||
|
self.__p.close()
|
||||||
|
|
||||||
|
def set_enable(self, on):
|
||||||
|
self.__on = on
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.panda.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
if self.__lin_enable:
|
||||||
|
self.__lin_monitor_thread.start()
|
||||||
|
if self.__can_enable:
|
||||||
|
self.__can_monitor_thread.start()
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# CAN related functions #
|
||||||
|
#########################
|
||||||
|
|
||||||
|
def __can_monitor(self):
|
||||||
|
print("STARTING CAN THREAD")
|
||||||
|
self.panda.set_can_speed_kbps(0, self.__can_kbaud)
|
||||||
|
self.panda.can_recv() # Toss whatever was already there
|
||||||
|
|
||||||
|
while not self.__stop:
|
||||||
|
for address, ts, data, src in self.panda.can_recv():
|
||||||
|
if self.__on and src == 0 and len(data) == 8 and data[0] >= 2:
|
||||||
|
if not self.__silent:
|
||||||
|
print("Processing CAN message", src, hex(address), binascii.hexlify(data))
|
||||||
|
self.__can_process_msg(data[1], data[2], address, ts, data, src)
|
||||||
|
elif not self.__silent:
|
||||||
|
print("Rejecting CAN message", src, hex(address), binascii.hexlify(data))
|
||||||
|
|
||||||
|
def can_mode_11b(self):
|
||||||
|
self.__can11b = True
|
||||||
|
self.__can29b = False
|
||||||
|
|
||||||
|
def can_mode_29b(self):
|
||||||
|
self.__can11b = False
|
||||||
|
self.__can29b = True
|
||||||
|
|
||||||
|
def can_mode_11b_29b(self):
|
||||||
|
self.__can11b = True
|
||||||
|
self.__can29b = True
|
||||||
|
|
||||||
|
def change_can_baud(self, kbaud):
|
||||||
|
self.__can_kbaud = kbaud
|
||||||
|
self.panda.set_can_speed_kbps(0, self.__can_kbaud)
|
||||||
|
|
||||||
|
def can_add_extra_noise(self, noise_msg, addr=None):
|
||||||
|
self.__can_extra_noise_msgs.append((addr, noise_msg))
|
||||||
|
|
||||||
|
def _can_send(self, addr, msg):
|
||||||
|
if not self.__silent:
|
||||||
|
print(" CAN Reply (%x)" % addr, binascii.hexlify(msg))
|
||||||
|
self.panda.can_send(addr, msg + b'\x00' * (8 - len(msg)), 0)
|
||||||
|
if self.__can_extra_noise_msgs:
|
||||||
|
noise = self.__can_extra_noise_msgs.popleft()
|
||||||
|
self.panda.can_send(noise[0] if noise[0] is not None else addr,
|
||||||
|
noise[1] + b'\x00' * (8 - len(noise[1])), 0)
|
||||||
|
|
||||||
|
def _can_addr_matches(self, addr):
|
||||||
|
if self.__can11b and (addr == 0x7DF or (addr & 0x7F8) == 0x7E0):
|
||||||
|
return True
|
||||||
|
if self.__can29b and (addr == 0x18db33f1 or (addr & 0x1FFF00FF) == 0x18da00f1):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __can_process_msg(self, mode, pid, address, ts, data, src):
|
||||||
|
if not self.__silent:
|
||||||
|
print("CAN MSG", binascii.hexlify(data[1:1 + data[0]]),
|
||||||
|
"Addr:", hex(address), "Mode:", hex(mode)[2:].zfill(2),
|
||||||
|
"PID:", hex(pid)[2:].zfill(2), "canLen:", len(data),
|
||||||
|
binascii.hexlify(data))
|
||||||
|
|
||||||
|
if self._can_addr_matches(address) and len(data) == 8:
|
||||||
|
outmsg = None
|
||||||
|
if data[:3] == b'\x30\x00\x00' and len(self.__can_multipart_data):
|
||||||
|
if not self.__silent:
|
||||||
|
print("Request for more data")
|
||||||
|
outaddr = 0x7E8 if address == 0x7DF or address == 0x7E0 else 0x18DAF110
|
||||||
|
msgnum = 1
|
||||||
|
while(self.__can_multipart_data):
|
||||||
|
datalen = min(7, len(self.__can_multipart_data))
|
||||||
|
msgpiece = struct.pack("B", 0x20 | msgnum) + self.__can_multipart_data[:datalen]
|
||||||
|
self._can_send(outaddr, msgpiece)
|
||||||
|
self.__can_multipart_data = self.__can_multipart_data[7:]
|
||||||
|
msgnum = (msgnum + 1) % 0x10
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
else:
|
||||||
|
outmsg = self._process_obd(mode, pid)
|
||||||
|
|
||||||
|
if outmsg:
|
||||||
|
outaddr = 0x7E8 if address == 0x7DF or address == 0x7E0 else 0x18DAF110
|
||||||
|
|
||||||
|
if len(outmsg) <= 5:
|
||||||
|
self._can_send(outaddr,
|
||||||
|
struct.pack("BBB", len(outmsg) + 2, 0x40 | data[1], pid) + outmsg)
|
||||||
|
else:
|
||||||
|
first_msg_len = min(3, len(outmsg) % 7)
|
||||||
|
payload_len = len(outmsg) + 3
|
||||||
|
msgpiece = struct.pack("BBBBB", 0x10 | ((payload_len >> 8) & 0xF),
|
||||||
|
payload_len & 0xFF,
|
||||||
|
0x40 | data[1], pid, 1) + outmsg[:first_msg_len]
|
||||||
|
self._can_send(outaddr, msgpiece)
|
||||||
|
self.__can_multipart_data = outmsg[first_msg_len:]
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# General OBD functions #
|
||||||
|
#########################
|
||||||
|
|
||||||
|
def _process_obd(self, mode, pid):
|
||||||
|
if mode == 0x01: # Mode: Show current data
|
||||||
|
if pid == 0x00: # List supported things
|
||||||
|
return b"\xff\xff\xff\xfe" # b"\xBE\x1F\xB8\x10" #Bitfield, random features
|
||||||
|
elif pid == 0x01: # Monitor Status since DTC cleared
|
||||||
|
return b"\x00\x00\x00\x00" # Bitfield, random features
|
||||||
|
elif pid == 0x04: # Calculated engine load
|
||||||
|
return b"\x2f"
|
||||||
|
elif pid == 0x05: # Engine coolant temperature
|
||||||
|
return b"\x3c"
|
||||||
|
elif pid == 0x0B: # Intake manifold absolute pressure
|
||||||
|
return b"\x90"
|
||||||
|
elif pid == 0x0C: # Engine RPM
|
||||||
|
return b"\x1A\xF8"
|
||||||
|
elif pid == 0x0D: # Vehicle Speed
|
||||||
|
return b"\x53"
|
||||||
|
elif pid == 0x10: # MAF air flow rate
|
||||||
|
return b"\x01\xA0"
|
||||||
|
elif pid == 0x11: # Throttle Position
|
||||||
|
return b"\x90"
|
||||||
|
elif pid == 0x33: # Absolute Barometric Pressure
|
||||||
|
return b"\x90"
|
||||||
|
elif mode == 0x09: # Mode: Request vehicle information
|
||||||
|
if pid == 0x02: # Show VIN
|
||||||
|
return b"1D4GP00R55B123456"
|
||||||
|
if pid == 0xFC: # test long multi message. Ligned up for LIN responses
|
||||||
|
return b''.join(struct.pack(">BBH", 0xAA, 0xAA, num + 1) for num in range(80))
|
||||||
|
if pid == 0xFD: # test long multi message
|
||||||
|
parts = (b'\xAA\xAA\xAA' + struct.pack(">I", num) for num in range(80))
|
||||||
|
return b'\xAA\xAA\xAA' + b''.join(parts)
|
||||||
|
if pid == 0xFE: # test very long multi message
|
||||||
|
parts = (b'\xAA\xAA\xAA' + struct.pack(">I", num) for num in range(584))
|
||||||
|
return b'\xAA\xAA\xAA' + b''.join(parts) + b'\xAA'
|
||||||
|
if pid == 0xFF:
|
||||||
|
return b'\xAA\x00\x00' + \
|
||||||
|
b"".join((b'\xAA' * 5) + struct.pack(">H", num + 1) for num in range(584))
|
||||||
|
#return b"\xAA"*100#(0xFFF-3)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
serial = os.getenv("SERIAL") if os.getenv("SERIAL") else None
|
||||||
|
kbaud = int(os.getenv("CANKBAUD")) if os.getenv("CANKBAUD") else 500 # type: ignore
|
||||||
|
bitwidth = int(os.getenv("CANBITWIDTH")) if os.getenv("CANBITWIDTH") else 0 # type: ignore
|
||||||
|
canenable = bool(int(os.getenv("CANENABLE"))) if os.getenv("CANENABLE") else True # type: ignore
|
||||||
|
linenable = bool(int(os.getenv("LINENABLE"))) if os.getenv("LINENABLE") else True # type: ignore
|
||||||
|
sim = ELMCarSimulator(serial, can_kbaud=kbaud, can=canenable, lin=linenable)
|
||||||
|
if(bitwidth == 0):
|
||||||
|
sim.can_mode_11b_29b()
|
||||||
|
if(bitwidth == 11):
|
||||||
|
sim.can_mode_11b()
|
||||||
|
if(bitwidth == 29):
|
||||||
|
sim.can_mode_29b()
|
||||||
|
|
||||||
|
import signal
|
||||||
|
|
||||||
|
def signal_handler(signal, frame):
|
||||||
|
print('\nShutting down simulator')
|
||||||
|
sim.stop()
|
||||||
|
sim.join()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
|
sim.start()
|
||||||
|
|
||||||
|
signal.pause()
|
||||||
44
panda/tests/elm_throughput.py
Normal file
44
panda/tests/elm_throughput.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import select
|
||||||
|
|
||||||
|
class Reader(threading.Thread):
|
||||||
|
def __init__(self, s, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._s = s
|
||||||
|
self.__stop = False
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.__stop = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while not self.__stop:
|
||||||
|
s.recv(1000)
|
||||||
|
|
||||||
|
def read_or_fail(s):
|
||||||
|
ready = select.select([s], [], [], 4)
|
||||||
|
assert ready[0], "Socket did not receive data within the timeout duration."
|
||||||
|
return s.recv(1000)
|
||||||
|
|
||||||
|
def send_msg(s, msg):
|
||||||
|
s.send(msg)
|
||||||
|
res = b''
|
||||||
|
while not res.endswith(">"):
|
||||||
|
res += read_or_fail(s)
|
||||||
|
return res
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
s = socket.create_connection(("192.168.0.10", 35000))
|
||||||
|
send_msg(s, b"ATZ\r")
|
||||||
|
send_msg(s, b"ATL1\r")
|
||||||
|
print(send_msg(s, b"ATE0\r"))
|
||||||
|
print(send_msg(s, b"ATS0\r"))
|
||||||
|
print(send_msg(s, b"ATSP6\r"))
|
||||||
|
|
||||||
|
print("\nLOOP\n")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print(send_msg(s, b"0100\r"))
|
||||||
|
print(send_msg(s, b"010d\r"))
|
||||||
14
panda/tests/fan/fan_test.py
Normal file
14
panda/tests/fan/fan_test.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import time
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = Panda()
|
||||||
|
power = 0
|
||||||
|
while True:
|
||||||
|
p.set_fan_power(power)
|
||||||
|
time.sleep(5)
|
||||||
|
print("Power: ", power, "RPM:", str(p.get_fan_rpm()), "Expected:", int(6500 * power / 100))
|
||||||
|
power += 10
|
||||||
|
power %= 110
|
||||||
88
panda/tests/fan/fan_tuning.py
Normal file
88
panda/tests/fan/fan_tuning.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
def drain_serial(p):
|
||||||
|
ret = []
|
||||||
|
while True:
|
||||||
|
d = p.serial_read(0)
|
||||||
|
if len(d) == 0:
|
||||||
|
break
|
||||||
|
ret.append(d)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
fan_cmd = 0.
|
||||||
|
|
||||||
|
def logger(event):
|
||||||
|
# requires a build with DEBUG_FAN
|
||||||
|
with Panda(claim=False) as p, open('/tmp/fan_log', 'w') as f:
|
||||||
|
power = None
|
||||||
|
target_rpm = None
|
||||||
|
stall_count = None
|
||||||
|
rpm_fast = None
|
||||||
|
t = time.monotonic()
|
||||||
|
|
||||||
|
drain_serial(p)
|
||||||
|
while not event.is_set():
|
||||||
|
p.set_fan_power(fan_cmd)
|
||||||
|
|
||||||
|
for l in drain_serial(p)[::-1]:
|
||||||
|
ns = l.decode('utf8').strip().split(' ')
|
||||||
|
if len(ns) == 4:
|
||||||
|
target_rpm, rpm_fast, power, stall_count = (int(n, 16) for n in ns)
|
||||||
|
break
|
||||||
|
|
||||||
|
dat = {
|
||||||
|
't': time.monotonic() - t,
|
||||||
|
'cmd_power': fan_cmd,
|
||||||
|
'pwm_power': power,
|
||||||
|
'target_rpm': target_rpm,
|
||||||
|
'rpm_fast': rpm_fast,
|
||||||
|
'rpm': p.get_fan_rpm(),
|
||||||
|
'stall_counter': stall_count,
|
||||||
|
'total_stall_count': p.health()['fan_stall_count'],
|
||||||
|
}
|
||||||
|
f.write(json.dumps(dat) + '\n')
|
||||||
|
time.sleep(1/16.)
|
||||||
|
p.set_fan_power(0)
|
||||||
|
|
||||||
|
def get_overshoot_rpm(p, power):
|
||||||
|
global fan_cmd
|
||||||
|
|
||||||
|
# make sure the fan is stopped completely
|
||||||
|
fan_cmd = 0.
|
||||||
|
while p.get_fan_rpm() > 100:
|
||||||
|
time.sleep(0.1)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# set it to 30% power to mimic going onroad
|
||||||
|
fan_cmd = power
|
||||||
|
max_rpm = 0
|
||||||
|
max_power = 0
|
||||||
|
for _ in range(70):
|
||||||
|
max_rpm = max(max_rpm, p.get_fan_rpm())
|
||||||
|
max_power = max(max_power, p.health()['fan_power'])
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# tolerate 10% overshoot
|
||||||
|
expected_rpm = Panda.MAX_FAN_RPMs[bytes(p.get_type())] * power / 100
|
||||||
|
overshoot = (max_rpm / expected_rpm) - 1
|
||||||
|
|
||||||
|
return overshoot, max_rpm, max_power
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
event = threading.Event()
|
||||||
|
threading.Thread(target=logger, args=(event, )).start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
p = Panda()
|
||||||
|
for power in range(10, 101, 10):
|
||||||
|
overshoot, max_rpm, max_power = get_overshoot_rpm(p, power)
|
||||||
|
print(f"Fan power {power}%: overshoot {overshoot:.2%}, Max RPM {max_rpm}, Max power {max_power}%")
|
||||||
|
finally:
|
||||||
|
event.set()
|
||||||
7
panda/tests/get_version.py
Normal file
7
panda/tests/get_version.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for p in Panda.list():
|
||||||
|
pp = Panda(p)
|
||||||
|
print(f"{pp.get_serial()[0]}: {pp.get_version()}")
|
||||||
18
panda/tests/gmbitbang/recv.py
Normal file
18
panda/tests/gmbitbang/recv.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = Panda()
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
p.set_gmlan(bus=2)
|
||||||
|
#p.can_send(0xaaa, b"\x00\x00", bus=3)
|
||||||
|
last_add: int | None = None
|
||||||
|
while True:
|
||||||
|
ret = p.can_recv()
|
||||||
|
if len(ret) > 0:
|
||||||
|
add = ret[0][0]
|
||||||
|
if last_add is not None and add != last_add + 1:
|
||||||
|
print("MISS: ", last_add, add)
|
||||||
|
last_add = add
|
||||||
|
print(ret)
|
||||||
36
panda/tests/gmbitbang/rigol.py
Normal file
36
panda/tests/gmbitbang/rigol.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# pylint: skip-file
|
||||||
|
# type: ignore
|
||||||
|
import numpy as np
|
||||||
|
import visa
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
resources = visa.ResourceManager()
|
||||||
|
print(resources.list_resources())
|
||||||
|
|
||||||
|
scope = resources.open_resource('USB0::0x1AB1::0x04CE::DS1ZA184652242::INSTR', timeout=2000, chunk_size=1024000)
|
||||||
|
print(scope.query('*IDN?').strip())
|
||||||
|
|
||||||
|
#voltscale = scope.ask_for_values(':CHAN1:SCAL?')[0]
|
||||||
|
#voltoffset = scope.ask_for_values(":CHAN1:OFFS?")[0]
|
||||||
|
|
||||||
|
#scope.write(":STOP")
|
||||||
|
scope.write(":WAV:POIN:MODE RAW")
|
||||||
|
scope.write(":WAV:DATA? CHAN1")[10:]
|
||||||
|
rawdata = scope.read_raw()
|
||||||
|
data = np.frombuffer(rawdata, 'B')
|
||||||
|
print(data.shape)
|
||||||
|
|
||||||
|
s1 = data[0:650]
|
||||||
|
s2 = data[650:]
|
||||||
|
s1i = np.argmax(s1 > 100)
|
||||||
|
s2i = np.argmax(s2 > 100)
|
||||||
|
s1 = s1[s1i:]
|
||||||
|
s2 = s2[s2i:]
|
||||||
|
|
||||||
|
plt.plot(s1)
|
||||||
|
plt.plot(s2)
|
||||||
|
plt.show()
|
||||||
|
#data = (data - 130.0 - voltoffset/voltscale*25) / 25 * voltscale
|
||||||
|
|
||||||
|
print(data)
|
||||||
32
panda/tests/gmbitbang/test.py
Normal file
32
panda/tests/gmbitbang/test.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import time
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
p1 = Panda('380016000551363338383037')
|
||||||
|
p2 = Panda('430026000951363338383037')
|
||||||
|
|
||||||
|
# this is a test, no safety
|
||||||
|
p1.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
p2.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
# get versions
|
||||||
|
print(p1.get_version())
|
||||||
|
print(p2.get_version())
|
||||||
|
|
||||||
|
# this sets bus 2 to actually be GMLAN
|
||||||
|
p2.set_gmlan(bus=2)
|
||||||
|
|
||||||
|
# send w bitbang then without
|
||||||
|
#iden = 123
|
||||||
|
iden = 18000
|
||||||
|
#dat = "\x01\x02"
|
||||||
|
dat = "\x01\x02\x03\x04\x05\x06\x07\x08"
|
||||||
|
while 1:
|
||||||
|
iden += 1
|
||||||
|
p1.set_gmlan(bus=None)
|
||||||
|
p1.can_send(iden, dat, bus=3)
|
||||||
|
#p1.set_gmlan(bus=2)
|
||||||
|
#p1.can_send(iden, dat, bus=3)
|
||||||
|
time.sleep(0.01)
|
||||||
|
print(p2.can_recv())
|
||||||
|
#exit(0)
|
||||||
22
panda/tests/gmbitbang/test_one.py
Normal file
22
panda/tests/gmbitbang/test_one.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import time
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
p = Panda()
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
# hack anything on bus
|
||||||
|
p.set_gmlan(bus=2)
|
||||||
|
time.sleep(0.1)
|
||||||
|
while len(p.can_recv()) > 0:
|
||||||
|
print("clearing")
|
||||||
|
time.sleep(0.1)
|
||||||
|
print("cleared")
|
||||||
|
p.set_gmlan(bus=None)
|
||||||
|
|
||||||
|
iden = 18000
|
||||||
|
dat = "\x01\x02\x03\x04\x05\x06\x07\x08"
|
||||||
|
while 1:
|
||||||
|
iden += 1
|
||||||
|
p.can_send(iden, dat, bus=3)
|
||||||
|
time.sleep(0.01)
|
||||||
28
panda/tests/gmbitbang/test_packer.c
Normal file
28
panda/tests/gmbitbang/test_packer.c
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define CANPACKET_DATA_SIZE_MAX 8
|
||||||
|
#include "../../board/can_definitions.h"
|
||||||
|
|
||||||
|
#include "../../board/drivers/canbitbang.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
char out[300];
|
||||||
|
CANPacket_t to_bang = {0};
|
||||||
|
to_bang.addr = 20 << 18;
|
||||||
|
to_bang.data_len_code = 1;
|
||||||
|
to_bang.data[0] = 1;
|
||||||
|
|
||||||
|
int len = get_bit_message(out, &to_bang);
|
||||||
|
printf("T:");
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
printf("%d", out[i]);
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
printf("R:0000010010100000100010000010011110111010100111111111111111");
|
||||||
|
printf("\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
78
panda/tests/gmlan_harness_test.py
Normal file
78
panda/tests/gmlan_harness_test.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
WHITE_GMLAN_BUS = 3
|
||||||
|
OTHER_GMLAN_BUS = 1
|
||||||
|
|
||||||
|
def set_gmlan(p):
|
||||||
|
if p.get_type() == Panda.HW_TYPE_WHITE_PANDA:
|
||||||
|
p.set_gmlan(2)
|
||||||
|
else:
|
||||||
|
p.set_obd(True)
|
||||||
|
|
||||||
|
def set_speed_kbps(p, speed):
|
||||||
|
if p.get_type() == Panda.HW_TYPE_WHITE_PANDA:
|
||||||
|
p.set_can_speed_kbps(WHITE_GMLAN_BUS, speed)
|
||||||
|
else:
|
||||||
|
p.set_can_speed_kbps(OTHER_GMLAN_BUS, speed)
|
||||||
|
|
||||||
|
def send(p, id_, msg):
|
||||||
|
if p.get_type() == Panda.HW_TYPE_WHITE_PANDA:
|
||||||
|
p.can_send(id_, msg, WHITE_GMLAN_BUS)
|
||||||
|
else:
|
||||||
|
p.can_send(id_, msg, OTHER_GMLAN_BUS)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pl = Panda.list()
|
||||||
|
assert(len(pl) == 2)
|
||||||
|
p0 = Panda(pl[1])
|
||||||
|
p1 = Panda(pl[0])
|
||||||
|
|
||||||
|
p0.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
p1.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
print("0: ", p0.get_type())
|
||||||
|
print("1: ", p1.get_type())
|
||||||
|
|
||||||
|
set_gmlan(p0)
|
||||||
|
set_gmlan(p1)
|
||||||
|
|
||||||
|
p0.can_clear(0xFFFF)
|
||||||
|
p1.can_clear(0xFFFF)
|
||||||
|
|
||||||
|
try:
|
||||||
|
loops = 0
|
||||||
|
while True:
|
||||||
|
for speed in [33.3, 83.3]:
|
||||||
|
set_speed_kbps(p0, speed)
|
||||||
|
set_speed_kbps(p1, speed)
|
||||||
|
p0.can_clear(0xFFFF)
|
||||||
|
p1.can_clear(0xFFFF)
|
||||||
|
|
||||||
|
print(f"Speed: {speed}")
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
print("Send 1 -> 0")
|
||||||
|
send(p1, 1, b"1to0:" + bytes(str(loops%100), "utf-8"))
|
||||||
|
time.sleep(0.05)
|
||||||
|
rx = list(filter(lambda x: x[3] < 128, p0.can_recv()))
|
||||||
|
print(rx)
|
||||||
|
assert(len(rx) == 1)
|
||||||
|
|
||||||
|
print("Send 0 -> 1")
|
||||||
|
send(p0, 1, b"0to1:" + bytes(str(loops%100), "utf-8"))
|
||||||
|
time.sleep(0.05)
|
||||||
|
rx = list(filter(lambda x: x[3] < 128, p1.can_recv()))
|
||||||
|
print(rx)
|
||||||
|
assert(len(rx) == 1)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
loops += 1
|
||||||
|
print(f"Completed {loops} loops")
|
||||||
|
except Exception:
|
||||||
|
print("Test failed somehow. Did you power the black panda using the GMLAN harness?")
|
||||||
18
panda/tests/health_test.py
Normal file
18
panda/tests/health_test.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import time
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
i = 0
|
||||||
|
pi = 0
|
||||||
|
|
||||||
|
panda = Panda()
|
||||||
|
while True:
|
||||||
|
st = time.monotonic()
|
||||||
|
while time.monotonic() - st < 1:
|
||||||
|
panda.health()
|
||||||
|
i += 1
|
||||||
|
print(i, panda.health(), "\n")
|
||||||
|
print(f"Speed: {i - pi}Hz")
|
||||||
|
pi = i
|
||||||
|
|
||||||
103
panda/tests/hitl/1_program.py
Normal file
103
panda/tests/hitl/1_program.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from panda import Panda, PandaDFU, McuType, BASEDIR
|
||||||
|
|
||||||
|
|
||||||
|
def check_signature(p):
|
||||||
|
assert not p.bootstub, "Flashed firmware not booting. Stuck in bootstub."
|
||||||
|
assert p.up_to_date()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dfu(p):
|
||||||
|
app_mcu_type = p.get_mcu_type()
|
||||||
|
dfu_serial = p.get_dfu_serial()
|
||||||
|
|
||||||
|
p.reset(enter_bootstub=True)
|
||||||
|
p.reset(enter_bootloader=True)
|
||||||
|
assert Panda.wait_for_dfu(dfu_serial, timeout=19), "failed to enter DFU"
|
||||||
|
|
||||||
|
dfu = PandaDFU(dfu_serial)
|
||||||
|
assert dfu.get_mcu_type() == app_mcu_type
|
||||||
|
|
||||||
|
assert dfu_serial in PandaDFU.list()
|
||||||
|
|
||||||
|
dfu._handle.clear_status()
|
||||||
|
dfu.reset()
|
||||||
|
p.reconnect()
|
||||||
|
|
||||||
|
# TODO: make more comprehensive bootstub tests and run on a few production ones + current
|
||||||
|
# TODO: also test release-signed app
|
||||||
|
@pytest.mark.timeout(30)
|
||||||
|
def test_known_bootstub(p):
|
||||||
|
"""
|
||||||
|
Test that compiled app can work with known production bootstub
|
||||||
|
"""
|
||||||
|
known_bootstubs = {
|
||||||
|
# covers the two cases listed in Panda.connect
|
||||||
|
McuType.F4: [
|
||||||
|
# case A - no bcdDevice or panda type, has to assume F4
|
||||||
|
"bootstub_f4_first_dos_production.panda.bin",
|
||||||
|
|
||||||
|
# case B - just bcdDevice
|
||||||
|
"bootstub_f4_only_bcd.panda.bin",
|
||||||
|
],
|
||||||
|
McuType.H7: ["bootstub.panda_h7.bin"],
|
||||||
|
}
|
||||||
|
|
||||||
|
for kb in known_bootstubs[p.get_mcu_type()]:
|
||||||
|
app_ids = (p.get_mcu_type(), p.get_usb_serial())
|
||||||
|
assert None not in app_ids
|
||||||
|
|
||||||
|
p.reset(enter_bootstub=True)
|
||||||
|
p.reset(enter_bootloader=True)
|
||||||
|
|
||||||
|
dfu_serial = p.get_dfu_serial()
|
||||||
|
assert Panda.wait_for_dfu(dfu_serial, timeout=30)
|
||||||
|
|
||||||
|
dfu = PandaDFU(dfu_serial)
|
||||||
|
with open(os.path.join(BASEDIR, "tests/hitl/known_bootstub", kb), "rb") as f:
|
||||||
|
code = f.read()
|
||||||
|
|
||||||
|
dfu.program_bootstub(code)
|
||||||
|
dfu.reset()
|
||||||
|
|
||||||
|
p.connect(claim=False, wait=True)
|
||||||
|
|
||||||
|
# check for MCU or serial mismatch
|
||||||
|
with Panda(p._serial, claim=False) as np:
|
||||||
|
bootstub_ids = (np.get_mcu_type(), np.get_usb_serial())
|
||||||
|
assert app_ids == bootstub_ids
|
||||||
|
|
||||||
|
# ensure we can flash app and it jumps to app
|
||||||
|
p.flash()
|
||||||
|
check_signature(p)
|
||||||
|
assert not p.bootstub
|
||||||
|
|
||||||
|
@pytest.mark.timeout(25)
|
||||||
|
def test_recover(p):
|
||||||
|
assert p.recover(timeout=30)
|
||||||
|
check_signature(p)
|
||||||
|
|
||||||
|
@pytest.mark.timeout(25)
|
||||||
|
def test_flash(p):
|
||||||
|
# test flash from bootstub
|
||||||
|
serial = p._serial
|
||||||
|
assert serial is not None
|
||||||
|
p.reset(enter_bootstub=True)
|
||||||
|
p.close()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
with Panda(serial) as np:
|
||||||
|
assert np.bootstub
|
||||||
|
assert np._serial == serial
|
||||||
|
np.flash()
|
||||||
|
|
||||||
|
p.reconnect()
|
||||||
|
p.reset()
|
||||||
|
check_signature(p)
|
||||||
|
|
||||||
|
# test flash from app
|
||||||
|
p.flash()
|
||||||
|
check_signature(p)
|
||||||
125
panda/tests/hitl/2_health.py
Normal file
125
panda/tests/hitl/2_health.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
from panda import PandaJungle
|
||||||
|
from panda.tests.hitl.conftest import PandaGroup
|
||||||
|
|
||||||
|
|
||||||
|
def test_ignition(p, panda_jungle):
|
||||||
|
# Set harness orientation to #2, since the ignition line is on the wrong SBU bus :/
|
||||||
|
panda_jungle.set_harness_orientation(PandaJungle.HARNESS_ORIENTATION_2)
|
||||||
|
p.reset()
|
||||||
|
|
||||||
|
for ign in (True, False):
|
||||||
|
panda_jungle.set_ignition(ign)
|
||||||
|
time.sleep(0.1)
|
||||||
|
assert p.health()['ignition_line'] == ign
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.test_panda_types(PandaGroup.GEN2)
|
||||||
|
def test_harness_status(p, panda_jungle):
|
||||||
|
flipped = None
|
||||||
|
for ignition in [True, False]:
|
||||||
|
for orientation in [Panda.HARNESS_STATUS_NC, Panda.HARNESS_STATUS_NORMAL, Panda.HARNESS_STATUS_FLIPPED]:
|
||||||
|
panda_jungle.set_harness_orientation(orientation)
|
||||||
|
panda_jungle.set_ignition(ignition)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
health = p.health()
|
||||||
|
detected_orientation = health['car_harness_status']
|
||||||
|
print(f"set: {orientation} detected: {detected_orientation}")
|
||||||
|
|
||||||
|
# Orientation
|
||||||
|
if orientation == Panda.HARNESS_STATUS_NC:
|
||||||
|
assert detected_orientation == Panda.HARNESS_STATUS_NC
|
||||||
|
else:
|
||||||
|
if flipped is None:
|
||||||
|
flipped = (detected_orientation != orientation)
|
||||||
|
|
||||||
|
if orientation == Panda.HARNESS_STATUS_NORMAL:
|
||||||
|
assert detected_orientation == (Panda.HARNESS_STATUS_FLIPPED if flipped else Panda.HARNESS_STATUS_NORMAL)
|
||||||
|
else:
|
||||||
|
assert detected_orientation == (Panda.HARNESS_STATUS_NORMAL if flipped else Panda.HARNESS_STATUS_FLIPPED)
|
||||||
|
|
||||||
|
# Line ignition
|
||||||
|
assert health['ignition_line'] == (False if orientation == Panda.HARNESS_STATUS_NC else ignition)
|
||||||
|
|
||||||
|
# SBU voltages
|
||||||
|
supply_voltage_mV = 1800 if p.get_type() in [Panda.HW_TYPE_TRES, ] else 3300
|
||||||
|
|
||||||
|
if orientation == Panda.HARNESS_STATUS_NC:
|
||||||
|
assert health['sbu1_voltage_mV'] > 0.9 * supply_voltage_mV
|
||||||
|
assert health['sbu2_voltage_mV'] > 0.9 * supply_voltage_mV
|
||||||
|
else:
|
||||||
|
relay_line = 'sbu1_voltage_mV' if (detected_orientation == Panda.HARNESS_STATUS_FLIPPED) else 'sbu2_voltage_mV'
|
||||||
|
ignition_line = 'sbu2_voltage_mV' if (detected_orientation == Panda.HARNESS_STATUS_FLIPPED) else 'sbu1_voltage_mV'
|
||||||
|
|
||||||
|
assert health[relay_line] < 0.1 * supply_voltage_mV
|
||||||
|
assert health[ignition_line] > health[relay_line]
|
||||||
|
if ignition:
|
||||||
|
assert health[ignition_line] < 0.3 * supply_voltage_mV
|
||||||
|
else:
|
||||||
|
assert health[ignition_line] > 0.9 * supply_voltage_mV
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip_panda_types((Panda.HW_TYPE_DOS, ))
|
||||||
|
def test_voltage(p):
|
||||||
|
for _ in range(10):
|
||||||
|
voltage = p.health()['voltage']
|
||||||
|
assert ((voltage > 11000) and (voltage < 13000))
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
def test_hw_type(p):
|
||||||
|
"""
|
||||||
|
hw type should be same in bootstub as application
|
||||||
|
"""
|
||||||
|
|
||||||
|
hw_type = p.get_type()
|
||||||
|
mcu_type = p.get_mcu_type()
|
||||||
|
assert mcu_type is not None
|
||||||
|
|
||||||
|
app_uid = p.get_uid()
|
||||||
|
usb_serial = p.get_usb_serial()
|
||||||
|
assert app_uid == usb_serial
|
||||||
|
|
||||||
|
p.reset(enter_bootstub=True, reconnect=True)
|
||||||
|
p.close()
|
||||||
|
time.sleep(3)
|
||||||
|
with Panda(p.get_usb_serial()) as pp:
|
||||||
|
assert pp.bootstub
|
||||||
|
assert pp.get_type() == hw_type, "Bootstub and app hw type mismatch"
|
||||||
|
assert pp.get_mcu_type() == mcu_type, "Bootstub and app MCU type mismatch"
|
||||||
|
assert pp.get_uid() == app_uid
|
||||||
|
|
||||||
|
def test_heartbeat(p, panda_jungle):
|
||||||
|
panda_jungle.set_ignition(True)
|
||||||
|
# TODO: add more cases here once the tests aren't super slow
|
||||||
|
p.set_safety_mode(mode=Panda.SAFETY_HYUNDAI, param=Panda.FLAG_HYUNDAI_LONG)
|
||||||
|
p.send_heartbeat()
|
||||||
|
assert p.health()['safety_mode'] == Panda.SAFETY_HYUNDAI
|
||||||
|
assert p.health()['safety_param'] == Panda.FLAG_HYUNDAI_LONG
|
||||||
|
|
||||||
|
# shouldn't do anything once we're in a car safety mode
|
||||||
|
p.set_heartbeat_disabled()
|
||||||
|
|
||||||
|
time.sleep(6.)
|
||||||
|
|
||||||
|
h = p.health()
|
||||||
|
assert h['heartbeat_lost']
|
||||||
|
assert h['safety_mode'] == Panda.SAFETY_SILENT
|
||||||
|
assert h['safety_param'] == 0
|
||||||
|
assert h['controls_allowed'] == 0
|
||||||
|
|
||||||
|
def test_microsecond_timer(p):
|
||||||
|
start_time = p.get_microsecond_timer()
|
||||||
|
time.sleep(1)
|
||||||
|
end_time = p.get_microsecond_timer()
|
||||||
|
|
||||||
|
# account for uint32 overflow
|
||||||
|
if end_time < start_time:
|
||||||
|
end_time += 2**32
|
||||||
|
|
||||||
|
time_diff = (end_time - start_time) / 1e6
|
||||||
|
assert 0.98 < time_diff < 1.02, f"Timer not running at the correct speed! (got {time_diff:.2f}s instead of 1.0s)"
|
||||||
127
panda/tests/hitl/3_usb_to_can.py
Normal file
127
panda/tests/hitl/3_usb_to_can.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
from flaky import flaky
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.hitl.conftest import SPEED_NORMAL, SPEED_GMLAN, PandaGroup
|
||||||
|
from panda.tests.hitl.helpers import time_many_sends
|
||||||
|
|
||||||
|
def test_can_loopback(p):
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
p.set_can_loopback(True)
|
||||||
|
|
||||||
|
for bus in (0, 1, 2):
|
||||||
|
# set bus 0 speed to 5000
|
||||||
|
p.set_can_speed_kbps(bus, 500)
|
||||||
|
|
||||||
|
# send a message on bus 0
|
||||||
|
p.can_send(0x1aa, b"message", bus)
|
||||||
|
|
||||||
|
# confirm receive both on loopback and send receipt
|
||||||
|
time.sleep(0.05)
|
||||||
|
r = p.can_recv()
|
||||||
|
sr = [x for x in r if x[3] == 0x80 | bus]
|
||||||
|
lb = [x for x in r if x[3] == bus]
|
||||||
|
assert len(sr) == 1
|
||||||
|
assert len(lb) == 1
|
||||||
|
|
||||||
|
# confirm data is correct
|
||||||
|
assert 0x1aa == sr[0][0] == lb[0][0]
|
||||||
|
assert b"message" == sr[0][2] == lb[0][2]
|
||||||
|
|
||||||
|
def test_reliability(p):
|
||||||
|
MSG_COUNT = 100
|
||||||
|
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
p.set_can_loopback(True)
|
||||||
|
p.set_can_speed_kbps(0, 1000)
|
||||||
|
|
||||||
|
addrs = list(range(100, 100 + MSG_COUNT))
|
||||||
|
ts = [(j, 0, b"\xaa" * 8, 0) for j in addrs]
|
||||||
|
|
||||||
|
for _ in range(100):
|
||||||
|
st = time.monotonic()
|
||||||
|
|
||||||
|
p.can_send_many(ts)
|
||||||
|
|
||||||
|
r = []
|
||||||
|
while len(r) < 200 and (time.monotonic() - st) < 0.5:
|
||||||
|
r.extend(p.can_recv())
|
||||||
|
|
||||||
|
sent_echo = [x for x in r if x[3] == 0x80]
|
||||||
|
loopback_resp = [x for x in r if x[3] == 0]
|
||||||
|
|
||||||
|
assert sorted([x[0] for x in loopback_resp]) == addrs
|
||||||
|
assert sorted([x[0] for x in sent_echo]) == addrs
|
||||||
|
assert len(r) == 200
|
||||||
|
|
||||||
|
# take sub 20ms
|
||||||
|
et = (time.monotonic() - st) * 1000.0
|
||||||
|
assert et < 20
|
||||||
|
|
||||||
|
@flaky(max_runs=6, min_passes=1)
|
||||||
|
def test_throughput(p):
|
||||||
|
# enable output mode
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
# enable CAN loopback mode
|
||||||
|
p.set_can_loopback(True)
|
||||||
|
|
||||||
|
for speed in [10, 20, 50, 100, 125, 250, 500, 1000]:
|
||||||
|
# set bus 0 speed to speed
|
||||||
|
p.set_can_speed_kbps(0, speed)
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
comp_kbps = time_many_sends(p, 0)
|
||||||
|
|
||||||
|
# bit count from https://en.wikipedia.org/wiki/CAN_bus
|
||||||
|
saturation_pct = (comp_kbps / speed) * 100.0
|
||||||
|
assert saturation_pct > 80
|
||||||
|
assert saturation_pct < 100
|
||||||
|
|
||||||
|
print("loopback 100 messages at speed %d, comp speed is %.2f, percent %.2f" % (speed, comp_kbps, saturation_pct))
|
||||||
|
|
||||||
|
@pytest.mark.test_panda_types(PandaGroup.GMLAN)
|
||||||
|
def test_gmlan(p):
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
p.set_can_loopback(True)
|
||||||
|
|
||||||
|
# set gmlan on CAN2
|
||||||
|
for bus in [Panda.GMLAN_CAN2, Panda.GMLAN_CAN3, Panda.GMLAN_CAN2, Panda.GMLAN_CAN3]:
|
||||||
|
p.set_gmlan(bus)
|
||||||
|
comp_kbps_gmlan = time_many_sends(p, 3)
|
||||||
|
assert comp_kbps_gmlan > (0.8 * SPEED_GMLAN)
|
||||||
|
assert comp_kbps_gmlan < (1.0 * SPEED_GMLAN)
|
||||||
|
|
||||||
|
p.set_gmlan(None)
|
||||||
|
comp_kbps_normal = time_many_sends(p, bus)
|
||||||
|
assert comp_kbps_normal > (0.8 * SPEED_NORMAL)
|
||||||
|
assert comp_kbps_normal < (1.0 * SPEED_NORMAL)
|
||||||
|
|
||||||
|
print("%d: %.2f kbps vs %.2f kbps" % (bus, comp_kbps_gmlan, comp_kbps_normal))
|
||||||
|
|
||||||
|
@pytest.mark.test_panda_types(PandaGroup.GMLAN)
|
||||||
|
def test_gmlan_bad_toggle(p):
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
p.set_can_loopback(True)
|
||||||
|
|
||||||
|
# GMLAN_CAN2
|
||||||
|
for bus in [Panda.GMLAN_CAN2, Panda.GMLAN_CAN3]:
|
||||||
|
p.set_gmlan(bus)
|
||||||
|
comp_kbps_gmlan = time_many_sends(p, 3)
|
||||||
|
assert comp_kbps_gmlan > (0.6 * SPEED_GMLAN)
|
||||||
|
assert comp_kbps_gmlan < (1.0 * SPEED_GMLAN)
|
||||||
|
|
||||||
|
# normal
|
||||||
|
for bus in [Panda.GMLAN_CAN2, Panda.GMLAN_CAN3]:
|
||||||
|
p.set_gmlan(None)
|
||||||
|
comp_kbps_normal = time_many_sends(p, bus)
|
||||||
|
assert comp_kbps_normal > (0.6 * SPEED_NORMAL)
|
||||||
|
assert comp_kbps_normal < (1.0 * SPEED_NORMAL)
|
||||||
|
|
||||||
|
|
||||||
|
# this will fail if you have hardware serial connected
|
||||||
|
def test_serial_debug(p):
|
||||||
|
_ = p.serial_read(Panda.SERIAL_DEBUG) # junk
|
||||||
|
p.call_control_api(0x01)
|
||||||
|
assert p.serial_read(Panda.SERIAL_DEBUG).startswith(b"NO HANDLER")
|
||||||
202
panda/tests/hitl/4_can_loopback.py
Normal file
202
panda/tests/hitl/4_can_loopback.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
import random
|
||||||
|
import threading
|
||||||
|
from flaky import flaky
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.hitl.conftest import PandaGroup
|
||||||
|
from panda.tests.hitl.helpers import time_many_sends, get_random_can_messages, clear_can_buffers
|
||||||
|
|
||||||
|
@flaky(max_runs=3, min_passes=1)
|
||||||
|
@pytest.mark.timeout(35)
|
||||||
|
def test_send_recv(p, panda_jungle):
|
||||||
|
def test(p_send, p_recv):
|
||||||
|
for bus in (0, 1, 2):
|
||||||
|
for speed in (10, 20, 50, 100, 125, 250, 500, 1000):
|
||||||
|
clear_can_buffers(p_send, speed)
|
||||||
|
clear_can_buffers(p_recv, speed)
|
||||||
|
|
||||||
|
comp_kbps = time_many_sends(p_send, bus, p_recv, two_pandas=True)
|
||||||
|
|
||||||
|
saturation_pct = (comp_kbps / speed) * 100.0
|
||||||
|
assert 80 < saturation_pct < 100
|
||||||
|
|
||||||
|
print(f"two pandas bus {bus}, 100 messages at speed {speed:4d}, comp speed is {comp_kbps:7.2f}, {saturation_pct:6.2f}%")
|
||||||
|
|
||||||
|
# Run tests in both directions
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
test(p, panda_jungle)
|
||||||
|
test(panda_jungle, p)
|
||||||
|
|
||||||
|
|
||||||
|
@flaky(max_runs=6, min_passes=1)
|
||||||
|
@pytest.mark.timeout(30)
|
||||||
|
def test_latency(p, panda_jungle):
|
||||||
|
def test(p_send, p_recv):
|
||||||
|
for bus in (0, 1, 2):
|
||||||
|
for speed in (10, 20, 50, 100, 125, 250, 500, 1000):
|
||||||
|
clear_can_buffers(p_send, speed)
|
||||||
|
clear_can_buffers(p_recv, speed)
|
||||||
|
|
||||||
|
latencies = []
|
||||||
|
comp_kbps_list = []
|
||||||
|
saturation_pcts = []
|
||||||
|
|
||||||
|
num_messages = 100
|
||||||
|
|
||||||
|
for _ in range(num_messages):
|
||||||
|
st = time.monotonic()
|
||||||
|
p_send.can_send(0x1ab, b"message", bus)
|
||||||
|
r = []
|
||||||
|
while len(r) < 1 and (time.monotonic() - st) < 5:
|
||||||
|
r = p_recv.can_recv()
|
||||||
|
et = time.monotonic()
|
||||||
|
r_echo = []
|
||||||
|
while len(r_echo) < 1 and (time.monotonic() - st) < 10:
|
||||||
|
r_echo = p_send.can_recv()
|
||||||
|
|
||||||
|
if len(r) == 0 or len(r_echo) == 0:
|
||||||
|
print(f"r: {r}, r_echo: {r_echo}")
|
||||||
|
|
||||||
|
assert len(r) == 1
|
||||||
|
assert len(r_echo) == 1
|
||||||
|
|
||||||
|
et = (et - st) * 1000.0
|
||||||
|
comp_kbps = (1 + 11 + 1 + 1 + 1 + 4 + 8 * 8 + 15 + 1 + 1 + 1 + 7) / et
|
||||||
|
latency = et - ((1 + 11 + 1 + 1 + 1 + 4 + 8 * 8 + 15 + 1 + 1 + 1 + 7) / speed)
|
||||||
|
|
||||||
|
assert latency < 5.0
|
||||||
|
|
||||||
|
saturation_pct = (comp_kbps / speed) * 100.0
|
||||||
|
latencies.append(latency)
|
||||||
|
comp_kbps_list.append(comp_kbps)
|
||||||
|
saturation_pcts.append(saturation_pct)
|
||||||
|
|
||||||
|
average_latency = sum(latencies) / num_messages
|
||||||
|
assert average_latency < 1.0
|
||||||
|
average_comp_kbps = sum(comp_kbps_list) / num_messages
|
||||||
|
average_saturation_pct = sum(saturation_pcts) / num_messages
|
||||||
|
|
||||||
|
print("two pandas bus {}, {} message average at speed {:4d}, latency is {:5.3f}ms, comp speed is {:7.2f}, percent {:6.2f}"
|
||||||
|
.format(bus, num_messages, speed, average_latency, average_comp_kbps, average_saturation_pct))
|
||||||
|
|
||||||
|
# Run tests in both directions
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
test(p, panda_jungle)
|
||||||
|
test(panda_jungle, p)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.panda_expect_can_error
|
||||||
|
@pytest.mark.test_panda_types(PandaGroup.GEN2)
|
||||||
|
def test_gen2_loopback(p, panda_jungle):
|
||||||
|
def test(p_send, p_recv, address=None):
|
||||||
|
for bus in range(4):
|
||||||
|
obd = False
|
||||||
|
if bus == 3:
|
||||||
|
obd = True
|
||||||
|
bus = 1
|
||||||
|
|
||||||
|
# Clear buses
|
||||||
|
clear_can_buffers(p_send)
|
||||||
|
clear_can_buffers(p_recv)
|
||||||
|
|
||||||
|
# Send a random string
|
||||||
|
addr = address if address else random.randint(1, 2000)
|
||||||
|
string = b"test" + os.urandom(4)
|
||||||
|
p_send.set_obd(obd)
|
||||||
|
p_recv.set_obd(obd)
|
||||||
|
time.sleep(0.2)
|
||||||
|
p_send.can_send(addr, string, bus)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
content = p_recv.can_recv()
|
||||||
|
|
||||||
|
# Check amount of messages
|
||||||
|
assert len(content) == 1
|
||||||
|
|
||||||
|
# Check content
|
||||||
|
assert content[0][0] == addr and content[0][2] == string
|
||||||
|
|
||||||
|
# Check bus
|
||||||
|
assert content[0][3] == bus
|
||||||
|
|
||||||
|
print("Bus:", bus, "address:", addr, "OBD:", obd, "OK")
|
||||||
|
|
||||||
|
# Run tests in both directions
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
test(p, panda_jungle)
|
||||||
|
test(panda_jungle, p)
|
||||||
|
|
||||||
|
# Test extended frame address with ELM327 mode
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ELM327)
|
||||||
|
test(p, panda_jungle, 0x18DB33F1)
|
||||||
|
test(panda_jungle, p, 0x18DB33F1)
|
||||||
|
|
||||||
|
# TODO: why it's not being reset by fixtures reinit?
|
||||||
|
p.set_obd(False)
|
||||||
|
panda_jungle.set_obd(False)
|
||||||
|
|
||||||
|
def test_bulk_write(p, panda_jungle):
|
||||||
|
# The TX buffers on pandas is 0x100 in length.
|
||||||
|
NUM_MESSAGES_PER_BUS = 10000
|
||||||
|
|
||||||
|
def flood_tx(panda):
|
||||||
|
print('Sending!')
|
||||||
|
msg = b"\xaa" * 8
|
||||||
|
packet = []
|
||||||
|
# start with many messages on a single bus (higher contention for single TX ring buffer)
|
||||||
|
packet += [[0xaa, None, msg, 0]] * NUM_MESSAGES_PER_BUS
|
||||||
|
# end with many messages on multiple buses
|
||||||
|
packet += [[0xaa, None, msg, 0], [0xaa, None, msg, 1], [0xaa, None, msg, 2]] * NUM_MESSAGES_PER_BUS
|
||||||
|
|
||||||
|
# Disable timeout
|
||||||
|
panda.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
panda.can_send_many(packet, timeout=0)
|
||||||
|
print(f"Done sending {4 * NUM_MESSAGES_PER_BUS} messages!", time.monotonic())
|
||||||
|
print(panda.health())
|
||||||
|
|
||||||
|
# Start transmisson
|
||||||
|
threading.Thread(target=flood_tx, args=(p,)).start()
|
||||||
|
|
||||||
|
# Receive as much as we can in a few second time period
|
||||||
|
rx = []
|
||||||
|
old_len = 0
|
||||||
|
start_time = time.monotonic()
|
||||||
|
while time.monotonic() - start_time < 5 or len(rx) > old_len:
|
||||||
|
old_len = len(rx)
|
||||||
|
rx.extend(panda_jungle.can_recv())
|
||||||
|
print(f"Received {len(rx)} messages", time.monotonic())
|
||||||
|
|
||||||
|
# All messages should have been received
|
||||||
|
if len(rx) != 4 * NUM_MESSAGES_PER_BUS:
|
||||||
|
raise Exception("Did not receive all messages!")
|
||||||
|
|
||||||
|
def test_message_integrity(p):
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
p.set_can_loopback(True)
|
||||||
|
for i in range(250):
|
||||||
|
sent_msgs = defaultdict(set)
|
||||||
|
for _ in range(random.randrange(10)):
|
||||||
|
to_send = get_random_can_messages(random.randrange(100))
|
||||||
|
for m in to_send:
|
||||||
|
sent_msgs[m[3]].add((m[0], m[2]))
|
||||||
|
p.can_send_many(to_send, timeout=0)
|
||||||
|
|
||||||
|
start_time = time.monotonic()
|
||||||
|
while time.monotonic() - start_time < 2 and any(len(sent_msgs[bus]) for bus in range(3)):
|
||||||
|
recvd = p.can_recv()
|
||||||
|
for msg in recvd:
|
||||||
|
if msg[3] >= 128:
|
||||||
|
k = (msg[0], bytes(msg[2]))
|
||||||
|
bus = msg[3]-128
|
||||||
|
assert k in sent_msgs[bus], f"message {k} was never sent on bus {bus}"
|
||||||
|
sent_msgs[msg[3]-128].discard(k)
|
||||||
|
|
||||||
|
# if a set isn't empty, messages got dropped
|
||||||
|
for bus in range(3):
|
||||||
|
assert not len(sent_msgs[bus]), f"loop {i}: bus {bus} missing {len(sent_msgs[bus])} messages"
|
||||||
|
|
||||||
|
print("Got all messages intact")
|
||||||
103
panda/tests/hitl/5_spi.py
Normal file
103
panda/tests/hitl/5_spi.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import binascii
|
||||||
|
import pytest
|
||||||
|
import random
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from panda import Panda, PandaDFU
|
||||||
|
from panda.python.spi import SpiDevice, PandaProtocolMismatch, PandaSpiNackResponse
|
||||||
|
|
||||||
|
pytestmark = [
|
||||||
|
pytest.mark.test_panda_types((Panda.HW_TYPE_TRES, ))
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.skip("doesn't work, bootloader seems to ignore commands once it sees junk")
|
||||||
|
def test_dfu_with_spam(p):
|
||||||
|
dfu_serial = p.get_dfu_serial()
|
||||||
|
|
||||||
|
# enter DFU
|
||||||
|
p.reset(enter_bootstub=True)
|
||||||
|
p.reset(enter_bootloader=True)
|
||||||
|
assert Panda.wait_for_dfu(dfu_serial, timeout=19), "failed to enter DFU"
|
||||||
|
|
||||||
|
# send junk
|
||||||
|
d = SpiDevice()
|
||||||
|
for _ in range(9):
|
||||||
|
with d.acquire() as spi:
|
||||||
|
dat = [random.randint(-1, 255) for _ in range(random.randint(1, 100))]
|
||||||
|
spi.xfer(dat)
|
||||||
|
|
||||||
|
# should still show up
|
||||||
|
assert dfu_serial in PandaDFU.list()
|
||||||
|
|
||||||
|
class TestSpi:
|
||||||
|
def _ping(self, mocker, panda):
|
||||||
|
# should work with no retries
|
||||||
|
spy = mocker.spy(panda._handle, '_wait_for_ack')
|
||||||
|
panda.health()
|
||||||
|
assert spy.call_count == 2
|
||||||
|
mocker.stop(spy)
|
||||||
|
|
||||||
|
def test_protocol_version_check(self, p):
|
||||||
|
for bootstub in (False, True):
|
||||||
|
p.reset(enter_bootstub=bootstub)
|
||||||
|
with patch('panda.python.spi.PandaSpiHandle.PROTOCOL_VERSION', return_value="abc"):
|
||||||
|
# list should still work with wrong version
|
||||||
|
assert p._serial in Panda.list()
|
||||||
|
|
||||||
|
# connect but raise protocol error
|
||||||
|
with pytest.raises(PandaProtocolMismatch):
|
||||||
|
Panda(p._serial)
|
||||||
|
|
||||||
|
def test_protocol_version_data(self, p):
|
||||||
|
for bootstub in (False, True):
|
||||||
|
p.reset(enter_bootstub=bootstub)
|
||||||
|
v = p._handle.get_protocol_version()
|
||||||
|
|
||||||
|
uid = binascii.hexlify(v[:12]).decode()
|
||||||
|
assert uid == p.get_uid()
|
||||||
|
|
||||||
|
hwtype = v[12]
|
||||||
|
assert hwtype == ord(p.get_type())
|
||||||
|
|
||||||
|
bstub = v[13]
|
||||||
|
assert bstub == (0xEE if bootstub else 0xCC)
|
||||||
|
|
||||||
|
def test_all_comm_types(self, mocker, p):
|
||||||
|
spy = mocker.spy(p._handle, '_wait_for_ack')
|
||||||
|
|
||||||
|
# controlRead + controlWrite
|
||||||
|
p.health()
|
||||||
|
p.can_clear(0)
|
||||||
|
assert spy.call_count == 2*2
|
||||||
|
|
||||||
|
# bulkRead + bulkWrite
|
||||||
|
p.can_recv()
|
||||||
|
p.can_send(0x123, b"somedata", 0)
|
||||||
|
assert spy.call_count == 2*4
|
||||||
|
|
||||||
|
def test_bad_header(self, mocker, p):
|
||||||
|
with patch('panda.python.spi.SYNC', return_value=0):
|
||||||
|
with pytest.raises(PandaSpiNackResponse):
|
||||||
|
p._handle.controlRead(Panda.REQUEST_IN, 0xd2, 0, 0, p.HEALTH_STRUCT.size, timeout=50)
|
||||||
|
self._ping(mocker, p)
|
||||||
|
|
||||||
|
def test_bad_checksum(self, mocker, p):
|
||||||
|
cnt = p.health()['spi_checksum_error_count']
|
||||||
|
with patch('panda.python.spi.PandaSpiHandle._calc_checksum', return_value=0):
|
||||||
|
with pytest.raises(PandaSpiNackResponse):
|
||||||
|
p._handle.controlRead(Panda.REQUEST_IN, 0xd2, 0, 0, p.HEALTH_STRUCT.size, timeout=50)
|
||||||
|
self._ping(mocker, p)
|
||||||
|
assert (p.health()['spi_checksum_error_count'] - cnt) > 0
|
||||||
|
|
||||||
|
def test_non_existent_endpoint(self, mocker, p):
|
||||||
|
for _ in range(10):
|
||||||
|
ep = random.randint(4, 20)
|
||||||
|
with pytest.raises(PandaSpiNackResponse):
|
||||||
|
p._handle.bulkRead(ep, random.randint(1, 1000), timeout=50)
|
||||||
|
|
||||||
|
self._ping(mocker, p)
|
||||||
|
|
||||||
|
with pytest.raises(PandaSpiNackResponse):
|
||||||
|
p._handle.bulkWrite(ep, b"abc", timeout=50)
|
||||||
|
|
||||||
|
self._ping(mocker, p)
|
||||||
29
panda/tests/hitl/6_safety.py
Normal file
29
panda/tests/hitl/6_safety.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
|
||||||
|
def test_safety_nooutput(p):
|
||||||
|
p.set_safety_mode(Panda.SAFETY_SILENT)
|
||||||
|
p.set_can_loopback(True)
|
||||||
|
|
||||||
|
# send a message on bus 0
|
||||||
|
p.can_send(0x1aa, b"message", 0)
|
||||||
|
|
||||||
|
# confirm receive nothing
|
||||||
|
time.sleep(0.05)
|
||||||
|
r = p.can_recv()
|
||||||
|
# bus 192 is messages blocked by TX safety hook on bus 0
|
||||||
|
assert len([x for x in r if x[3] != 192]) == 0
|
||||||
|
assert len([x for x in r if x[3] == 192]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_canfd_safety_modes(p):
|
||||||
|
# works on all pandas
|
||||||
|
p.set_safety_mode(Panda.SAFETY_TOYOTA)
|
||||||
|
assert p.health()['safety_mode'] == Panda.SAFETY_TOYOTA
|
||||||
|
|
||||||
|
# shouldn't be able to set a CAN-FD safety mode on non CAN-FD panda
|
||||||
|
p.set_safety_mode(Panda.SAFETY_HYUNDAI_CANFD)
|
||||||
|
expected_mode = Panda.SAFETY_HYUNDAI_CANFD if p.get_type() in Panda.H7_DEVICES else Panda.SAFETY_SILENT
|
||||||
|
assert p.health()['safety_mode'] == expected_mode
|
||||||
68
panda/tests/hitl/7_internal.py
Normal file
68
panda/tests/hitl/7_internal.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
pytestmark = [
|
||||||
|
pytest.mark.skip_panda_types(Panda.HW_TYPE_UNO),
|
||||||
|
pytest.mark.test_panda_types(Panda.INTERNAL_DEVICES)
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.timeout(2*60)
|
||||||
|
def test_fan_controller(p):
|
||||||
|
start_health = p.health()
|
||||||
|
|
||||||
|
for power in (30, 50, 80, 100):
|
||||||
|
p.set_fan_power(0)
|
||||||
|
while p.get_fan_rpm() > 0:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# wait until fan spins up (and recovers if needed),
|
||||||
|
# then wait a bit more for the RPM to converge
|
||||||
|
p.set_fan_power(power)
|
||||||
|
for _ in range(20):
|
||||||
|
time.sleep(1)
|
||||||
|
if p.get_fan_rpm() > 1000:
|
||||||
|
break
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
expected_rpm = Panda.MAX_FAN_RPMs[bytes(p.get_type())] * power / 100
|
||||||
|
assert 0.9 * expected_rpm <= p.get_fan_rpm() <= 1.1 * expected_rpm
|
||||||
|
|
||||||
|
# Ensure the stall detection is tested on dos
|
||||||
|
if p.get_type() == Panda.HW_TYPE_DOS:
|
||||||
|
stalls = p.health()['fan_stall_count'] - start_health['fan_stall_count']
|
||||||
|
assert stalls >= 2
|
||||||
|
print("stall count", stalls)
|
||||||
|
else:
|
||||||
|
assert p.health()['fan_stall_count'] == 0
|
||||||
|
|
||||||
|
def test_fan_cooldown(p):
|
||||||
|
# if the fan cooldown doesn't work, we get high frequency noise on the tach line
|
||||||
|
# while the rotor spins down. this makes sure it never goes beyond the expected max RPM
|
||||||
|
p.set_fan_power(100)
|
||||||
|
time.sleep(3)
|
||||||
|
p.set_fan_power(0)
|
||||||
|
for _ in range(5):
|
||||||
|
assert p.get_fan_rpm() <= 7000
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
def test_fan_overshoot(p):
|
||||||
|
if p.get_type() == Panda.HW_TYPE_DOS:
|
||||||
|
pytest.skip("panda's fan controller overshoots on the comma three fans that need stall recovery")
|
||||||
|
|
||||||
|
# make sure it's stopped completely
|
||||||
|
p.set_fan_power(0)
|
||||||
|
while p.get_fan_rpm() > 0:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# set it to 30% power to mimic going onroad
|
||||||
|
p.set_fan_power(30)
|
||||||
|
max_rpm = 0
|
||||||
|
for _ in range(50):
|
||||||
|
max_rpm = max(max_rpm, p.get_fan_rpm())
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# tolerate 10% overshoot
|
||||||
|
expected_rpm = Panda.MAX_FAN_RPMs[bytes(p.get_type())] * 30 / 100
|
||||||
|
assert max_rpm <= 1.1 * expected_rpm, f"Fan overshoot: {(max_rpm / expected_rpm * 100) - 100:.1f}%"
|
||||||
0
panda/tests/hitl/__init__.py
Normal file
0
panda/tests/hitl/__init__.py
Normal file
225
panda/tests/hitl/conftest.py
Normal file
225
panda/tests/hitl/conftest.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
|
from panda import Panda, PandaDFU, PandaJungle
|
||||||
|
from panda.tests.hitl.helpers import clear_can_buffers
|
||||||
|
|
||||||
|
# needed to get output when using xdist
|
||||||
|
if "DEBUG" in os.environ:
|
||||||
|
import sys
|
||||||
|
sys.stdout = sys.stderr
|
||||||
|
|
||||||
|
SPEED_NORMAL = 500
|
||||||
|
SPEED_GMLAN = 33.3
|
||||||
|
BUS_SPEEDS = [(0, SPEED_NORMAL), (1, SPEED_NORMAL), (2, SPEED_NORMAL), (3, SPEED_GMLAN)]
|
||||||
|
|
||||||
|
|
||||||
|
JUNGLE_SERIAL = os.getenv("PANDAS_JUNGLE")
|
||||||
|
NO_JUNGLE = os.environ.get("NO_JUNGLE", "0") == "1"
|
||||||
|
PANDAS_EXCLUDE = os.getenv("PANDAS_EXCLUDE", "").strip().split(" ")
|
||||||
|
HW_TYPES = os.environ.get("HW_TYPES", None)
|
||||||
|
|
||||||
|
PARALLEL = "PARALLEL" in os.environ
|
||||||
|
NON_PARALLEL = "NON_PARALLEL" in os.environ
|
||||||
|
if PARALLEL:
|
||||||
|
NO_JUNGLE = True
|
||||||
|
|
||||||
|
class PandaGroup:
|
||||||
|
H7 = (Panda.HW_TYPE_RED_PANDA, Panda.HW_TYPE_RED_PANDA_V2, Panda.HW_TYPE_TRES)
|
||||||
|
GEN2 = (Panda.HW_TYPE_BLACK_PANDA, Panda.HW_TYPE_UNO, Panda.HW_TYPE_DOS) + H7
|
||||||
|
GMLAN = (Panda.HW_TYPE_WHITE_PANDA, Panda.HW_TYPE_GREY_PANDA)
|
||||||
|
|
||||||
|
TESTED = (Panda.HW_TYPE_WHITE_PANDA, Panda.HW_TYPE_BLACK_PANDA, Panda.HW_TYPE_RED_PANDA, Panda.HW_TYPE_RED_PANDA_V2, Panda.HW_TYPE_UNO)
|
||||||
|
|
||||||
|
if HW_TYPES is not None:
|
||||||
|
PandaGroup.TESTED = [bytes([int(x), ]) for x in HW_TYPES.strip().split(",")] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# Find all pandas connected
|
||||||
|
_all_pandas = {}
|
||||||
|
_panda_jungle = None
|
||||||
|
def init_all_pandas():
|
||||||
|
if not NO_JUNGLE:
|
||||||
|
global _panda_jungle
|
||||||
|
_panda_jungle = PandaJungle(JUNGLE_SERIAL)
|
||||||
|
_panda_jungle.set_panda_power(True)
|
||||||
|
|
||||||
|
for serial in Panda.list():
|
||||||
|
if serial not in PANDAS_EXCLUDE:
|
||||||
|
with Panda(serial=serial, claim=False) as p:
|
||||||
|
ptype = bytes(p.get_type())
|
||||||
|
if ptype in PandaGroup.TESTED:
|
||||||
|
_all_pandas[serial] = ptype
|
||||||
|
|
||||||
|
# ensure we have all tested panda types
|
||||||
|
missing_types = set(PandaGroup.TESTED) - set(_all_pandas.values())
|
||||||
|
assert len(missing_types) == 0, f"Missing panda types: {missing_types}"
|
||||||
|
|
||||||
|
print(f"{len(_all_pandas)} total pandas")
|
||||||
|
init_all_pandas()
|
||||||
|
_all_panda_serials = sorted(_all_pandas.keys())
|
||||||
|
|
||||||
|
|
||||||
|
def init_jungle():
|
||||||
|
if _panda_jungle is None:
|
||||||
|
return
|
||||||
|
clear_can_buffers(_panda_jungle)
|
||||||
|
_panda_jungle.set_panda_power(True)
|
||||||
|
_panda_jungle.set_can_loopback(False)
|
||||||
|
_panda_jungle.set_obd(False)
|
||||||
|
_panda_jungle.set_harness_orientation(PandaJungle.HARNESS_ORIENTATION_1)
|
||||||
|
for bus, speed in BUS_SPEEDS:
|
||||||
|
_panda_jungle.set_can_speed_kbps(bus, speed)
|
||||||
|
|
||||||
|
# ensure FW hasn't changed
|
||||||
|
assert _panda_jungle.up_to_date()
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure(config):
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers", "test_panda_types(name): whitelist a test for specific panda types"
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers", "skip_panda_types(name): blacklist panda types from a test"
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers", "panda_expect_can_error: mark test to ignore CAN health errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.hookimpl(tryfirst=True)
|
||||||
|
def pytest_collection_modifyitems(items):
|
||||||
|
for item in items:
|
||||||
|
if item.get_closest_marker('timeout') is None:
|
||||||
|
item.add_marker(pytest.mark.timeout(60))
|
||||||
|
|
||||||
|
# xdist grouping by panda
|
||||||
|
serial = item.name.split("serial=")[1].split(",")[0]
|
||||||
|
assert len(serial) == 24
|
||||||
|
item.add_marker(pytest.mark.xdist_group(serial))
|
||||||
|
|
||||||
|
needs_jungle = "panda_jungle" in item.fixturenames
|
||||||
|
if PARALLEL and needs_jungle:
|
||||||
|
item.add_marker(pytest.mark.skip(reason="no jungle tests in PARALLEL mode"))
|
||||||
|
elif NON_PARALLEL and not needs_jungle:
|
||||||
|
item.add_marker(pytest.mark.skip(reason="only running jungle tests"))
|
||||||
|
|
||||||
|
def pytest_make_parametrize_id(config, val, argname):
|
||||||
|
if val in _all_pandas:
|
||||||
|
# TODO: get nice string instead of int
|
||||||
|
hw_type = _all_pandas[val][0]
|
||||||
|
return f"serial={val}, hw_type={hw_type}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name='panda_jungle', scope='function')
|
||||||
|
def fixture_panda_jungle(request):
|
||||||
|
init_jungle()
|
||||||
|
return _panda_jungle
|
||||||
|
|
||||||
|
@pytest.fixture(name='p', scope='function')
|
||||||
|
def func_fixture_panda(request, module_panda):
|
||||||
|
p = module_panda
|
||||||
|
|
||||||
|
# Check if test is applicable to this panda
|
||||||
|
mark = request.node.get_closest_marker('test_panda_types')
|
||||||
|
if mark:
|
||||||
|
assert len(mark.args) > 0, "Missing panda types argument in mark"
|
||||||
|
test_types = mark.args[0]
|
||||||
|
if _all_pandas[p.get_usb_serial()] not in test_types:
|
||||||
|
pytest.skip(f"Not applicable, {test_types} pandas only")
|
||||||
|
|
||||||
|
mark = request.node.get_closest_marker('skip_panda_types')
|
||||||
|
if mark:
|
||||||
|
assert len(mark.args) > 0, "Missing panda types argument in mark"
|
||||||
|
skip_types = mark.args[0]
|
||||||
|
if _all_pandas[p.get_usb_serial()] in skip_types:
|
||||||
|
pytest.skip(f"Not applicable to {skip_types}")
|
||||||
|
|
||||||
|
# TODO: reset is slow (2+ seconds)
|
||||||
|
p.reset()
|
||||||
|
|
||||||
|
# ensure FW hasn't changed
|
||||||
|
assert p.up_to_date()
|
||||||
|
|
||||||
|
# Run test
|
||||||
|
yield p
|
||||||
|
|
||||||
|
# Teardown
|
||||||
|
|
||||||
|
# reconnect
|
||||||
|
if p.get_dfu_serial() in PandaDFU.list():
|
||||||
|
PandaDFU(p.get_dfu_serial()).reset()
|
||||||
|
p.reconnect()
|
||||||
|
if not p.connected:
|
||||||
|
p.reconnect()
|
||||||
|
if p.bootstub:
|
||||||
|
p.reset()
|
||||||
|
|
||||||
|
assert not p.bootstub
|
||||||
|
|
||||||
|
# TODO: would be nice to make these common checks in the teardown
|
||||||
|
# show up as failed tests instead of "errors"
|
||||||
|
|
||||||
|
# Check for faults
|
||||||
|
assert p.health()['faults'] == 0
|
||||||
|
assert p.health()['fault_status'] == 0
|
||||||
|
|
||||||
|
# Check for SPI errors
|
||||||
|
#assert p.health()['spi_checksum_error_count'] == 0
|
||||||
|
|
||||||
|
# Check health of each CAN core after test, normal to fail for test_gen2_loopback on OBD bus, so skipping
|
||||||
|
mark = request.node.get_closest_marker('panda_expect_can_error')
|
||||||
|
expect_can_error = mark is not None
|
||||||
|
if not expect_can_error:
|
||||||
|
for i in range(3):
|
||||||
|
can_health = p.can_health(i)
|
||||||
|
assert can_health['bus_off_cnt'] == 0
|
||||||
|
assert can_health['receive_error_cnt'] < 127
|
||||||
|
assert can_health['transmit_error_cnt'] < 255
|
||||||
|
assert can_health['error_passive'] == 0
|
||||||
|
assert can_health['error_warning'] == 0
|
||||||
|
assert can_health['total_rx_lost_cnt'] == 0
|
||||||
|
assert can_health['total_tx_lost_cnt'] == 0
|
||||||
|
assert can_health['total_error_cnt'] == 0
|
||||||
|
assert can_health['total_tx_checksum_error_cnt'] == 0
|
||||||
|
|
||||||
|
@pytest.fixture(name='module_panda', params=_all_panda_serials, scope='module')
|
||||||
|
def fixture_panda_setup(request):
|
||||||
|
"""
|
||||||
|
Clean up all pandas + jungle and return the panda under test.
|
||||||
|
"""
|
||||||
|
panda_serial = request.param
|
||||||
|
|
||||||
|
# Initialize jungle
|
||||||
|
init_jungle()
|
||||||
|
|
||||||
|
# Connect to pandas
|
||||||
|
def cnnct(s):
|
||||||
|
if s == panda_serial:
|
||||||
|
p = Panda(serial=s)
|
||||||
|
p.reset(reconnect=True)
|
||||||
|
|
||||||
|
p.set_can_loopback(False)
|
||||||
|
p.set_gmlan(None)
|
||||||
|
p.set_power_save(False)
|
||||||
|
for bus, speed in BUS_SPEEDS:
|
||||||
|
p.set_can_speed_kbps(bus, speed)
|
||||||
|
clear_can_buffers(p)
|
||||||
|
p.set_power_save(False)
|
||||||
|
return p
|
||||||
|
elif not PARALLEL:
|
||||||
|
with Panda(serial=s) as p:
|
||||||
|
p.reset(reconnect=False)
|
||||||
|
return None
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor() as exc:
|
||||||
|
ps = list(exc.map(cnnct, _all_panda_serials, timeout=20))
|
||||||
|
pandas = [p for p in ps if p is not None]
|
||||||
|
|
||||||
|
# run test
|
||||||
|
yield pandas[0]
|
||||||
|
|
||||||
|
# Teardown
|
||||||
|
for p in pandas:
|
||||||
|
p.close()
|
||||||
71
panda/tests/hitl/helpers.py
Normal file
71
panda/tests/hitl/helpers.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import time
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_can_messages(n):
|
||||||
|
m = []
|
||||||
|
for _ in range(n):
|
||||||
|
bus = random.randrange(3)
|
||||||
|
addr = random.randrange(1 << 29)
|
||||||
|
dat = bytes([random.getrandbits(8) for _ in range(random.randrange(1, 9))])
|
||||||
|
m.append([addr, None, dat, bus])
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def time_many_sends(p, bus, p_recv=None, msg_count=100, two_pandas=False, msg_len=8):
|
||||||
|
if p_recv is None:
|
||||||
|
p_recv = p
|
||||||
|
if p == p_recv and two_pandas:
|
||||||
|
raise ValueError("Cannot have two pandas that are the same panda")
|
||||||
|
|
||||||
|
msg_id = random.randint(0x100, 0x200)
|
||||||
|
to_send = [(msg_id, 0, b"\xaa" * msg_len, bus)] * msg_count
|
||||||
|
|
||||||
|
start_time = time.monotonic()
|
||||||
|
p.can_send_many(to_send)
|
||||||
|
r = []
|
||||||
|
r_echo = []
|
||||||
|
r_len_expected = msg_count if two_pandas else msg_count * 2
|
||||||
|
r_echo_len_exected = msg_count if two_pandas else 0
|
||||||
|
|
||||||
|
while len(r) < r_len_expected and (time.monotonic() - start_time) < 5:
|
||||||
|
r.extend(p_recv.can_recv())
|
||||||
|
end_time = time.monotonic()
|
||||||
|
if two_pandas:
|
||||||
|
while len(r_echo) < r_echo_len_exected and (time.monotonic() - start_time) < 10:
|
||||||
|
r_echo.extend(p.can_recv())
|
||||||
|
|
||||||
|
sent_echo = [x for x in r if x[3] == 0x80 | bus and x[0] == msg_id]
|
||||||
|
sent_echo.extend([x for x in r_echo if x[3] == 0x80 | bus and x[0] == msg_id])
|
||||||
|
resp = [x for x in r if x[3] == bus and x[0] == msg_id]
|
||||||
|
|
||||||
|
leftovers = [x for x in r if (x[3] != 0x80 | bus and x[3] != bus) or x[0] != msg_id]
|
||||||
|
assert len(leftovers) == 0
|
||||||
|
|
||||||
|
assert len(resp) == msg_count
|
||||||
|
assert len(sent_echo) == msg_count
|
||||||
|
|
||||||
|
end_time = (end_time - start_time) * 1000.0
|
||||||
|
comp_kbps = (1 + 11 + 1 + 1 + 1 + 4 + (msg_len * 8) + 15 + 1 + 1 + 1 + 7) * msg_count / end_time
|
||||||
|
|
||||||
|
return comp_kbps
|
||||||
|
|
||||||
|
|
||||||
|
def clear_can_buffers(panda, speed: int | None = None):
|
||||||
|
if speed is not None:
|
||||||
|
for bus in range(3):
|
||||||
|
panda.set_can_speed_kbps(bus, speed)
|
||||||
|
|
||||||
|
# clear tx buffers
|
||||||
|
for i in range(4):
|
||||||
|
panda.can_clear(i)
|
||||||
|
|
||||||
|
# clear rx buffers
|
||||||
|
panda.can_clear(0xFFFF)
|
||||||
|
r = [1]
|
||||||
|
st = time.monotonic()
|
||||||
|
while len(r) > 0:
|
||||||
|
r = panda.can_recv()
|
||||||
|
time.sleep(0.05)
|
||||||
|
if (time.monotonic() - st) > 10:
|
||||||
|
raise Exception("Unable to clear can buffers for panda ", panda.get_serial())
|
||||||
BIN
panda/tests/hitl/known_bootstub/bootstub.panda_h7.bin
Normal file
BIN
panda/tests/hitl/known_bootstub/bootstub.panda_h7.bin
Normal file
Binary file not shown.
Binary file not shown.
BIN
panda/tests/hitl/known_bootstub/bootstub_f4_only_bcd.panda.bin
Normal file
BIN
panda/tests/hitl/known_bootstub/bootstub_f4_only_bcd.panda.bin
Normal file
Binary file not shown.
42
panda/tests/hitl/reset_jungles.py
Normal file
42
panda/tests/hitl/reset_jungles.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
|
from panda import PandaJungle, PandaJungleDFU, McuType
|
||||||
|
from panda.tests.libs.resetter import Resetter
|
||||||
|
|
||||||
|
SERIALS = {'180019001451313236343430', '1d0017000c50435635333720'}
|
||||||
|
|
||||||
|
def recover(s):
|
||||||
|
with PandaJungleDFU(s) as pd:
|
||||||
|
pd.recover()
|
||||||
|
|
||||||
|
def flash(s):
|
||||||
|
with PandaJungle(s) as p:
|
||||||
|
p.flash()
|
||||||
|
return p.get_mcu_type()
|
||||||
|
|
||||||
|
# Reset + flash all CI hardware to get it into a consistent state
|
||||||
|
# * port 1: jungles-under-test
|
||||||
|
# * port 2: USB hubs
|
||||||
|
# * port 3: HITL pandas and their jungles
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with Resetter() as r:
|
||||||
|
# everything off
|
||||||
|
for i in range(1, 4):
|
||||||
|
r.enable_power(i, 0)
|
||||||
|
r.cycle_power(ports=[1, 2], dfu=True)
|
||||||
|
|
||||||
|
dfu_serials = PandaJungleDFU.list()
|
||||||
|
print(len(dfu_serials), len(SERIALS))
|
||||||
|
assert len(dfu_serials) == len(SERIALS)
|
||||||
|
|
||||||
|
with concurrent.futures.ProcessPoolExecutor(max_workers=len(dfu_serials)) as exc:
|
||||||
|
list(exc.map(recover, dfu_serials, timeout=30))
|
||||||
|
|
||||||
|
# power cycle for H7 bootloader bug
|
||||||
|
r.cycle_power(ports=[1, 2])
|
||||||
|
|
||||||
|
serials = PandaJungle.list()
|
||||||
|
assert set(PandaJungle.list()) >= SERIALS
|
||||||
|
mcu_types = list(exc.map(flash, SERIALS, timeout=20))
|
||||||
|
assert set(mcu_types) == {McuType.F4, McuType.H7}
|
||||||
8
panda/tests/hitl/run_parallel_tests.sh
Normal file
8
panda/tests/hitl/run_parallel_tests.sh
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||||
|
cd $DIR
|
||||||
|
|
||||||
|
# n = number of pandas tested
|
||||||
|
PARALLEL=1 pytest --durations=0 *.py -n 5 --dist loadgroup -x
|
||||||
7
panda/tests/hitl/run_serial_tests.sh
Normal file
7
panda/tests/hitl/run_serial_tests.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||||
|
cd $DIR
|
||||||
|
|
||||||
|
NON_PARALLEL=1 pytest --durations=0 *.py -x
|
||||||
14
panda/tests/ir_test.py
Normal file
14
panda/tests/ir_test.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import time
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
power = 0
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = Panda()
|
||||||
|
while True:
|
||||||
|
p.set_ir_power(power)
|
||||||
|
print("Power: ", str(power))
|
||||||
|
time.sleep(1)
|
||||||
|
power += 10
|
||||||
|
power %= 100
|
||||||
42
panda/tests/libpanda/SConscript
Normal file
42
panda/tests/libpanda/SConscript
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import platform
|
||||||
|
|
||||||
|
CC = 'gcc'
|
||||||
|
system = platform.system()
|
||||||
|
if system == 'Darwin':
|
||||||
|
# gcc installed by homebrew has version suffix (e.g. gcc-12) in order to be
|
||||||
|
# distinguishable from system one - which acts as a symlink to clang
|
||||||
|
CC += '-13'
|
||||||
|
|
||||||
|
env = Environment(
|
||||||
|
CC=CC,
|
||||||
|
CFLAGS=[
|
||||||
|
'-nostdlib',
|
||||||
|
'-fno-builtin',
|
||||||
|
'-std=gnu11',
|
||||||
|
'-Wfatal-errors',
|
||||||
|
'-Wno-pointer-to-int-cast',
|
||||||
|
],
|
||||||
|
CPPPATH=[".", "../../board/"],
|
||||||
|
)
|
||||||
|
if system == "Darwin":
|
||||||
|
env.PrependENVPath('PATH', '/opt/homebrew/bin')
|
||||||
|
|
||||||
|
if GetOption('ubsan'):
|
||||||
|
flags = [
|
||||||
|
"-fsanitize=undefined",
|
||||||
|
"-fno-sanitize-recover=undefined",
|
||||||
|
]
|
||||||
|
env['CFLAGS'] += flags
|
||||||
|
env['LINKFLAGS'] += flags
|
||||||
|
|
||||||
|
panda = env.SharedObject("panda.os", "panda.c")
|
||||||
|
libpanda = env.SharedLibrary("libpanda.so", [panda])
|
||||||
|
|
||||||
|
if GetOption('coverage'):
|
||||||
|
env.Append(
|
||||||
|
CFLAGS=["-fprofile-arcs", "-ftest-coverage", "-fprofile-abs-path",],
|
||||||
|
LIBS=["gcov"],
|
||||||
|
)
|
||||||
|
# GCC note file is generated by compiler, ensure we build it, and allow scons to clean it up
|
||||||
|
AlwaysBuild(panda)
|
||||||
|
env.SideEffect("panda.gcno", panda)
|
||||||
98
panda/tests/libpanda/libpanda_py.py
Normal file
98
panda/tests/libpanda/libpanda_py.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import os
|
||||||
|
from cffi import FFI
|
||||||
|
from typing import Any, Protocol
|
||||||
|
|
||||||
|
from panda import LEN_TO_DLC
|
||||||
|
from panda.tests.libpanda.safety_helpers import PandaSafety, setup_safety_helpers
|
||||||
|
|
||||||
|
libpanda_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
libpanda_fn = os.path.join(libpanda_dir, "libpanda.so")
|
||||||
|
|
||||||
|
ffi = FFI()
|
||||||
|
|
||||||
|
ffi.cdef("""
|
||||||
|
typedef struct {
|
||||||
|
unsigned char reserved : 1;
|
||||||
|
unsigned char bus : 3;
|
||||||
|
unsigned char data_len_code : 4;
|
||||||
|
unsigned char rejected : 1;
|
||||||
|
unsigned char returned : 1;
|
||||||
|
unsigned char extended : 1;
|
||||||
|
unsigned int addr : 29;
|
||||||
|
unsigned char checksum;
|
||||||
|
unsigned char data[64];
|
||||||
|
} CANPacket_t;
|
||||||
|
""", packed=True)
|
||||||
|
|
||||||
|
ffi.cdef("""
|
||||||
|
bool safety_rx_hook(CANPacket_t *to_send);
|
||||||
|
bool safety_tx_hook(CANPacket_t *to_push);
|
||||||
|
int safety_fwd_hook(int bus_num, int addr);
|
||||||
|
int set_safety_hooks(uint16_t mode, uint16_t param);
|
||||||
|
""")
|
||||||
|
|
||||||
|
ffi.cdef("""
|
||||||
|
typedef struct {
|
||||||
|
volatile uint32_t w_ptr;
|
||||||
|
volatile uint32_t r_ptr;
|
||||||
|
uint32_t fifo_size;
|
||||||
|
CANPacket_t *elems;
|
||||||
|
} can_ring;
|
||||||
|
|
||||||
|
extern can_ring *rx_q;
|
||||||
|
extern can_ring *txgmlan_q;
|
||||||
|
extern can_ring *tx1_q;
|
||||||
|
extern can_ring *tx2_q;
|
||||||
|
extern can_ring *tx3_q;
|
||||||
|
|
||||||
|
bool can_pop(can_ring *q, CANPacket_t *elem);
|
||||||
|
bool can_push(can_ring *q, CANPacket_t *elem);
|
||||||
|
void can_set_checksum(CANPacket_t *packet);
|
||||||
|
int comms_can_read(uint8_t *data, uint32_t max_len);
|
||||||
|
void comms_can_write(uint8_t *data, uint32_t len);
|
||||||
|
void comms_can_reset(void);
|
||||||
|
uint32_t can_slots_empty(can_ring *q);
|
||||||
|
""")
|
||||||
|
|
||||||
|
setup_safety_helpers(ffi)
|
||||||
|
|
||||||
|
class CANPacket:
|
||||||
|
reserved: int
|
||||||
|
bus: int
|
||||||
|
data_len_code: int
|
||||||
|
rejected: int
|
||||||
|
returned: int
|
||||||
|
extended: int
|
||||||
|
addr: int
|
||||||
|
data: list[int]
|
||||||
|
|
||||||
|
class Panda(PandaSafety, Protocol):
|
||||||
|
# CAN
|
||||||
|
tx1_q: Any
|
||||||
|
tx2_q: Any
|
||||||
|
tx3_q: Any
|
||||||
|
txgmlan_q: Any
|
||||||
|
def can_set_checksum(self, p: CANPacket) -> None: ...
|
||||||
|
|
||||||
|
# safety
|
||||||
|
def safety_rx_hook(self, to_send: CANPacket) -> int: ...
|
||||||
|
def safety_tx_hook(self, to_push: CANPacket) -> int: ...
|
||||||
|
def safety_fwd_hook(self, bus_num: int, addr: int) -> int: ...
|
||||||
|
def set_safety_hooks(self, mode: int, param: int) -> int: ...
|
||||||
|
|
||||||
|
|
||||||
|
libpanda: Panda = ffi.dlopen(libpanda_fn)
|
||||||
|
|
||||||
|
|
||||||
|
# helpers
|
||||||
|
|
||||||
|
def make_CANPacket(addr: int, bus: int, dat):
|
||||||
|
ret = ffi.new('CANPacket_t *')
|
||||||
|
ret[0].extended = 1 if addr >= 0x800 else 0
|
||||||
|
ret[0].addr = addr
|
||||||
|
ret[0].data_len_code = LEN_TO_DLC[len(dat)]
|
||||||
|
ret[0].bus = bus
|
||||||
|
ret[0].data = bytes(dat)
|
||||||
|
libpanda.can_set_checksum(ret)
|
||||||
|
|
||||||
|
return ret
|
||||||
33
panda/tests/libpanda/panda.c
Normal file
33
panda/tests/libpanda/panda.c
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#include "fake_stm.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "can_definitions.h"
|
||||||
|
|
||||||
|
bool bitbang_gmlan(CANPacket_t *to_bang) { return true; }
|
||||||
|
bool can_init(uint8_t can_number) { return true; }
|
||||||
|
void process_can(uint8_t can_number) { }
|
||||||
|
//int safety_tx_hook(CANPacket_t *to_send) { return 1; }
|
||||||
|
|
||||||
|
typedef struct harness_configuration harness_configuration;
|
||||||
|
void refresh_can_tx_slots_available(void);
|
||||||
|
void can_tx_comms_resume_usb(void) { };
|
||||||
|
void can_tx_comms_resume_spi(void) { };
|
||||||
|
|
||||||
|
#include "health.h"
|
||||||
|
#include "faults.h"
|
||||||
|
#include "libc.h"
|
||||||
|
#include "boards/board_declarations.h"
|
||||||
|
#include "safety.h"
|
||||||
|
#include "main_declarations.h"
|
||||||
|
#include "drivers/can_common.h"
|
||||||
|
|
||||||
|
can_ring *rx_q = &can_rx_q;
|
||||||
|
can_ring *txgmlan_q = &can_txgmlan_q;
|
||||||
|
can_ring *tx1_q = &can_tx1_q;
|
||||||
|
can_ring *tx2_q = &can_tx2_q;
|
||||||
|
can_ring *tx3_q = &can_tx3_q;
|
||||||
|
|
||||||
|
#include "comms_definitions.h"
|
||||||
|
#include "can_comms.h"
|
||||||
|
|
||||||
|
// libpanda stuff
|
||||||
|
#include "safety_helpers.h"
|
||||||
195
panda/tests/libpanda/safety_helpers.h
Normal file
195
panda/tests/libpanda/safety_helpers.h
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
void safety_tick_current_safety_config() {
|
||||||
|
safety_tick(¤t_safety_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool safety_config_valid() {
|
||||||
|
if (current_safety_config.rx_checks_len <= 0) {
|
||||||
|
printf("missing RX checks\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
|
||||||
|
const RxCheck addr = current_safety_config.rx_checks[i];
|
||||||
|
bool valid = addr.status.msg_seen && !addr.status.lagging && addr.status.valid_checksum && (addr.status.wrong_counters < MAX_WRONG_COUNTERS) && addr.status.valid_quality_flag;
|
||||||
|
if (!valid) {
|
||||||
|
// printf("i %d seen %d lagging %d valid checksum %d wrong counters %d valid quality flag %d\n", i, addr.status.msg_seen, addr.status.lagging, addr.status.valid_checksum, addr.status.wrong_counters, addr.status.valid_quality_flag);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_controls_allowed(bool c){
|
||||||
|
controls_allowed = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_alternative_experience(int mode){
|
||||||
|
alternative_experience = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_relay_malfunction(bool c){
|
||||||
|
relay_malfunction = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_controls_allowed(void){
|
||||||
|
return controls_allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_alternative_experience(void){
|
||||||
|
return alternative_experience;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_relay_malfunction(void){
|
||||||
|
return relay_malfunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_gas_interceptor_prev(void){
|
||||||
|
return gas_interceptor_prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_gas_pressed_prev(void){
|
||||||
|
return gas_pressed_prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_brake_pressed_prev(void){
|
||||||
|
return brake_pressed_prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_regen_braking_prev(void){
|
||||||
|
return regen_braking_prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_cruise_engaged_prev(void){
|
||||||
|
return cruise_engaged_prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_cruise_engaged_prev(bool engaged){
|
||||||
|
cruise_engaged_prev = engaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_vehicle_moving(void){
|
||||||
|
return vehicle_moving;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_acc_main_on(void){
|
||||||
|
return acc_main_on;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_vehicle_speed_min(void){
|
||||||
|
return vehicle_speed.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_vehicle_speed_max(void){
|
||||||
|
return vehicle_speed.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_vehicle_speed_last(void){
|
||||||
|
return vehicle_speed.values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_current_safety_mode(void){
|
||||||
|
return current_safety_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_current_safety_param(void){
|
||||||
|
return current_safety_param;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_hw_type(void){
|
||||||
|
return hw_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_timer(uint32_t t){
|
||||||
|
timer.CNT = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_torque_meas(int min, int max){
|
||||||
|
torque_meas.min = min;
|
||||||
|
torque_meas.max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_torque_meas_min(void){
|
||||||
|
return torque_meas.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_torque_meas_max(void){
|
||||||
|
return torque_meas.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_torque_driver(int min, int max){
|
||||||
|
torque_driver.min = min;
|
||||||
|
torque_driver.max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_torque_driver_min(void){
|
||||||
|
return torque_driver.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_torque_driver_max(void){
|
||||||
|
return torque_driver.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_rt_torque_last(int t){
|
||||||
|
rt_torque_last = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_desired_torque_last(int t){
|
||||||
|
desired_torque_last = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_desired_angle_last(int t){
|
||||||
|
desired_angle_last = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_desired_angle_last(void){
|
||||||
|
return desired_angle_last;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_angle_meas(int min, int max){
|
||||||
|
angle_meas.min = min;
|
||||||
|
angle_meas.max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_angle_meas_min(void){
|
||||||
|
return angle_meas.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_angle_meas_max(void){
|
||||||
|
return angle_meas.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ***** car specific helpers *****
|
||||||
|
|
||||||
|
void set_honda_alt_brake_msg(bool c){
|
||||||
|
honda_alt_brake_msg = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_honda_bosch_long(bool c){
|
||||||
|
honda_bosch_long = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_honda_hw(void) {
|
||||||
|
return honda_hw;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_honda_fwd_brake(bool c){
|
||||||
|
honda_fwd_brake = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_honda_fwd_brake(void){
|
||||||
|
return honda_fwd_brake;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_tests(void){
|
||||||
|
// get HW_TYPE from env variable set in test.sh
|
||||||
|
if (getenv("HW_TYPE")) {
|
||||||
|
hw_type = atoi(getenv("HW_TYPE"));
|
||||||
|
}
|
||||||
|
safety_mode_cnt = 2U; // avoid ignoring relay_malfunction logic
|
||||||
|
alternative_experience = 0;
|
||||||
|
set_timer(0);
|
||||||
|
ts_steer_req_mismatch_last = 0;
|
||||||
|
valid_steer_req_count = 0;
|
||||||
|
invalid_steer_req_count = 0;
|
||||||
|
}
|
||||||
106
panda/tests/libpanda/safety_helpers.py
Normal file
106
panda/tests/libpanda/safety_helpers.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# panda safety helpers, from safety_helpers.c
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
def setup_safety_helpers(ffi):
|
||||||
|
ffi.cdef("""
|
||||||
|
void set_controls_allowed(bool c);
|
||||||
|
bool get_controls_allowed(void);
|
||||||
|
bool get_longitudinal_allowed(void);
|
||||||
|
void set_alternative_experience(int mode);
|
||||||
|
int get_alternative_experience(void);
|
||||||
|
void set_relay_malfunction(bool c);
|
||||||
|
bool get_relay_malfunction(void);
|
||||||
|
int get_gas_interceptor_prev(void);
|
||||||
|
bool get_gas_pressed_prev(void);
|
||||||
|
bool get_brake_pressed_prev(void);
|
||||||
|
bool get_regen_braking_prev(void);
|
||||||
|
bool get_acc_main_on(void);
|
||||||
|
int get_vehicle_speed_min(void);
|
||||||
|
int get_vehicle_speed_max(void);
|
||||||
|
int get_vehicle_speed_last(void);
|
||||||
|
int get_current_safety_mode(void);
|
||||||
|
int get_current_safety_param(void);
|
||||||
|
|
||||||
|
void set_torque_meas(int min, int max);
|
||||||
|
int get_torque_meas_min(void);
|
||||||
|
int get_torque_meas_max(void);
|
||||||
|
void set_torque_driver(int min, int max);
|
||||||
|
int get_torque_driver_min(void);
|
||||||
|
int get_torque_driver_max(void);
|
||||||
|
void set_desired_torque_last(int t);
|
||||||
|
void set_rt_torque_last(int t);
|
||||||
|
void set_desired_angle_last(int t);
|
||||||
|
int get_desired_angle_last();
|
||||||
|
void set_angle_meas(int min, int max);
|
||||||
|
int get_angle_meas_min(void);
|
||||||
|
int get_angle_meas_max(void);
|
||||||
|
|
||||||
|
bool get_cruise_engaged_prev(void);
|
||||||
|
void set_cruise_engaged_prev(bool engaged);
|
||||||
|
bool get_vehicle_moving(void);
|
||||||
|
int get_hw_type(void);
|
||||||
|
void set_timer(uint32_t t);
|
||||||
|
|
||||||
|
void safety_tick_current_safety_config();
|
||||||
|
bool safety_config_valid();
|
||||||
|
|
||||||
|
void init_tests(void);
|
||||||
|
|
||||||
|
void set_honda_fwd_brake(bool c);
|
||||||
|
bool get_honda_fwd_brake(void);
|
||||||
|
void set_honda_alt_brake_msg(bool c);
|
||||||
|
void set_honda_bosch_long(bool c);
|
||||||
|
int get_honda_hw(void);
|
||||||
|
""")
|
||||||
|
|
||||||
|
class PandaSafety(Protocol):
|
||||||
|
def set_controls_allowed(self, c: bool) -> None: ...
|
||||||
|
def get_controls_allowed(self) -> bool: ...
|
||||||
|
def get_longitudinal_allowed(self) -> bool: ...
|
||||||
|
def set_alternative_experience(self, mode: int) -> None: ...
|
||||||
|
def get_alternative_experience(self) -> int: ...
|
||||||
|
def set_relay_malfunction(self, c: bool) -> None: ...
|
||||||
|
def get_relay_malfunction(self) -> bool: ...
|
||||||
|
def get_gas_interceptor_prev(self) -> int: ...
|
||||||
|
def get_gas_pressed_prev(self) -> bool: ...
|
||||||
|
def get_brake_pressed_prev(self) -> bool: ...
|
||||||
|
def get_regen_braking_prev(self) -> bool: ...
|
||||||
|
def get_acc_main_on(self) -> bool: ...
|
||||||
|
def get_vehicle_speed_min(self) -> int: ...
|
||||||
|
def get_vehicle_speed_max(self) -> int: ...
|
||||||
|
def get_vehicle_speed_last(self) -> int: ...
|
||||||
|
def get_current_safety_mode(self) -> int: ...
|
||||||
|
def get_current_safety_param(self) -> int: ...
|
||||||
|
|
||||||
|
def set_torque_meas(self, min: int, max: int) -> None: ... # noqa: A002
|
||||||
|
def get_torque_meas_min(self) -> int: ...
|
||||||
|
def get_torque_meas_max(self) -> int: ...
|
||||||
|
def set_torque_driver(self, min: int, max: int) -> None: ... # noqa: A002
|
||||||
|
def get_torque_driver_min(self) -> int: ...
|
||||||
|
def get_torque_driver_max(self) -> int: ...
|
||||||
|
def set_desired_torque_last(self, t: int) -> None: ...
|
||||||
|
def set_rt_torque_last(self, t: int) -> None: ...
|
||||||
|
def set_desired_angle_last(self, t: int) -> None: ...
|
||||||
|
def get_desired_angle_last(self) -> int: ...
|
||||||
|
def set_angle_meas(self, min: int, max: int) -> None: ... # noqa: A002
|
||||||
|
def get_angle_meas_min(self) -> int: ...
|
||||||
|
def get_angle_meas_max(self) -> int: ...
|
||||||
|
|
||||||
|
def get_cruise_engaged_prev(self) -> bool: ...
|
||||||
|
def set_cruise_engaged_prev(self, enabled: bool) -> None: ...
|
||||||
|
def get_vehicle_moving(self) -> bool: ...
|
||||||
|
def get_hw_type(self) -> int: ...
|
||||||
|
def set_timer(self, t: int) -> None: ...
|
||||||
|
|
||||||
|
def safety_tick_current_safety_config(self) -> None: ...
|
||||||
|
def safety_config_valid(self) -> bool: ...
|
||||||
|
|
||||||
|
def init_tests(self) -> None: ...
|
||||||
|
|
||||||
|
def set_honda_fwd_brake(self, c: bool) -> None: ...
|
||||||
|
def get_honda_fwd_brake(self) -> bool: ...
|
||||||
|
def set_honda_alt_brake_msg(self, c: bool) -> None: ...
|
||||||
|
def set_honda_bosch_long(self, c: bool) -> None: ...
|
||||||
|
def get_honda_hw(self) -> int: ...
|
||||||
|
|
||||||
|
|
||||||
57
panda/tests/libs/resetter.py
Normal file
57
panda/tests/libs/resetter.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import time
|
||||||
|
import usb1
|
||||||
|
|
||||||
|
|
||||||
|
class Resetter():
|
||||||
|
def __init__(self):
|
||||||
|
self._handle = None
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._handle.close()
|
||||||
|
self._context.close()
|
||||||
|
self._handle = None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
if self._handle:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
self._handle = None
|
||||||
|
|
||||||
|
self._context = usb1.USBContext()
|
||||||
|
self._context.open()
|
||||||
|
for device in self._context.getDeviceList(skip_on_error=True):
|
||||||
|
if device.getVendorID() == 0xbbaa and device.getProductID() == 0xddc0:
|
||||||
|
try:
|
||||||
|
self._handle = device.open()
|
||||||
|
self._handle.claimInterface(0)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
assert self._handle
|
||||||
|
|
||||||
|
def enable_power(self, port, enabled):
|
||||||
|
self._handle.controlWrite((usb1.ENDPOINT_OUT | usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE), 0xff, port, enabled, b'')
|
||||||
|
|
||||||
|
def enable_boot(self, enabled):
|
||||||
|
self._handle.controlWrite((usb1.ENDPOINT_OUT | usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE), 0xff, 0, enabled, b'')
|
||||||
|
|
||||||
|
def cycle_power(self, delay=5, dfu=False, ports=None):
|
||||||
|
if ports is None:
|
||||||
|
ports = [1, 2, 3]
|
||||||
|
|
||||||
|
self.enable_boot(dfu)
|
||||||
|
for port in ports:
|
||||||
|
self.enable_power(port, False)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
for port in ports:
|
||||||
|
self.enable_power(port, True)
|
||||||
|
time.sleep(delay)
|
||||||
|
self.enable_boot(False)
|
||||||
94
panda/tests/loopback_test.py
Normal file
94
panda/tests/loopback_test.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import argparse
|
||||||
|
from itertools import permutations
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
def get_test_string():
|
||||||
|
return b"test" + os.urandom(10)
|
||||||
|
|
||||||
|
def run_test(sleep_duration):
|
||||||
|
pandas = Panda.list()
|
||||||
|
print(pandas)
|
||||||
|
|
||||||
|
if len(pandas) < 2:
|
||||||
|
raise Exception("Minimum two pandas are needed for test")
|
||||||
|
|
||||||
|
run_test_w_pandas(pandas, sleep_duration)
|
||||||
|
|
||||||
|
def run_test_w_pandas(pandas, sleep_duration):
|
||||||
|
h = [Panda(x) for x in pandas]
|
||||||
|
print("H", h)
|
||||||
|
|
||||||
|
for hh in h:
|
||||||
|
hh.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
# test both directions
|
||||||
|
for ho in permutations(list(range(len(h))), r=2):
|
||||||
|
print("***************** TESTING", ho)
|
||||||
|
|
||||||
|
panda0, panda1 = h[ho[0]], h[ho[1]]
|
||||||
|
|
||||||
|
# **** test health packet ****
|
||||||
|
print("health", ho[0], h[ho[0]].health())
|
||||||
|
|
||||||
|
# **** test can line loopback ****
|
||||||
|
for bus, gmlan in [(0, False), (1, False), (2, False), (1, True), (2, True)]:
|
||||||
|
print("\ntest can", bus)
|
||||||
|
# flush
|
||||||
|
cans_echo = panda0.can_recv()
|
||||||
|
cans_loop = panda1.can_recv()
|
||||||
|
|
||||||
|
panda0.set_gmlan(None)
|
||||||
|
panda1.set_gmlan(None)
|
||||||
|
|
||||||
|
if gmlan is True:
|
||||||
|
panda0.set_gmlan(bus)
|
||||||
|
panda1.set_gmlan(bus)
|
||||||
|
bus = 3
|
||||||
|
|
||||||
|
# send the characters
|
||||||
|
at = random.randint(1, 2000)
|
||||||
|
st = get_test_string()[0:8]
|
||||||
|
panda0.can_send(at, st, bus)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# check for receive
|
||||||
|
cans_echo = panda0.can_recv()
|
||||||
|
cans_loop = panda1.can_recv()
|
||||||
|
|
||||||
|
print("Bus", bus, "echo", cans_echo, "loop", cans_loop)
|
||||||
|
|
||||||
|
assert len(cans_echo) == 1
|
||||||
|
assert len(cans_loop) == 1
|
||||||
|
|
||||||
|
assert cans_echo[0][0] == at
|
||||||
|
assert cans_loop[0][0] == at
|
||||||
|
|
||||||
|
assert cans_echo[0][2] == st
|
||||||
|
assert cans_loop[0][2] == st
|
||||||
|
|
||||||
|
assert cans_echo[0][3] == 0x80 | bus
|
||||||
|
if cans_loop[0][3] != bus:
|
||||||
|
print("EXPECTED %d GOT %d" % (bus, cans_loop[0][3]))
|
||||||
|
assert cans_loop[0][3] == bus
|
||||||
|
|
||||||
|
print("CAN pass", bus, ho)
|
||||||
|
time.sleep(sleep_duration)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-n", type=int, help="Number of test iterations to run")
|
||||||
|
parser.add_argument("-sleep", type=int, help="Sleep time between tests", default=0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.n is None:
|
||||||
|
while True:
|
||||||
|
run_test(sleep_duration=args.sleep)
|
||||||
|
else:
|
||||||
|
for _ in range(args.n):
|
||||||
|
run_test(sleep_duration=args.sleep)
|
||||||
70
panda/tests/message_drop_test.py
Normal file
70
panda/tests/message_drop_test.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import usb1
|
||||||
|
import time
|
||||||
|
import struct
|
||||||
|
import itertools
|
||||||
|
import threading
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
JUNGLE = "JUNGLE" in os.environ
|
||||||
|
if JUNGLE:
|
||||||
|
from panda import PandaJungle
|
||||||
|
|
||||||
|
# Generate unique messages
|
||||||
|
NUM_MESSAGES_PER_BUS = 10000
|
||||||
|
messages = [bytes(struct.pack("Q", i)) for i in range(NUM_MESSAGES_PER_BUS)]
|
||||||
|
tx_messages = list(itertools.chain.from_iterable([[0xaa, None, msg, 0], [0xaa, None, msg, 1], [0xaa, None, msg, 2]] for msg in messages))
|
||||||
|
|
||||||
|
def flood_tx(panda):
|
||||||
|
print('Sending!')
|
||||||
|
transferred = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(f"Sending block {transferred}-{len(tx_messages)}: ", end="")
|
||||||
|
panda.can_send_many(tx_messages[transferred:], timeout=10)
|
||||||
|
print("OK")
|
||||||
|
break
|
||||||
|
except usb1.USBErrorTimeout as e:
|
||||||
|
transferred += (e.transferred // 16)
|
||||||
|
print("timeout, transferred: ", transferred)
|
||||||
|
|
||||||
|
print(f"Done sending {3*NUM_MESSAGES_PER_BUS} messages!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
serials = Panda.list()
|
||||||
|
receiver: Panda | PandaJungle
|
||||||
|
if JUNGLE:
|
||||||
|
sender = Panda()
|
||||||
|
receiver = PandaJungle()
|
||||||
|
else:
|
||||||
|
if len(serials) != 2:
|
||||||
|
raise Exception("Connect two pandas to perform this test!")
|
||||||
|
sender = Panda(serials[0])
|
||||||
|
receiver = Panda(serials[1])
|
||||||
|
receiver.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
sender.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
# Start transmisson
|
||||||
|
threading.Thread(target=flood_tx, args=(sender,)).start()
|
||||||
|
|
||||||
|
# Receive as much as we can, and stop when there hasn't been anything for a second
|
||||||
|
rx: list[Any] = []
|
||||||
|
old_len = 0
|
||||||
|
last_change = time.monotonic()
|
||||||
|
while time.monotonic() - last_change < 1:
|
||||||
|
if old_len < len(rx):
|
||||||
|
last_change = time.monotonic()
|
||||||
|
old_len = len(rx)
|
||||||
|
|
||||||
|
rx.extend(receiver.can_recv())
|
||||||
|
print(f"Received {len(rx)} messages")
|
||||||
|
|
||||||
|
# Check if we received everything
|
||||||
|
for bus in range(3):
|
||||||
|
received_msgs = {bytes(m[2]) for m in filter(lambda m, b=bus: m[3] == b, rx)} # type: ignore
|
||||||
|
dropped_msgs = set(messages).difference(received_msgs)
|
||||||
|
print(f"Bus {bus} dropped msgs: {len(list(dropped_msgs))} / {len(messages)}")
|
||||||
5
panda/tests/misra/.gitignore
vendored
Normal file
5
panda/tests/misra/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
*.pdf
|
||||||
|
*.txt
|
||||||
|
.output.log
|
||||||
|
new_table
|
||||||
|
cppcheck/
|
||||||
156
panda/tests/misra/coverage_table
Normal file
156
panda/tests/misra/coverage_table
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
1.1
|
||||||
|
1.2 X (Addon)
|
||||||
|
1.3 X (Cppcheck)
|
||||||
|
2.1 X (Cppcheck)
|
||||||
|
2.2 X (Addon)
|
||||||
|
2.3 X (Addon)
|
||||||
|
2.4 X (Addon)
|
||||||
|
2.5 X (Addon)
|
||||||
|
2.6 X (Cppcheck)
|
||||||
|
2.7 X (Addon)
|
||||||
|
3.1 X (Addon)
|
||||||
|
3.2 X (Addon)
|
||||||
|
4.1 X (Addon)
|
||||||
|
4.2 X (Addon)
|
||||||
|
5.1 X (Addon)
|
||||||
|
5.2 X (Addon)
|
||||||
|
5.3 X (Cppcheck)
|
||||||
|
5.4 X (Addon)
|
||||||
|
5.5 X (Addon)
|
||||||
|
5.6 X (Addon)
|
||||||
|
5.7 X (Addon)
|
||||||
|
5.8 X (Addon)
|
||||||
|
5.9 X (Addon)
|
||||||
|
6.1 X (Addon)
|
||||||
|
6.2 X (Addon)
|
||||||
|
7.1 X (Addon)
|
||||||
|
7.2 X (Addon)
|
||||||
|
7.3 X (Addon)
|
||||||
|
7.4 X (Addon)
|
||||||
|
8.1 X (Addon)
|
||||||
|
8.2 X (Addon)
|
||||||
|
8.3 X (Cppcheck)
|
||||||
|
8.4 X (Addon)
|
||||||
|
8.5 X (Addon)
|
||||||
|
8.6 X (Addon)
|
||||||
|
8.7 X (Addon)
|
||||||
|
8.8 X (Addon)
|
||||||
|
8.9 X (Addon)
|
||||||
|
8.10 X (Addon)
|
||||||
|
8.11 X (Addon)
|
||||||
|
8.12 X (Addon)
|
||||||
|
8.13 X (Cppcheck)
|
||||||
|
8.14 X (Addon)
|
||||||
|
9.1 X (Cppcheck)
|
||||||
|
9.2 X (Addon)
|
||||||
|
9.3 X (Addon)
|
||||||
|
9.4 X (Addon)
|
||||||
|
9.5 X (Addon)
|
||||||
|
10.1 X (Addon)
|
||||||
|
10.2 X (Addon)
|
||||||
|
10.3 X (Addon)
|
||||||
|
10.4 X (Addon)
|
||||||
|
10.5 X (Addon)
|
||||||
|
10.6 X (Addon)
|
||||||
|
10.7 X (Addon)
|
||||||
|
10.8 X (Addon)
|
||||||
|
11.1 X (Addon)
|
||||||
|
11.2 X (Addon)
|
||||||
|
11.3 X (Addon)
|
||||||
|
11.4 X (Addon)
|
||||||
|
11.5 X (Addon)
|
||||||
|
11.6 X (Addon)
|
||||||
|
11.7 X (Addon)
|
||||||
|
11.8 X (Addon)
|
||||||
|
11.9 X (Addon)
|
||||||
|
12.1 X (Addon)
|
||||||
|
12.2 X (Addon)
|
||||||
|
12.3 X (Addon)
|
||||||
|
12.4 X (Addon)
|
||||||
|
13.1 X (Addon)
|
||||||
|
13.2 X (Cppcheck)
|
||||||
|
13.3 X (Addon)
|
||||||
|
13.4 X (Addon)
|
||||||
|
13.5 X (Addon)
|
||||||
|
13.6 X (Addon)
|
||||||
|
14.1 X (Addon)
|
||||||
|
14.2 X (Addon)
|
||||||
|
14.3 X (Cppcheck)
|
||||||
|
14.4 X (Addon)
|
||||||
|
15.1 X (Addon)
|
||||||
|
15.2 X (Addon)
|
||||||
|
15.3 X (Addon)
|
||||||
|
15.4 X (Addon)
|
||||||
|
15.5 X (Addon)
|
||||||
|
15.6 X (Addon)
|
||||||
|
15.7 X (Addon)
|
||||||
|
16.1 X (Addon)
|
||||||
|
16.2 X (Addon)
|
||||||
|
16.3 X (Addon)
|
||||||
|
16.4 X (Addon)
|
||||||
|
16.5 X (Addon)
|
||||||
|
16.6 X (Addon)
|
||||||
|
16.7 X (Addon)
|
||||||
|
17.1 X (Addon)
|
||||||
|
17.2 X (Addon)
|
||||||
|
17.3 X (Addon)
|
||||||
|
17.4 X (Cppcheck)
|
||||||
|
17.5 X (Cppcheck)
|
||||||
|
17.6 X (Addon)
|
||||||
|
17.7 X (Addon)
|
||||||
|
17.8 X (Addon)
|
||||||
|
18.1 X (Cppcheck)
|
||||||
|
18.2 X (Cppcheck)
|
||||||
|
18.3 X (Cppcheck)
|
||||||
|
18.4 X (Addon)
|
||||||
|
18.5 X (Addon)
|
||||||
|
18.6 X (Cppcheck)
|
||||||
|
18.7 X (Addon)
|
||||||
|
18.8 X (Addon)
|
||||||
|
19.1 X (Cppcheck)
|
||||||
|
19.2 X (Addon)
|
||||||
|
20.1 X (Addon)
|
||||||
|
20.2 X (Addon)
|
||||||
|
20.3 X (Addon)
|
||||||
|
20.4 X (Addon)
|
||||||
|
20.5 X (Addon)
|
||||||
|
20.6 X (Cppcheck)
|
||||||
|
20.7 X (Addon)
|
||||||
|
20.8 X (Addon)
|
||||||
|
20.9 X (Addon)
|
||||||
|
20.10 X (Addon)
|
||||||
|
20.11 X (Addon)
|
||||||
|
20.12 X (Addon)
|
||||||
|
20.13 X (Addon)
|
||||||
|
20.14 X (Addon)
|
||||||
|
21.1 X (Addon)
|
||||||
|
21.2 X (Addon)
|
||||||
|
21.3 X (Addon)
|
||||||
|
21.4 X (Addon)
|
||||||
|
21.5 X (Addon)
|
||||||
|
21.6 X (Addon)
|
||||||
|
21.7 X (Addon)
|
||||||
|
21.8 X (Addon)
|
||||||
|
21.9 X (Addon)
|
||||||
|
21.10 X (Addon)
|
||||||
|
21.11 X (Addon)
|
||||||
|
21.12 X (Addon)
|
||||||
|
21.13 X (Cppcheck)
|
||||||
|
21.14 X (Addon)
|
||||||
|
21.15 X (Addon)
|
||||||
|
21.16 X (Addon)
|
||||||
|
21.17 X (Cppcheck)
|
||||||
|
21.18 X (Cppcheck)
|
||||||
|
21.19 X (Addon)
|
||||||
|
21.20 X (Addon)
|
||||||
|
21.21 X (Addon)
|
||||||
|
22.1 X (Cppcheck)
|
||||||
|
22.2 X (Cppcheck)
|
||||||
|
22.3 X (Cppcheck)
|
||||||
|
22.4 X (Cppcheck)
|
||||||
|
22.5 X (Addon)
|
||||||
|
22.6 X (Cppcheck)
|
||||||
|
22.7 X (Addon)
|
||||||
|
22.8 X (Addon)
|
||||||
|
22.9 X (Addon)
|
||||||
|
22.10 X (Addon)
|
||||||
19
panda/tests/misra/install.sh
Normal file
19
panda/tests/misra/install.sh
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||||
|
: "${CPPCHECK_DIR:=$DIR/cppcheck/}"
|
||||||
|
|
||||||
|
if [ ! -d "$CPPCHECK_DIR" ]; then
|
||||||
|
git clone https://github.com/danmar/cppcheck.git $CPPCHECK_DIR
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd $CPPCHECK_DIR
|
||||||
|
|
||||||
|
VERS="2.13.0"
|
||||||
|
git fetch --all --tags
|
||||||
|
git checkout $VERS
|
||||||
|
git cherry-pick -n f6b538e855f0bacea33c4074664628024ef39dc6 b11b42087ff29569bc3740f5aa07eb6616ea4f63
|
||||||
|
|
||||||
|
#make clean
|
||||||
|
make MATCHCOMPILTER=yes CXXFLAGS="-O2" -j8
|
||||||
61
panda/tests/misra/test_misra.sh
Normal file
61
panda/tests/misra/test_misra.sh
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||||
|
PANDA_DIR=$(realpath $DIR/../../)
|
||||||
|
|
||||||
|
GREEN="\e[1;32m"
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
: "${CPPCHECK_DIR:=$DIR/cppcheck/}"
|
||||||
|
|
||||||
|
# install cppcheck if missing
|
||||||
|
if [ -z "${SKIP_BUILD}" ]; then
|
||||||
|
$DIR/install.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ensure checked in coverage table is up to date
|
||||||
|
cd $DIR
|
||||||
|
python $CPPCHECK_DIR/addons/misra.py -generate-table > new_table
|
||||||
|
if ! cmp -s new_table coverage_table; then
|
||||||
|
echo "MISRA coverage table doesn't match. Update and commit:"
|
||||||
|
echo "mv new_table coverage_table && git add . && git commit -m 'update table'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd $PANDA_DIR
|
||||||
|
if [ -z "${SKIP_BUILD}" ]; then
|
||||||
|
scons -j8
|
||||||
|
fi
|
||||||
|
|
||||||
|
cppcheck() {
|
||||||
|
# note that cppcheck build cache results in inconsistent results as of v2.13.0
|
||||||
|
OUTPUT=$DIR/.output.log
|
||||||
|
$CPPCHECK_DIR/cppcheck --force --inline-suppr -I $PANDA_DIR/board/ \
|
||||||
|
-I $gcc_inc "$(arm-none-eabi-gcc -print-file-name=include)" \
|
||||||
|
--suppressions-list=$DIR/suppressions.txt --suppress=*:*inc/* \
|
||||||
|
--suppress=*:*include/* --error-exitcode=2 --check-level=exhaustive \
|
||||||
|
--platform=arm32-wchar_t2 \
|
||||||
|
"$@" |& tee $OUTPUT
|
||||||
|
|
||||||
|
# cppcheck bug: some MISRA errors won't result in the error exit code,
|
||||||
|
# so check the output (https://trac.cppcheck.net/ticket/12440#no1)
|
||||||
|
if grep -e "misra violation" -e "error" -e "style: " $OUTPUT > /dev/null; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
PANDA_OPTS="--enable=all --disable=unusedFunction -DPANDA --addon=misra"
|
||||||
|
|
||||||
|
printf "\n${GREEN}** PANDA F4 CODE **${NC}\n"
|
||||||
|
cppcheck $PANDA_OPTS -DSTM32F4 -DUID_BASE $PANDA_DIR/board/main.c
|
||||||
|
|
||||||
|
printf "\n${GREEN}** PANDA H7 CODE **${NC}\n"
|
||||||
|
cppcheck $PANDA_OPTS -DSTM32H7 -DUID_BASE $PANDA_DIR/board/main.c
|
||||||
|
|
||||||
|
# unused needs to run globally
|
||||||
|
#printf "\n${GREEN}** UNUSED ALL CODE **${NC}\n"
|
||||||
|
#cppcheck --enable=unusedFunction --quiet $PANDA_DIR/board/
|
||||||
|
|
||||||
|
printf "\n${GREEN}Success!${NC} took $SECONDS seconds\n"
|
||||||
|
|
||||||
84
panda/tests/misra/test_mutation.py
Normal file
84
panda/tests/misra/test_mutation.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import pytest
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import random
|
||||||
|
|
||||||
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
ROOT = os.path.join(HERE, "../../")
|
||||||
|
|
||||||
|
IGNORED_PATHS = (
|
||||||
|
'board/obj',
|
||||||
|
'board/jungle',
|
||||||
|
'board/stm32h7/inc',
|
||||||
|
'board/stm32fx/inc',
|
||||||
|
'board/fake_stm.h',
|
||||||
|
|
||||||
|
# bootstub only files
|
||||||
|
'board/flasher.h',
|
||||||
|
'board/bootstub.c',
|
||||||
|
'board/bootstub_declarations.h',
|
||||||
|
'board/stm32fx/llflash.h'
|
||||||
|
)
|
||||||
|
|
||||||
|
mutations = [
|
||||||
|
# default
|
||||||
|
(None, None, False),
|
||||||
|
# F4 only
|
||||||
|
("board/stm32fx/llbxcan.h", "s/1U/1/g", True),
|
||||||
|
# H7 only
|
||||||
|
("board/stm32h7/llfdcan.h", "s/return ret;/if (true) { return ret; } else { return false; }/g", True),
|
||||||
|
# general safety
|
||||||
|
("board/safety/safety_toyota.h", "s/is_lkas_msg =.*;/is_lkas_msg = addr == 1 || addr == 2;/g", True),
|
||||||
|
]
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
# misra-c2012-13.3
|
||||||
|
"$a void test(int tmp) { int tmp2 = tmp++ + 2; if (tmp2) {;}}",
|
||||||
|
# misra-c2012-13.4
|
||||||
|
"$a int test(int x, int y) { return (x=2) && (y=2); }",
|
||||||
|
# misra-c2012-13.5
|
||||||
|
"$a void test(int tmp) { if (true && tmp++) {;} }",
|
||||||
|
# misra-c2012-13.6
|
||||||
|
"$a void test(int tmp) { if (sizeof(tmp++)) {;} }",
|
||||||
|
# misra-c2012-14.1
|
||||||
|
"$a void test(float len) { for (float j = 0; j < len; j++) {;} }",
|
||||||
|
# misra-c2012-14.4
|
||||||
|
"$a void test(int len) { if (len - 8) {;} }",
|
||||||
|
# misra-c2012-16.4
|
||||||
|
r"$a void test(int temp) {switch (temp) { case 1: ; }}\n",
|
||||||
|
# misra-c2012-17.8
|
||||||
|
"$a void test(int cnt) { for (cnt=0;;cnt++) {;} }",
|
||||||
|
# misra-c2012-20.4
|
||||||
|
r"$a #define auto 1\n",
|
||||||
|
# misra-c2012-20.5
|
||||||
|
r"$a #define TEST 1\n#undef TEST\n",
|
||||||
|
]
|
||||||
|
|
||||||
|
all_files = glob.glob('board/**', root_dir=ROOT, recursive=True)
|
||||||
|
files = [f for f in all_files if f.endswith(('.c', '.h')) and not f.startswith(IGNORED_PATHS)]
|
||||||
|
assert len(files) > 70, all(d in files for d in ('board/main.c', 'board/stm32fx/llbxcan.h', 'board/stm32h7/llfdcan.h', 'board/safety/safety_toyota.h'))
|
||||||
|
|
||||||
|
for p in patterns:
|
||||||
|
mutations.append((random.choice(files), p, True))
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fn, patch, should_fail", mutations)
|
||||||
|
def test_misra_mutation(fn, patch, should_fail):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
shutil.copytree(ROOT, tmp, dirs_exist_ok=True)
|
||||||
|
|
||||||
|
# apply patch
|
||||||
|
if fn is not None:
|
||||||
|
r = os.system(f"cd {tmp} && sed -i '{patch}' {fn}")
|
||||||
|
assert r == 0
|
||||||
|
|
||||||
|
# run test
|
||||||
|
r = subprocess.run("tests/misra/test_misra.sh", cwd=tmp, shell=True)
|
||||||
|
failed = r.returncode != 0
|
||||||
|
assert failed == should_fail
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-n 8"])
|
||||||
32
panda/tests/read_flash_spi.py
Normal file
32
panda/tests/read_flash_spi.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from panda import Panda, PandaDFU
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
from openpilot.system.hardware import HARDWARE
|
||||||
|
HARDWARE.recover_internal_panda()
|
||||||
|
Panda.wait_for_dfu(None, 5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
p = PandaDFU(None)
|
||||||
|
cfg = p.get_mcu_type().config
|
||||||
|
|
||||||
|
def readmem(addr, length, fn):
|
||||||
|
print(f"reading {hex(addr)} {hex(length)} bytes to {fn}")
|
||||||
|
max_size = 255
|
||||||
|
with open(fn, "wb") as f:
|
||||||
|
to_read = length
|
||||||
|
while to_read > 0:
|
||||||
|
l = min(to_read, max_size)
|
||||||
|
dat = p._handle.read(addr, l)
|
||||||
|
assert len(dat) == l
|
||||||
|
f.write(dat)
|
||||||
|
|
||||||
|
to_read -= len(dat)
|
||||||
|
addr += len(dat)
|
||||||
|
|
||||||
|
addr = cfg.bootstub_address
|
||||||
|
for i, sector_size in enumerate(cfg.sector_sizes):
|
||||||
|
readmem(addr, sector_size, f"sector_{i}.bin")
|
||||||
|
addr += sector_size
|
||||||
6
panda/tests/read_st_flash.sh
Normal file
6
panda/tests/read_st_flash.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
rm -f /tmp/dump_bootstub
|
||||||
|
rm -f /tmp/dump_main
|
||||||
|
dfu-util -a 0 -s 0x08000000 -U /tmp/dump_bootstub
|
||||||
|
dfu-util -a 0 -s 0x08004000 -U /tmp/dump_main
|
||||||
|
|
||||||
34
panda/tests/read_winusb_descriptors.py
Normal file
34
panda/tests/read_winusb_descriptors.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# type: ignore
|
||||||
|
from panda import Panda
|
||||||
|
from hexdump import hexdump
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = Panda()
|
||||||
|
|
||||||
|
length = p._handle.controlRead(Panda.REQUEST_IN, 0x06, 3 << 8 | 238, 0, 1)
|
||||||
|
print('Microsoft OS String Descriptor')
|
||||||
|
dat = p._handle.controlRead(Panda.REQUEST_IN, 0x06, 3 << 8 | 238, 0, length[0])
|
||||||
|
if DEBUG:
|
||||||
|
print(f'LEN: {hex(length[0])}')
|
||||||
|
hexdump("".join(map(chr, dat)))
|
||||||
|
|
||||||
|
ms_vendor_code = dat[16]
|
||||||
|
if DEBUG:
|
||||||
|
print(f'MS_VENDOR_CODE: {hex(length[0])}')
|
||||||
|
|
||||||
|
print('\nMicrosoft Compatible ID Feature Descriptor')
|
||||||
|
length = p._handle.controlRead(Panda.REQUEST_IN, ms_vendor_code, 0, 4, 1)
|
||||||
|
if DEBUG:
|
||||||
|
print(f'LEN: {hex(length[0])}')
|
||||||
|
dat = p._handle.controlRead(Panda.REQUEST_IN, ms_vendor_code, 0, 4, length[0])
|
||||||
|
hexdump("".join(map(chr, dat)))
|
||||||
|
|
||||||
|
print('\nMicrosoft Extended Properties Feature Descriptor')
|
||||||
|
length = p._handle.controlRead(Panda.REQUEST_IN, ms_vendor_code, 0, 5, 1)
|
||||||
|
if DEBUG:
|
||||||
|
print(f'LEN: {hex(length[0])}')
|
||||||
|
dat = p._handle.controlRead(Panda.REQUEST_IN, ms_vendor_code, 0, 5, length[0])
|
||||||
|
hexdump("".join(map(chr, dat)))
|
||||||
49
panda/tests/reflash_internal_panda.py
Normal file
49
panda/tests/reflash_internal_panda.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import time
|
||||||
|
from panda import Panda, PandaDFU
|
||||||
|
|
||||||
|
class GPIO:
|
||||||
|
STM_RST_N = 124
|
||||||
|
STM_BOOT0 = 134
|
||||||
|
HUB_RST_N = 30
|
||||||
|
|
||||||
|
|
||||||
|
def gpio_init(pin, output):
|
||||||
|
with open(f"/sys/class/gpio/gpio{pin}/direction", 'wb') as f:
|
||||||
|
f.write(b"out" if output else b"in")
|
||||||
|
|
||||||
|
def gpio_set(pin, high):
|
||||||
|
with open(f"/sys/class/gpio/gpio{pin}/value", 'wb') as f:
|
||||||
|
f.write(b"1" if high else b"0")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for pin in (GPIO.STM_RST_N, GPIO.STM_BOOT0, GPIO.HUB_RST_N):
|
||||||
|
gpio_init(pin, True)
|
||||||
|
|
||||||
|
# reset USB hub
|
||||||
|
gpio_set(GPIO.HUB_RST_N, 0)
|
||||||
|
time.sleep(0.5)
|
||||||
|
gpio_set(GPIO.HUB_RST_N, 1)
|
||||||
|
|
||||||
|
# flash bootstub
|
||||||
|
print("resetting into DFU")
|
||||||
|
gpio_set(GPIO.STM_RST_N, 1)
|
||||||
|
gpio_set(GPIO.STM_BOOT0, 1)
|
||||||
|
time.sleep(1)
|
||||||
|
gpio_set(GPIO.STM_RST_N, 0)
|
||||||
|
gpio_set(GPIO.STM_BOOT0, 0)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print("flashing bootstub")
|
||||||
|
PandaDFU(None).recover()
|
||||||
|
|
||||||
|
gpio_set(GPIO.STM_RST_N, 1)
|
||||||
|
time.sleep(0.5)
|
||||||
|
gpio_set(GPIO.STM_RST_N, 0)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print("flashing app")
|
||||||
|
p = Panda()
|
||||||
|
assert p.bootstub
|
||||||
|
p.flash()
|
||||||
16
panda/tests/relay_test.py
Normal file
16
panda/tests/relay_test.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import time
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
p = Panda()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
p.set_safety_mode(Panda.SAFETY_TOYOTA)
|
||||||
|
p.send_heartbeat()
|
||||||
|
print("ON")
|
||||||
|
time.sleep(1)
|
||||||
|
p.set_safety_mode(Panda.SAFETY_NOOUTPUT)
|
||||||
|
p.send_heartbeat()
|
||||||
|
print("OFF")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
27
panda/tests/restore_flash_spi.py
Normal file
27
panda/tests/restore_flash_spi.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from panda import Panda, PandaDFU, STBootloaderSPIHandle
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
from openpilot.system.hardware import HARDWARE
|
||||||
|
HARDWARE.recover_internal_panda()
|
||||||
|
Panda.wait_for_dfu(None, 5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
p = PandaDFU(None)
|
||||||
|
assert isinstance(p._handle, STBootloaderSPIHandle)
|
||||||
|
cfg = p.get_mcu_type().config
|
||||||
|
|
||||||
|
print("restoring from backup...")
|
||||||
|
addr = cfg.bootstub_address
|
||||||
|
for i, sector_size in enumerate(cfg.sector_sizes):
|
||||||
|
print(f"- sector #{i}")
|
||||||
|
p._handle.erase_sector(i)
|
||||||
|
with open(f"sector_{i}.bin", "rb") as f:
|
||||||
|
dat = f.read()
|
||||||
|
assert len(dat) == sector_size
|
||||||
|
p._handle.program(addr, dat)
|
||||||
|
addr += len(dat)
|
||||||
|
|
||||||
|
p.reset()
|
||||||
10
panda/tests/rtc_test.py
Normal file
10
panda/tests/rtc_test.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = Panda()
|
||||||
|
|
||||||
|
p.set_datetime(datetime.datetime.now())
|
||||||
|
print(p.get_datetime())
|
||||||
0
panda/tests/safety/__init__.py
Normal file
0
panda/tests/safety/__init__.py
Normal file
1071
panda/tests/safety/common.py
Normal file
1071
panda/tests/safety/common.py
Normal file
File diff suppressed because it is too large
Load Diff
157
panda/tests/safety/hyundai_common.py
Normal file
157
panda/tests/safety/hyundai_common.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
from panda.tests.safety.common import make_msg
|
||||||
|
|
||||||
|
|
||||||
|
class Buttons:
|
||||||
|
NONE = 0
|
||||||
|
RESUME = 1
|
||||||
|
SET = 2
|
||||||
|
CANCEL = 4
|
||||||
|
|
||||||
|
|
||||||
|
PREV_BUTTON_SAMPLES = 8
|
||||||
|
ENABLE_BUTTONS = (Buttons.RESUME, Buttons.SET, Buttons.CANCEL)
|
||||||
|
|
||||||
|
|
||||||
|
class HyundaiButtonBase:
|
||||||
|
# pylint: disable=no-member,abstract-method
|
||||||
|
BUTTONS_TX_BUS = 0 # tx on this bus, rx on 0
|
||||||
|
SCC_BUS = 0 # rx on this bus
|
||||||
|
|
||||||
|
def test_button_sends(self):
|
||||||
|
"""
|
||||||
|
Only RES and CANCEL buttons are allowed
|
||||||
|
- RES allowed while controls allowed
|
||||||
|
- CANCEL allowed while cruise is enabled
|
||||||
|
"""
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
self.assertFalse(self._tx(self._button_msg(Buttons.RESUME, bus=self.BUTTONS_TX_BUS)))
|
||||||
|
self.assertFalse(self._tx(self._button_msg(Buttons.SET, bus=self.BUTTONS_TX_BUS)))
|
||||||
|
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self.assertTrue(self._tx(self._button_msg(Buttons.RESUME, bus=self.BUTTONS_TX_BUS)))
|
||||||
|
self.assertFalse(self._tx(self._button_msg(Buttons.SET, bus=self.BUTTONS_TX_BUS)))
|
||||||
|
|
||||||
|
for enabled in (True, False):
|
||||||
|
self._rx(self._pcm_status_msg(enabled))
|
||||||
|
self.assertEqual(enabled, self._tx(self._button_msg(Buttons.CANCEL, bus=self.BUTTONS_TX_BUS)))
|
||||||
|
|
||||||
|
def test_enable_control_allowed_from_cruise(self):
|
||||||
|
"""
|
||||||
|
Hyundai non-longitudinal only enables on PCM rising edge and recent button press. Tests PCM enabling with:
|
||||||
|
- disallowed: No buttons
|
||||||
|
- disallowed: Buttons that don't enable cruise
|
||||||
|
- allowed: Buttons that do enable cruise
|
||||||
|
- allowed: Main button with all above combinations
|
||||||
|
"""
|
||||||
|
for main_button in (0, 1):
|
||||||
|
for btn in range(8):
|
||||||
|
for _ in range(PREV_BUTTON_SAMPLES): # reset
|
||||||
|
self._rx(self._button_msg(Buttons.NONE))
|
||||||
|
|
||||||
|
self._rx(self._pcm_status_msg(False))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
self._rx(self._button_msg(btn, main_button=main_button))
|
||||||
|
self._rx(self._pcm_status_msg(True))
|
||||||
|
controls_allowed = btn in ENABLE_BUTTONS or main_button
|
||||||
|
self.assertEqual(controls_allowed, self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_sampling_cruise_buttons(self):
|
||||||
|
"""
|
||||||
|
Test that we allow controls on recent button press, but not as button leaves sliding window
|
||||||
|
"""
|
||||||
|
self._rx(self._button_msg(Buttons.SET))
|
||||||
|
for i in range(2 * PREV_BUTTON_SAMPLES):
|
||||||
|
self._rx(self._pcm_status_msg(False))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
self._rx(self._pcm_status_msg(True))
|
||||||
|
controls_allowed = i < PREV_BUTTON_SAMPLES
|
||||||
|
self.assertEqual(controls_allowed, self.safety.get_controls_allowed())
|
||||||
|
self._rx(self._button_msg(Buttons.NONE))
|
||||||
|
|
||||||
|
|
||||||
|
class HyundaiLongitudinalBase(common.LongitudinalAccelSafetyTest):
|
||||||
|
# pylint: disable=no-member,abstract-method
|
||||||
|
|
||||||
|
DISABLED_ECU_UDS_MSG: tuple[int, int]
|
||||||
|
DISABLED_ECU_ACTUATION_MSG: tuple[int, int]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls.__name__ == "HyundaiLongitudinalBase":
|
||||||
|
cls.safety = None
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
# override these tests from PandaCarSafetyTest, hyundai longitudinal uses button enable
|
||||||
|
def test_disable_control_allowed_from_cruise(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_enable_control_allowed_from_cruise(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_sampling_cruise_buttons(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_cruise_engaged_prev(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_button_sends(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
raise Exception
|
||||||
|
|
||||||
|
def _accel_msg(self, accel, aeb_req=False, aeb_decel=0):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def test_set_resume_buttons(self):
|
||||||
|
"""
|
||||||
|
SET and RESUME enter controls allowed on their falling edge.
|
||||||
|
"""
|
||||||
|
for btn_prev in range(8):
|
||||||
|
for btn_cur in range(8):
|
||||||
|
self._rx(self._button_msg(Buttons.NONE))
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
for _ in range(10):
|
||||||
|
self._rx(self._button_msg(btn_prev))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
# should enter controls allowed on falling edge and not transitioning to cancel
|
||||||
|
should_enable = btn_cur != btn_prev and \
|
||||||
|
btn_cur != Buttons.CANCEL and \
|
||||||
|
btn_prev in (Buttons.RESUME, Buttons.SET)
|
||||||
|
|
||||||
|
self._rx(self._button_msg(btn_cur))
|
||||||
|
self.assertEqual(should_enable, self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_cancel_button(self):
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._button_msg(Buttons.CANCEL))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_tester_present_allowed(self):
|
||||||
|
"""
|
||||||
|
Ensure tester present diagnostic message is allowed to keep ECU knocked out
|
||||||
|
for longitudinal control.
|
||||||
|
"""
|
||||||
|
|
||||||
|
addr, bus = self.DISABLED_ECU_UDS_MSG
|
||||||
|
tester_present = libpanda_py.make_CANPacket(addr, bus, b"\x02\x3E\x80\x00\x00\x00\x00\x00")
|
||||||
|
self.assertTrue(self._tx(tester_present))
|
||||||
|
|
||||||
|
not_tester_present = libpanda_py.make_CANPacket(addr, bus, b"\x03\xAA\xAA\x00\x00\x00\x00\x00")
|
||||||
|
self.assertFalse(self._tx(not_tester_present))
|
||||||
|
|
||||||
|
def test_disabled_ecu_alive(self):
|
||||||
|
"""
|
||||||
|
If the ECU knockout failed, make sure the relay malfunction is shown
|
||||||
|
"""
|
||||||
|
|
||||||
|
addr, bus = self.DISABLED_ECU_ACTUATION_MSG
|
||||||
|
self.assertFalse(self.safety.get_relay_malfunction())
|
||||||
|
self._rx(make_msg(bus, addr, 8))
|
||||||
|
self.assertTrue(self.safety.get_relay_malfunction())
|
||||||
|
|
||||||
34
panda/tests/safety/test.sh
Normal file
34
panda/tests/safety/test.sh
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||||
|
cd $DIR
|
||||||
|
|
||||||
|
# reset coverage data and generate gcc note file
|
||||||
|
rm -f ../libpanda/*.gcda
|
||||||
|
scons -j$(nproc) -D --coverage
|
||||||
|
|
||||||
|
# run safety tests and generate coverage data
|
||||||
|
HW_TYPES=( 6 9 )
|
||||||
|
for hw_type in "${HW_TYPES[@]}"; do
|
||||||
|
echo "Testing HW_TYPE: $hw_type"
|
||||||
|
HW_TYPE=$hw_type pytest test_*.py
|
||||||
|
done
|
||||||
|
|
||||||
|
# generate and open report
|
||||||
|
if [ "$1" == "--report" ]; then
|
||||||
|
geninfo ../libpanda/ -o coverage.info
|
||||||
|
genhtml coverage.info -o coverage-out
|
||||||
|
sensible-browser coverage-out/index.html
|
||||||
|
fi
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
GCOV_OUTPUT=$(gcov -n ../libpanda/panda.c)
|
||||||
|
INCOMPLETE_COVERAGE=$(echo "$GCOV_OUTPUT" | paste -s -d' \n' | grep -E "File.*(safety\/safety_.*)|(safety)\.h" | grep -v "100.00%" || true)
|
||||||
|
if [ -n "$INCOMPLETE_COVERAGE" ]; then
|
||||||
|
echo "FAILED: Some files have less than 100% coverage:"
|
||||||
|
echo "$INCOMPLETE_COVERAGE"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "SUCCESS: All checked files have 100% coverage!"
|
||||||
|
fi
|
||||||
70
panda/tests/safety/test_body.py
Normal file
70
panda/tests/safety/test_body.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
|
||||||
|
|
||||||
|
class TestBody(common.PandaSafetyTest):
|
||||||
|
TX_MSGS = [[0x250, 0], [0x251, 0], [0x350, 0], [0x351, 0],
|
||||||
|
[0x1, 0], [0x1, 1], [0x1, 2], [0x1, 3]]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("comma_body")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_BODY, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _motors_data_msg(self, speed_l, speed_r):
|
||||||
|
values = {"SPEED_L": speed_l, "SPEED_R": speed_r}
|
||||||
|
return self.packer.make_can_msg_panda("MOTORS_DATA", 0, values)
|
||||||
|
|
||||||
|
def _torque_cmd_msg(self, torque_l, torque_r):
|
||||||
|
values = {"TORQUE_L": torque_l, "TORQUE_R": torque_r}
|
||||||
|
return self.packer.make_can_msg_panda("TORQUE_CMD", 0, values)
|
||||||
|
|
||||||
|
def _knee_torque_cmd_msg(self, torque_l, torque_r):
|
||||||
|
values = {"TORQUE_L": torque_l, "TORQUE_R": torque_r}
|
||||||
|
return self.packer.make_can_msg_panda("KNEE_TORQUE_CMD", 0, values)
|
||||||
|
|
||||||
|
def _max_motor_rpm_cmd_msg(self, max_rpm_l, max_rpm_r):
|
||||||
|
values = {"MAX_RPM_L": max_rpm_l, "MAX_RPM_R": max_rpm_r}
|
||||||
|
return self.packer.make_can_msg_panda("MAX_MOTOR_RPM_CMD", 0, values)
|
||||||
|
|
||||||
|
def test_rx_hook(self):
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
self.assertFalse(self.safety.get_vehicle_moving())
|
||||||
|
|
||||||
|
# controls allowed when we get MOTORS_DATA message
|
||||||
|
self.assertTrue(self._rx(self._torque_cmd_msg(0, 0)))
|
||||||
|
self.assertTrue(self.safety.get_vehicle_moving()) # always moving
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
self.assertTrue(self._rx(self._motors_data_msg(0, 0)))
|
||||||
|
self.assertTrue(self.safety.get_vehicle_moving()) # always moving
|
||||||
|
self.assertTrue(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_tx_hook(self):
|
||||||
|
self.assertFalse(self._tx(self._torque_cmd_msg(0, 0)))
|
||||||
|
self.assertFalse(self._tx(self._knee_torque_cmd_msg(0, 0)))
|
||||||
|
self.safety.set_controls_allowed(True)
|
||||||
|
self.assertTrue(self._tx(self._torque_cmd_msg(0, 0)))
|
||||||
|
self.assertTrue(self._tx(self._knee_torque_cmd_msg(0, 0)))
|
||||||
|
|
||||||
|
def test_can_flasher(self):
|
||||||
|
# CAN flasher always allowed
|
||||||
|
self.safety.set_controls_allowed(False)
|
||||||
|
self.assertTrue(self._tx(common.make_msg(0, 0x1, 8)))
|
||||||
|
|
||||||
|
# 0xdeadfaceU enters CAN flashing mode for base & knee
|
||||||
|
for addr in (0x250, 0x350):
|
||||||
|
self.assertTrue(self._tx(common.make_msg(0, addr, dat=b'\xce\xfa\xad\xde\x1e\x0b\xb0\x0a')))
|
||||||
|
self.assertFalse(self._tx(common.make_msg(0, addr, dat=b'\xce\xfa\xad\xde\x1e\x0b\xb0'))) # not correct data/len
|
||||||
|
self.assertFalse(self._tx(common.make_msg(0, addr + 1, dat=b'\xce\xfa\xad\xde\x1e\x0b\xb0\x0a'))) # wrong address
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
125
panda/tests/safety/test_chrysler.py
Normal file
125
panda/tests/safety/test_chrysler.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
|
||||||
|
|
||||||
|
class TestChryslerSafety(common.PandaCarSafetyTest, common.MotorTorqueSteeringSafetyTest):
|
||||||
|
TX_MSGS = [[0x23B, 0], [0x292, 0], [0x2A6, 0]]
|
||||||
|
STANDSTILL_THRESHOLD = 0
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x292,)}
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x292, 0x2A6]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
MAX_RATE_UP = 3
|
||||||
|
MAX_RATE_DOWN = 3
|
||||||
|
MAX_TORQUE = 261
|
||||||
|
MAX_RT_DELTA = 112
|
||||||
|
RT_INTERVAL = 250000
|
||||||
|
MAX_TORQUE_ERROR = 80
|
||||||
|
|
||||||
|
LKAS_ACTIVE_VALUE = 1
|
||||||
|
|
||||||
|
DAS_BUS = 0
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("chrysler_pacifica_2017_hybrid_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_CHRYSLER, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _button_msg(self, cancel=False, resume=False):
|
||||||
|
values = {"ACC_Cancel": cancel, "ACC_Resume": resume}
|
||||||
|
return self.packer.make_can_msg_panda("CRUISE_BUTTONS", self.DAS_BUS, values)
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
values = {"ACC_ACTIVE": enable}
|
||||||
|
return self.packer.make_can_msg_panda("DAS_3", self.DAS_BUS, values)
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {"SPEED_LEFT": speed, "SPEED_RIGHT": speed}
|
||||||
|
return self.packer.make_can_msg_panda("SPEED_1", 0, values)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"Accelerator_Position": gas}
|
||||||
|
return self.packer.make_can_msg_panda("ECM_5", 0, values)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"Brake_Pedal_State": 1 if brake else 0}
|
||||||
|
return self.packer.make_can_msg_panda("ESP_1", 0, values)
|
||||||
|
|
||||||
|
def _torque_meas_msg(self, torque):
|
||||||
|
values = {"EPS_TORQUE_MOTOR": torque}
|
||||||
|
return self.packer.make_can_msg_panda("EPS_2", 0, values)
|
||||||
|
|
||||||
|
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||||
|
values = {"STEERING_TORQUE": torque, "LKAS_CONTROL_BIT": self.LKAS_ACTIVE_VALUE if steer_req else 0}
|
||||||
|
return self.packer.make_can_msg_panda("LKAS_COMMAND", 0, values)
|
||||||
|
|
||||||
|
def test_buttons(self):
|
||||||
|
for controls_allowed in (True, False):
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
|
||||||
|
# resume only while controls allowed
|
||||||
|
self.assertEqual(controls_allowed, self._tx(self._button_msg(resume=True)))
|
||||||
|
|
||||||
|
# can always cancel
|
||||||
|
self.assertTrue(self._tx(self._button_msg(cancel=True)))
|
||||||
|
|
||||||
|
# only one button at a time
|
||||||
|
self.assertFalse(self._tx(self._button_msg(cancel=True, resume=True)))
|
||||||
|
self.assertFalse(self._tx(self._button_msg(cancel=False, resume=False)))
|
||||||
|
|
||||||
|
|
||||||
|
class TestChryslerRamDTSafety(TestChryslerSafety):
|
||||||
|
TX_MSGS = [[0xB1, 2], [0xA6, 0], [0xFA, 0]]
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0xA6,)}
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0xA6, 0xFA]}
|
||||||
|
|
||||||
|
MAX_RATE_UP = 6
|
||||||
|
MAX_RATE_DOWN = 6
|
||||||
|
MAX_TORQUE = 350
|
||||||
|
|
||||||
|
DAS_BUS = 2
|
||||||
|
|
||||||
|
LKAS_ACTIVE_VALUE = 2
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("chrysler_ram_dt_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_CHRYSLER, Panda.FLAG_CHRYSLER_RAM_DT)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {"Vehicle_Speed": speed}
|
||||||
|
return self.packer.make_can_msg_panda("ESP_8", 0, values)
|
||||||
|
|
||||||
|
class TestChryslerRamHDSafety(TestChryslerSafety):
|
||||||
|
TX_MSGS = [[0x275, 0], [0x276, 0], [0x23A, 2]]
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x276,)}
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x275, 0x276]}
|
||||||
|
|
||||||
|
MAX_TORQUE = 361
|
||||||
|
MAX_RATE_UP = 14
|
||||||
|
MAX_RATE_DOWN = 14
|
||||||
|
MAX_RT_DELTA = 182
|
||||||
|
|
||||||
|
DAS_BUS = 2
|
||||||
|
|
||||||
|
LKAS_ACTIVE_VALUE = 2
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("chrysler_ram_hd_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_CHRYSLER, Panda.FLAG_CHRYSLER_RAM_HD)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {"Vehicle_Speed": speed}
|
||||||
|
return self.packer.make_can_msg_panda("ESP_8", 0, values)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
73
panda/tests/safety/test_defaults.py
Normal file
73
panda/tests/safety/test_defaults.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
|
||||||
|
|
||||||
|
class TestDefaultRxHookBase(common.PandaSafetyTest):
|
||||||
|
def test_rx_hook(self):
|
||||||
|
# default rx hook allows all msgs
|
||||||
|
for bus in range(4):
|
||||||
|
for addr in self.SCANNED_ADDRS:
|
||||||
|
self.assertTrue(self._rx(common.make_msg(bus, addr, 8)), f"failed RX {addr=}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestNoOutput(TestDefaultRxHookBase):
|
||||||
|
TX_MSGS = []
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_NOOUTPUT, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestSilent(TestNoOutput):
|
||||||
|
"""SILENT uses same hooks as NOOUTPUT"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_SILENT, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAllOutput(TestDefaultRxHookBase):
|
||||||
|
# Allow all messages
|
||||||
|
TX_MSGS = [[addr, bus] for addr in common.PandaSafetyTest.SCANNED_ADDRS
|
||||||
|
for bus in range(4)]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_ALLOUTPUT, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def test_spam_can_buses(self):
|
||||||
|
# asserts tx allowed for all scanned addrs
|
||||||
|
for bus in range(4):
|
||||||
|
for addr in self.SCANNED_ADDRS:
|
||||||
|
should_tx = [addr, bus] in self.TX_MSGS
|
||||||
|
self.assertEqual(should_tx, self._tx(common.make_msg(bus, addr, 8)), f"allowed TX {addr=} {bus=}")
|
||||||
|
|
||||||
|
def test_default_controls_not_allowed(self):
|
||||||
|
# controls always allowed
|
||||||
|
self.assertTrue(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_tx_hook_on_wrong_safety_mode(self):
|
||||||
|
# No point, since we allow all messages
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestAllOutputPassthrough(TestAllOutput):
|
||||||
|
FWD_BLACKLISTED_ADDRS = {}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_ALLOUTPUT, 1)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
48
panda/tests/safety/test_elm327.py
Normal file
48
panda/tests/safety/test_elm327.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
|
||||||
|
from panda import DLC_TO_LEN, Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
from panda.tests.safety.test_defaults import TestDefaultRxHookBase
|
||||||
|
|
||||||
|
GM_CAMERA_DIAG_ADDR = 0x24B
|
||||||
|
|
||||||
|
|
||||||
|
class TestElm327(TestDefaultRxHookBase):
|
||||||
|
TX_MSGS = [[addr, bus] for addr in [GM_CAMERA_DIAG_ADDR, *range(0x600, 0x800),
|
||||||
|
*range(0x18DA00F1, 0x18DB00F1, 0x100), # 29-bit UDS physical addressing
|
||||||
|
*[0x18DB33F1], # 29-bit UDS functional address
|
||||||
|
] for bus in range(4)]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_ELM327, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def test_tx_hook(self):
|
||||||
|
# ensure we can transmit arbitrary data on allowed addresses
|
||||||
|
for bus in range(4):
|
||||||
|
for addr in self.SCANNED_ADDRS:
|
||||||
|
should_tx = [addr, bus] in self.TX_MSGS
|
||||||
|
self.assertEqual(should_tx, self._tx(common.make_msg(bus, addr, 8)))
|
||||||
|
|
||||||
|
# ELM only allows 8 byte UDS/KWP messages under ISO 15765-4
|
||||||
|
for msg_len in DLC_TO_LEN:
|
||||||
|
should_tx = msg_len == 8
|
||||||
|
self.assertEqual(should_tx, self._tx(common.make_msg(0, 0x700, msg_len)))
|
||||||
|
|
||||||
|
# TODO: perform this check for all addresses
|
||||||
|
# 4 to 15 are reserved ISO-TP frame types (https://en.wikipedia.org/wiki/ISO_15765-2)
|
||||||
|
for byte in range(0xff):
|
||||||
|
should_tx = (byte >> 4) <= 3
|
||||||
|
self.assertEqual(should_tx, self._tx(common.make_msg(0, GM_CAMERA_DIAG_ADDR, dat=bytes([byte] * 8))))
|
||||||
|
|
||||||
|
def test_tx_hook_on_wrong_safety_mode(self):
|
||||||
|
# No point, since we allow many diagnostic addresses
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
476
panda/tests/safety/test_ford.py
Normal file
476
panda/tests/safety/test_ford.py
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import numpy as np
|
||||||
|
import random
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
|
||||||
|
MSG_EngBrakeData = 0x165 # RX from PCM, for driver brake pedal and cruise state
|
||||||
|
MSG_EngVehicleSpThrottle = 0x204 # RX from PCM, for driver throttle input
|
||||||
|
MSG_BrakeSysFeatures = 0x415 # RX from ABS, for vehicle speed
|
||||||
|
MSG_EngVehicleSpThrottle2 = 0x202 # RX from PCM, for second vehicle speed
|
||||||
|
MSG_Yaw_Data_FD1 = 0x91 # RX from RCM, for yaw rate
|
||||||
|
MSG_Steering_Data_FD1 = 0x083 # TX by OP, various driver switches and LKAS/CC buttons
|
||||||
|
MSG_ACCDATA = 0x186 # TX by OP, ACC controls
|
||||||
|
MSG_ACCDATA_3 = 0x18A # TX by OP, ACC/TJA user interface
|
||||||
|
MSG_Lane_Assist_Data1 = 0x3CA # TX by OP, Lane Keep Assist
|
||||||
|
MSG_LateralMotionControl = 0x3D3 # TX by OP, Lateral Control message
|
||||||
|
MSG_LateralMotionControl2 = 0x3D6 # TX by OP, alternate Lateral Control message
|
||||||
|
MSG_IPMA_Data = 0x3D8 # TX by OP, IPMA and LKAS user interface
|
||||||
|
|
||||||
|
|
||||||
|
def checksum(msg):
|
||||||
|
addr, t, dat, bus = msg
|
||||||
|
ret = bytearray(dat)
|
||||||
|
|
||||||
|
if addr == MSG_Yaw_Data_FD1:
|
||||||
|
chksum = dat[0] + dat[1] # VehRol_W_Actl
|
||||||
|
chksum += dat[2] + dat[3] # VehYaw_W_Actl
|
||||||
|
chksum += dat[5] # VehRollYaw_No_Cnt
|
||||||
|
chksum += dat[6] >> 6 # VehRolWActl_D_Qf
|
||||||
|
chksum += (dat[6] >> 4) & 0x3 # VehYawWActl_D_Qf
|
||||||
|
chksum = 0xff - (chksum & 0xff)
|
||||||
|
ret[4] = chksum
|
||||||
|
|
||||||
|
elif addr == MSG_BrakeSysFeatures:
|
||||||
|
chksum = dat[0] + dat[1] # Veh_V_ActlBrk
|
||||||
|
chksum += (dat[2] >> 2) & 0xf # VehVActlBrk_No_Cnt
|
||||||
|
chksum += dat[2] >> 6 # VehVActlBrk_D_Qf
|
||||||
|
chksum = 0xff - (chksum & 0xff)
|
||||||
|
ret[3] = chksum
|
||||||
|
|
||||||
|
elif addr == MSG_EngVehicleSpThrottle2:
|
||||||
|
chksum = (dat[2] >> 3) & 0xf # VehVActlEng_No_Cnt
|
||||||
|
chksum += (dat[4] >> 5) & 0x3 # VehVActlEng_D_Qf
|
||||||
|
chksum += dat[6] + dat[7] # Veh_V_ActlEng
|
||||||
|
chksum = 0xff - (chksum & 0xff)
|
||||||
|
ret[1] = chksum
|
||||||
|
|
||||||
|
return addr, t, ret, bus
|
||||||
|
|
||||||
|
|
||||||
|
class Buttons:
|
||||||
|
CANCEL = 0
|
||||||
|
RESUME = 1
|
||||||
|
TJA_TOGGLE = 2
|
||||||
|
|
||||||
|
|
||||||
|
# Ford safety has four different configurations tested here:
|
||||||
|
# * CAN with stock longitudinal
|
||||||
|
# * CAN with openpilot longitudinal
|
||||||
|
# * CAN FD with stock longitudinal
|
||||||
|
# * CAN FD with openpilot longitudinal
|
||||||
|
|
||||||
|
class TestFordSafetyBase(common.PandaCarSafetyTest):
|
||||||
|
STANDSTILL_THRESHOLD = 1
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl,
|
||||||
|
MSG_LateralMotionControl2, MSG_IPMA_Data)}
|
||||||
|
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl,
|
||||||
|
MSG_LateralMotionControl2, MSG_IPMA_Data]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
# Max allowed delta between car speeds
|
||||||
|
MAX_SPEED_DELTA = 2.0 # m/s
|
||||||
|
|
||||||
|
STEER_MESSAGE = 0
|
||||||
|
|
||||||
|
# Curvature control limits
|
||||||
|
DEG_TO_CAN = 50000 # 1 / (2e-5) rad to can
|
||||||
|
MAX_CURVATURE = 0.02
|
||||||
|
MAX_CURVATURE_ERROR = 0.002
|
||||||
|
CURVATURE_ERROR_MIN_SPEED = 10.0 # m/s
|
||||||
|
|
||||||
|
ANGLE_RATE_BP = [5., 25., 25.]
|
||||||
|
ANGLE_RATE_UP = [0.0002, 0.0001, 0.0001] # windup limit
|
||||||
|
ANGLE_RATE_DOWN = [0.000225, 0.00015, 0.00015] # unwind limit
|
||||||
|
|
||||||
|
cnt_speed = 0
|
||||||
|
cnt_speed_2 = 0
|
||||||
|
cnt_yaw_rate = 0
|
||||||
|
|
||||||
|
packer: CANPackerPanda
|
||||||
|
safety: libpanda_py.Panda
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls.__name__ == "TestFordSafetyBase":
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
def _set_prev_desired_angle(self, t):
|
||||||
|
t = round(t * self.DEG_TO_CAN)
|
||||||
|
self.safety.set_desired_angle_last(t)
|
||||||
|
|
||||||
|
def _reset_curvature_measurement(self, curvature, speed):
|
||||||
|
for _ in range(6):
|
||||||
|
self._rx(self._speed_msg(speed))
|
||||||
|
self._rx(self._yaw_rate_msg(curvature, speed))
|
||||||
|
|
||||||
|
# Driver brake pedal
|
||||||
|
def _user_brake_msg(self, brake: bool):
|
||||||
|
# brake pedal and cruise state share same message, so we have to send
|
||||||
|
# the other signal too
|
||||||
|
enable = self.safety.get_controls_allowed()
|
||||||
|
values = {
|
||||||
|
"BpedDrvAppl_D_Actl": 2 if brake else 1,
|
||||||
|
"CcStat_D_Actl": 5 if enable else 0,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("EngBrakeData", 0, values)
|
||||||
|
|
||||||
|
# ABS vehicle speed
|
||||||
|
def _speed_msg(self, speed: float, quality_flag=True):
|
||||||
|
values = {"Veh_V_ActlBrk": speed * 3.6, "VehVActlBrk_D_Qf": 3 if quality_flag else 0, "VehVActlBrk_No_Cnt": self.cnt_speed % 16}
|
||||||
|
self.__class__.cnt_speed += 1
|
||||||
|
return self.packer.make_can_msg_panda("BrakeSysFeatures", 0, values, fix_checksum=checksum)
|
||||||
|
|
||||||
|
# PCM vehicle speed
|
||||||
|
def _speed_msg_2(self, speed: float, quality_flag=True):
|
||||||
|
values = {"Veh_V_ActlEng": speed * 3.6, "VehVActlEng_D_Qf": 3 if quality_flag else 0, "VehVActlEng_No_Cnt": self.cnt_speed_2 % 16}
|
||||||
|
self.__class__.cnt_speed_2 += 1
|
||||||
|
return self.packer.make_can_msg_panda("EngVehicleSpThrottle2", 0, values, fix_checksum=checksum)
|
||||||
|
|
||||||
|
# Standstill state
|
||||||
|
def _vehicle_moving_msg(self, speed: float):
|
||||||
|
values = {"VehStop_D_Stat": 1 if speed <= self.STANDSTILL_THRESHOLD else random.choice((0, 2, 3))}
|
||||||
|
return self.packer.make_can_msg_panda("DesiredTorqBrk", 0, values)
|
||||||
|
|
||||||
|
# Current curvature
|
||||||
|
def _yaw_rate_msg(self, curvature: float, speed: float, quality_flag=True):
|
||||||
|
values = {"VehYaw_W_Actl": curvature * speed, "VehYawWActl_D_Qf": 3 if quality_flag else 0,
|
||||||
|
"VehRollYaw_No_Cnt": self.cnt_yaw_rate % 256}
|
||||||
|
self.__class__.cnt_yaw_rate += 1
|
||||||
|
return self.packer.make_can_msg_panda("Yaw_Data_FD1", 0, values, fix_checksum=checksum)
|
||||||
|
|
||||||
|
# Drive throttle input
|
||||||
|
def _user_gas_msg(self, gas: float):
|
||||||
|
values = {"ApedPos_Pc_ActlArb": gas}
|
||||||
|
return self.packer.make_can_msg_panda("EngVehicleSpThrottle", 0, values)
|
||||||
|
|
||||||
|
# Cruise status
|
||||||
|
def _pcm_status_msg(self, enable: bool):
|
||||||
|
# brake pedal and cruise state share same message, so we have to send
|
||||||
|
# the other signal too
|
||||||
|
brake = self.safety.get_brake_pressed_prev()
|
||||||
|
values = {
|
||||||
|
"BpedDrvAppl_D_Actl": 2 if brake else 1,
|
||||||
|
"CcStat_D_Actl": 5 if enable else 0,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("EngBrakeData", 0, values)
|
||||||
|
|
||||||
|
# LKAS command
|
||||||
|
def _lkas_command_msg(self, action: int):
|
||||||
|
values = {
|
||||||
|
"LkaActvStats_D2_Req": action,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("Lane_Assist_Data1", 0, values)
|
||||||
|
|
||||||
|
# LCA command
|
||||||
|
def _lat_ctl_msg(self, enabled: bool, path_offset: float, path_angle: float, curvature: float, curvature_rate: float):
|
||||||
|
if self.STEER_MESSAGE == MSG_LateralMotionControl:
|
||||||
|
values = {
|
||||||
|
"LatCtl_D_Rq": 1 if enabled else 0,
|
||||||
|
"LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter
|
||||||
|
"LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians
|
||||||
|
"LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2
|
||||||
|
"LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("LateralMotionControl", 0, values)
|
||||||
|
elif self.STEER_MESSAGE == MSG_LateralMotionControl2:
|
||||||
|
values = {
|
||||||
|
"LatCtl_D2_Rq": 1 if enabled else 0,
|
||||||
|
"LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter
|
||||||
|
"LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians
|
||||||
|
"LatCtlCrv_NoRate2_Actl": curvature_rate, # Curvature rate [-0.001024|0.001023] 1/meter^2
|
||||||
|
"LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("LateralMotionControl2", 0, values)
|
||||||
|
|
||||||
|
# Cruise control buttons
|
||||||
|
def _acc_button_msg(self, button: int, bus: int):
|
||||||
|
values = {
|
||||||
|
"CcAslButtnCnclPress": 1 if button == Buttons.CANCEL else 0,
|
||||||
|
"CcAsllButtnResPress": 1 if button == Buttons.RESUME else 0,
|
||||||
|
"TjaButtnOnOffPress": 1 if button == Buttons.TJA_TOGGLE else 0,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("Steering_Data_FD1", bus, values)
|
||||||
|
|
||||||
|
def test_rx_hook(self):
|
||||||
|
# checksum, counter, and quality flag checks
|
||||||
|
for quality_flag in [True, False]:
|
||||||
|
for msg in ["speed", "speed_2", "yaw"]:
|
||||||
|
self.safety.set_controls_allowed(True)
|
||||||
|
# send multiple times to verify counter checks
|
||||||
|
for _ in range(10):
|
||||||
|
if msg == "speed":
|
||||||
|
to_push = self._speed_msg(0, quality_flag=quality_flag)
|
||||||
|
elif msg == "speed_2":
|
||||||
|
to_push = self._speed_msg_2(0, quality_flag=quality_flag)
|
||||||
|
elif msg == "yaw":
|
||||||
|
to_push = self._yaw_rate_msg(0, 0, quality_flag=quality_flag)
|
||||||
|
|
||||||
|
self.assertEqual(quality_flag, self._rx(to_push))
|
||||||
|
self.assertEqual(quality_flag, self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
# Mess with checksum to make it fail, checksum is not checked for 2nd speed
|
||||||
|
to_push[0].data[3] = 0 # Speed checksum & half of yaw signal
|
||||||
|
should_rx = msg == "speed_2" and quality_flag
|
||||||
|
self.assertEqual(should_rx, self._rx(to_push))
|
||||||
|
self.assertEqual(should_rx, self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_rx_hook_speed_mismatch(self):
|
||||||
|
# Ford relies on speed for driver curvature limiting, so it checks two sources
|
||||||
|
for speed in np.arange(0, 40, 0.5):
|
||||||
|
for speed_delta in np.arange(-5, 5, 0.1):
|
||||||
|
speed_2 = round(max(speed + speed_delta, 0), 1)
|
||||||
|
# Set controls allowed in between rx since first message can reset it
|
||||||
|
self._rx(self._speed_msg(speed))
|
||||||
|
self.safety.set_controls_allowed(True)
|
||||||
|
self._rx(self._speed_msg_2(speed_2))
|
||||||
|
|
||||||
|
within_delta = abs(speed - speed_2) <= self.MAX_SPEED_DELTA
|
||||||
|
self.assertEqual(self.safety.get_controls_allowed(), within_delta)
|
||||||
|
|
||||||
|
def test_angle_measurements(self):
|
||||||
|
"""Tests rx hook correctly parses the curvature measurement from the vehicle speed and yaw rate"""
|
||||||
|
for speed in np.arange(0.5, 40, 0.5):
|
||||||
|
for curvature in np.arange(0, self.MAX_CURVATURE * 2, 2e-3):
|
||||||
|
self._rx(self._speed_msg(speed))
|
||||||
|
for c in (curvature, -curvature, 0, 0, 0, 0):
|
||||||
|
self._rx(self._yaw_rate_msg(c, speed))
|
||||||
|
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_min(), round(-curvature * self.DEG_TO_CAN))
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_max(), round(curvature * self.DEG_TO_CAN))
|
||||||
|
|
||||||
|
self._rx(self._yaw_rate_msg(0, speed))
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_min(), round(-curvature * self.DEG_TO_CAN))
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_max(), 0)
|
||||||
|
|
||||||
|
self._rx(self._yaw_rate_msg(0, speed))
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_min(), 0)
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_max(), 0)
|
||||||
|
|
||||||
|
def test_steer_allowed(self):
|
||||||
|
path_offsets = np.arange(-5.12, 5.11, 1).round()
|
||||||
|
path_angles = np.arange(-0.5, 0.5235, 0.1).round(1)
|
||||||
|
curvature_rates = np.arange(-0.001024, 0.00102375, 0.001).round(3)
|
||||||
|
curvatures = np.arange(-0.02, 0.02094, 0.01).round(2)
|
||||||
|
|
||||||
|
for speed in (self.CURVATURE_ERROR_MIN_SPEED - 1,
|
||||||
|
self.CURVATURE_ERROR_MIN_SPEED + 1):
|
||||||
|
for controls_allowed in (True, False):
|
||||||
|
for steer_control_enabled in (True, False):
|
||||||
|
for path_offset in path_offsets:
|
||||||
|
for path_angle in path_angles:
|
||||||
|
for curvature_rate in curvature_rates:
|
||||||
|
for curvature in curvatures:
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
self._set_prev_desired_angle(curvature)
|
||||||
|
self._reset_curvature_measurement(curvature, speed)
|
||||||
|
|
||||||
|
should_tx = path_offset == 0 and path_angle == 0 and curvature_rate == 0
|
||||||
|
# when request bit is 0, only allow curvature of 0 since the signal range
|
||||||
|
# is not large enough to enforce it tracking measured
|
||||||
|
should_tx = should_tx and (controls_allowed if steer_control_enabled else curvature == 0)
|
||||||
|
with self.subTest(controls_allowed=controls_allowed, steer_control_enabled=steer_control_enabled,
|
||||||
|
path_offset=path_offset, path_angle=path_angle, curvature_rate=curvature_rate,
|
||||||
|
curvature=curvature):
|
||||||
|
self.assertEqual(should_tx, self._tx(self._lat_ctl_msg(steer_control_enabled, path_offset, path_angle, curvature, curvature_rate)))
|
||||||
|
|
||||||
|
def test_curvature_rate_limit_up(self):
|
||||||
|
"""
|
||||||
|
When the curvature error is exceeded, commanded curvature must start moving towards meas respecting rate limits.
|
||||||
|
Since panda allows higher rate limits to avoid false positives, we need to allow a lower rate to move towards meas.
|
||||||
|
"""
|
||||||
|
self.safety.set_controls_allowed(True)
|
||||||
|
small_curvature = 2 / self.DEG_TO_CAN # significant small amount of curvature to cross boundary
|
||||||
|
|
||||||
|
for speed in np.arange(0, 40, 0.5):
|
||||||
|
limit_command = speed > self.CURVATURE_ERROR_MIN_SPEED
|
||||||
|
max_delta_up = np.interp(speed, self.ANGLE_RATE_BP, self.ANGLE_RATE_UP)
|
||||||
|
max_delta_up_lower = np.interp(speed + 1, self.ANGLE_RATE_BP, self.ANGLE_RATE_UP)
|
||||||
|
|
||||||
|
cases = [
|
||||||
|
(not limit_command, 0),
|
||||||
|
(not limit_command, max_delta_up_lower - small_curvature),
|
||||||
|
(True, max_delta_up_lower),
|
||||||
|
(True, max_delta_up),
|
||||||
|
(False, max_delta_up + small_curvature),
|
||||||
|
]
|
||||||
|
|
||||||
|
for sign in (-1, 1):
|
||||||
|
self._reset_curvature_measurement(sign * (self.MAX_CURVATURE_ERROR + 1e-3), speed)
|
||||||
|
for should_tx, curvature in cases:
|
||||||
|
self._set_prev_desired_angle(sign * small_curvature)
|
||||||
|
self.assertEqual(should_tx, self._tx(self._lat_ctl_msg(True, 0, 0, sign * (small_curvature + curvature), 0)))
|
||||||
|
|
||||||
|
def test_curvature_rate_limit_down(self):
|
||||||
|
self.safety.set_controls_allowed(True)
|
||||||
|
small_curvature = 2 / self.DEG_TO_CAN # significant small amount of curvature to cross boundary
|
||||||
|
|
||||||
|
for speed in np.arange(0, 40, 0.5):
|
||||||
|
limit_command = speed > self.CURVATURE_ERROR_MIN_SPEED
|
||||||
|
max_delta_down = np.interp(speed, self.ANGLE_RATE_BP, self.ANGLE_RATE_DOWN)
|
||||||
|
max_delta_down_lower = np.interp(speed + 1, self.ANGLE_RATE_BP, self.ANGLE_RATE_DOWN)
|
||||||
|
|
||||||
|
cases = [
|
||||||
|
(not limit_command, self.MAX_CURVATURE),
|
||||||
|
(not limit_command, self.MAX_CURVATURE - max_delta_down_lower + small_curvature),
|
||||||
|
(True, self.MAX_CURVATURE - max_delta_down_lower),
|
||||||
|
(True, self.MAX_CURVATURE - max_delta_down),
|
||||||
|
(False, self.MAX_CURVATURE - max_delta_down - small_curvature),
|
||||||
|
]
|
||||||
|
|
||||||
|
for sign in (-1, 1):
|
||||||
|
self._reset_curvature_measurement(sign * (self.MAX_CURVATURE - self.MAX_CURVATURE_ERROR - 1e-3), speed)
|
||||||
|
for should_tx, curvature in cases:
|
||||||
|
self._set_prev_desired_angle(sign * self.MAX_CURVATURE)
|
||||||
|
self.assertEqual(should_tx, self._tx(self._lat_ctl_msg(True, 0, 0, sign * curvature, 0)))
|
||||||
|
|
||||||
|
def test_prevent_lkas_action(self):
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self.assertFalse(self._tx(self._lkas_command_msg(1)))
|
||||||
|
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
self.assertFalse(self._tx(self._lkas_command_msg(1)))
|
||||||
|
|
||||||
|
def test_acc_buttons(self):
|
||||||
|
for allowed in (0, 1):
|
||||||
|
self.safety.set_controls_allowed(allowed)
|
||||||
|
for enabled in (True, False):
|
||||||
|
self._rx(self._pcm_status_msg(enabled))
|
||||||
|
self.assertTrue(self._tx(self._acc_button_msg(Buttons.TJA_TOGGLE, 2)))
|
||||||
|
|
||||||
|
for allowed in (0, 1):
|
||||||
|
self.safety.set_controls_allowed(allowed)
|
||||||
|
for bus in (0, 2):
|
||||||
|
self.assertEqual(allowed, self._tx(self._acc_button_msg(Buttons.RESUME, bus)))
|
||||||
|
|
||||||
|
for enabled in (True, False):
|
||||||
|
self._rx(self._pcm_status_msg(enabled))
|
||||||
|
for bus in (0, 2):
|
||||||
|
self.assertEqual(enabled, self._tx(self._acc_button_msg(Buttons.CANCEL, bus)))
|
||||||
|
|
||||||
|
|
||||||
|
class TestFordStockSafety(TestFordSafetyBase):
|
||||||
|
STEER_MESSAGE = MSG_LateralMotionControl
|
||||||
|
|
||||||
|
TX_MSGS = [
|
||||||
|
[MSG_Steering_Data_FD1, 0], [MSG_Steering_Data_FD1, 2], [MSG_ACCDATA_3, 0], [MSG_Lane_Assist_Data1, 0],
|
||||||
|
[MSG_LateralMotionControl, 0], [MSG_IPMA_Data, 0],
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("ford_lincoln_base_pt")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_FORD, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestFordCANFDStockSafety(TestFordSafetyBase):
|
||||||
|
STEER_MESSAGE = MSG_LateralMotionControl2
|
||||||
|
|
||||||
|
TX_MSGS = [
|
||||||
|
[MSG_Steering_Data_FD1, 0], [MSG_Steering_Data_FD1, 2], [MSG_ACCDATA_3, 0], [MSG_Lane_Assist_Data1, 0],
|
||||||
|
[MSG_LateralMotionControl2, 0], [MSG_IPMA_Data, 0],
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("ford_lincoln_base_pt")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_FORD, Panda.FLAG_FORD_CANFD)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestFordLongitudinalSafetyBase(TestFordSafetyBase):
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (MSG_ACCDATA, MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl,
|
||||||
|
MSG_LateralMotionControl2, MSG_IPMA_Data)}
|
||||||
|
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [MSG_ACCDATA, MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl,
|
||||||
|
MSG_LateralMotionControl2, MSG_IPMA_Data]}
|
||||||
|
|
||||||
|
MAX_ACCEL = 2.0 # accel is used for brakes, but openpilot can set positive values
|
||||||
|
MIN_ACCEL = -3.5
|
||||||
|
INACTIVE_ACCEL = 0.0
|
||||||
|
|
||||||
|
MAX_GAS = 2.0
|
||||||
|
MIN_GAS = -0.5
|
||||||
|
INACTIVE_GAS = -5.0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls.__name__ == "TestFordLongitudinalSafetyBase":
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
# ACC command
|
||||||
|
def _acc_command_msg(self, gas: float, brake: float, cmbb_deny: bool = False):
|
||||||
|
values = {
|
||||||
|
"AccPrpl_A_Rq": gas, # [-5|5.23] m/s^2
|
||||||
|
"AccPrpl_A_Pred": gas, # [-5|5.23] m/s^2
|
||||||
|
"AccBrkTot_A_Rq": brake, # [-20|11.9449] m/s^2
|
||||||
|
"CmbbDeny_B_Actl": 1 if cmbb_deny else 0, # [0|1] deny AEB actuation
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("ACCDATA", 0, values)
|
||||||
|
|
||||||
|
def test_stock_aeb(self):
|
||||||
|
# Test that CmbbDeny_B_Actl is never 1, it prevents the ABS module from actuating AEB requests from ACCDATA_2
|
||||||
|
for controls_allowed in (True, False):
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
for cmbb_deny in (True, False):
|
||||||
|
should_tx = not cmbb_deny
|
||||||
|
self.assertEqual(should_tx, self._tx(self._acc_command_msg(self.INACTIVE_GAS, self.INACTIVE_ACCEL, cmbb_deny)))
|
||||||
|
should_tx = controls_allowed and not cmbb_deny
|
||||||
|
self.assertEqual(should_tx, self._tx(self._acc_command_msg(self.MAX_GAS, self.MAX_ACCEL, cmbb_deny)))
|
||||||
|
|
||||||
|
def test_gas_safety_check(self):
|
||||||
|
for controls_allowed in (True, False):
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
for gas in np.concatenate((np.arange(self.MIN_GAS - 2, self.MAX_GAS + 2, 0.05), [self.INACTIVE_GAS])):
|
||||||
|
gas = round(gas, 2) # floats might not hit exact boundary conditions without rounding
|
||||||
|
should_tx = (controls_allowed and self.MIN_GAS <= gas <= self.MAX_GAS) or gas == self.INACTIVE_GAS
|
||||||
|
self.assertEqual(should_tx, self._tx(self._acc_command_msg(gas, self.INACTIVE_ACCEL)))
|
||||||
|
|
||||||
|
def test_brake_safety_check(self):
|
||||||
|
for controls_allowed in (True, False):
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
for brake in np.arange(self.MIN_ACCEL - 2, self.MAX_ACCEL + 2, 0.05):
|
||||||
|
brake = round(brake, 2) # floats might not hit exact boundary conditions without rounding
|
||||||
|
should_tx = (controls_allowed and self.MIN_ACCEL <= brake <= self.MAX_ACCEL) or brake == self.INACTIVE_ACCEL
|
||||||
|
self.assertEqual(should_tx, self._tx(self._acc_command_msg(self.INACTIVE_GAS, brake)))
|
||||||
|
|
||||||
|
|
||||||
|
class TestFordLongitudinalSafety(TestFordLongitudinalSafetyBase):
|
||||||
|
STEER_MESSAGE = MSG_LateralMotionControl
|
||||||
|
|
||||||
|
TX_MSGS = [
|
||||||
|
[MSG_Steering_Data_FD1, 0], [MSG_Steering_Data_FD1, 2], [MSG_ACCDATA, 0], [MSG_ACCDATA_3, 0], [MSG_Lane_Assist_Data1, 0],
|
||||||
|
[MSG_LateralMotionControl, 0], [MSG_IPMA_Data, 0],
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("ford_lincoln_base_pt")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_FORD, Panda.FLAG_FORD_LONG_CONTROL)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestFordCANFDLongitudinalSafety(TestFordLongitudinalSafetyBase):
|
||||||
|
STEER_MESSAGE = MSG_LateralMotionControl2
|
||||||
|
|
||||||
|
TX_MSGS = [
|
||||||
|
[MSG_Steering_Data_FD1, 0], [MSG_Steering_Data_FD1, 2], [MSG_ACCDATA, 0], [MSG_ACCDATA_3, 0], [MSG_Lane_Assist_Data1, 0],
|
||||||
|
[MSG_LateralMotionControl2, 0], [MSG_IPMA_Data, 0],
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("ford_lincoln_base_pt")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_FORD, Panda.FLAG_FORD_LONG_CONTROL | Panda.FLAG_FORD_CANFD)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
228
panda/tests/safety/test_gm.py
Normal file
228
panda/tests/safety/test_gm.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
|
||||||
|
|
||||||
|
class Buttons:
|
||||||
|
UNPRESS = 1
|
||||||
|
RES_ACCEL = 2
|
||||||
|
DECEL_SET = 3
|
||||||
|
CANCEL = 6
|
||||||
|
|
||||||
|
|
||||||
|
class GmLongitudinalBase(common.PandaCarSafetyTest, common.LongitudinalGasBrakeSafetyTest):
|
||||||
|
# pylint: disable=no-member,abstract-method
|
||||||
|
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x180, 0x2CB)} # ASCMLKASteeringCmd, ASCMGasRegenCmd
|
||||||
|
|
||||||
|
MAX_POSSIBLE_BRAKE = 2 ** 12
|
||||||
|
MAX_BRAKE = 400
|
||||||
|
|
||||||
|
MAX_POSSIBLE_GAS = 2 ** 12
|
||||||
|
|
||||||
|
PCM_CRUISE = False # openpilot can control the PCM state if longitudinal
|
||||||
|
|
||||||
|
def _send_brake_msg(self, brake):
|
||||||
|
values = {"FrictionBrakeCmd": -brake}
|
||||||
|
return self.packer_chassis.make_can_msg_panda("EBCMFrictionBrakeCmd", self.BRAKE_BUS, values)
|
||||||
|
|
||||||
|
def _send_gas_msg(self, gas):
|
||||||
|
values = {"GasRegenCmd": gas}
|
||||||
|
return self.packer.make_can_msg_panda("ASCMGasRegenCmd", 0, values)
|
||||||
|
|
||||||
|
# override these tests from PandaCarSafetyTest, GM longitudinal uses button enable
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def test_disable_control_allowed_from_cruise(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_enable_control_allowed_from_cruise(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_cruise_engaged_prev(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_set_resume_buttons(self):
|
||||||
|
"""
|
||||||
|
SET and RESUME enter controls allowed on their falling and rising edges, respectively.
|
||||||
|
"""
|
||||||
|
for btn_prev in range(8):
|
||||||
|
for btn_cur in range(8):
|
||||||
|
with self.subTest(btn_prev=btn_prev, btn_cur=btn_cur):
|
||||||
|
self._rx(self._button_msg(btn_prev))
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
for _ in range(10):
|
||||||
|
self._rx(self._button_msg(btn_cur))
|
||||||
|
|
||||||
|
should_enable = btn_cur != Buttons.DECEL_SET and btn_prev == Buttons.DECEL_SET
|
||||||
|
should_enable = should_enable or (btn_cur == Buttons.RES_ACCEL and btn_prev != Buttons.RES_ACCEL)
|
||||||
|
should_enable = should_enable and btn_cur != Buttons.CANCEL
|
||||||
|
self.assertEqual(should_enable, self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_cancel_button(self):
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._button_msg(Buttons.CANCEL))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
|
||||||
|
class TestGmSafetyBase(common.PandaCarSafetyTest, common.DriverTorqueSteeringSafetyTest):
|
||||||
|
STANDSTILL_THRESHOLD = 10 * 0.0311
|
||||||
|
# Ensures ASCM is off on ASCM cars, and relay is not malfunctioning for camera-ACC cars
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x180,)} # ASCMLKASteeringCmd
|
||||||
|
BUTTONS_BUS = 0 # rx or tx
|
||||||
|
BRAKE_BUS = 0 # tx only
|
||||||
|
|
||||||
|
MAX_RATE_UP = 10
|
||||||
|
MAX_RATE_DOWN = 15
|
||||||
|
MAX_TORQUE = 300
|
||||||
|
MAX_RT_DELTA = 128
|
||||||
|
RT_INTERVAL = 250000
|
||||||
|
DRIVER_TORQUE_ALLOWANCE = 65
|
||||||
|
DRIVER_TORQUE_FACTOR = 4
|
||||||
|
|
||||||
|
PCM_CRUISE = True # openpilot is tied to the PCM state if not longitudinal
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls.__name__ == "TestGmSafetyBase":
|
||||||
|
cls.packer = None
|
||||||
|
cls.safety = None
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("gm_global_a_powertrain_generated")
|
||||||
|
self.packer_chassis = CANPackerPanda("gm_global_a_chassis")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_GM, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
if self.PCM_CRUISE:
|
||||||
|
values = {"CruiseState": enable}
|
||||||
|
return self.packer.make_can_msg_panda("AcceleratorPedal2", 0, values)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {"%sWheelSpd" % s: speed for s in ["RL", "RR"]}
|
||||||
|
return self.packer.make_can_msg_panda("EBCMWheelSpdRear", 0, values)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
# GM safety has a brake threshold of 8
|
||||||
|
values = {"BrakePedalPos": 8 if brake else 0}
|
||||||
|
return self.packer.make_can_msg_panda("ECMAcceleratorPos", 0, values)
|
||||||
|
|
||||||
|
def _user_regen_msg(self, regen):
|
||||||
|
values = {"RegenPaddle": 2 if regen else 0}
|
||||||
|
return self.packer.make_can_msg_panda("EBCMRegenPaddle", 0, values)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"AcceleratorPedal2": 1 if gas else 0}
|
||||||
|
if self.PCM_CRUISE:
|
||||||
|
# Fill CruiseState with expected value if the safety mode reads cruise state from gas msg
|
||||||
|
values["CruiseState"] = self.safety.get_controls_allowed()
|
||||||
|
return self.packer.make_can_msg_panda("AcceleratorPedal2", 0, values)
|
||||||
|
|
||||||
|
def _torque_driver_msg(self, torque):
|
||||||
|
# Safety tests assume driver torque is an int, use DBC factor
|
||||||
|
values = {"LKADriverAppldTrq": torque * 0.01}
|
||||||
|
return self.packer.make_can_msg_panda("PSCMStatus", 0, values)
|
||||||
|
|
||||||
|
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||||
|
values = {"LKASteeringCmd": torque, "LKASteeringCmdActive": steer_req}
|
||||||
|
return self.packer.make_can_msg_panda("ASCMLKASteeringCmd", 0, values)
|
||||||
|
|
||||||
|
def _button_msg(self, buttons):
|
||||||
|
values = {"ACCButtons": buttons}
|
||||||
|
return self.packer.make_can_msg_panda("ASCMSteeringButton", self.BUTTONS_BUS, values)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGmAscmSafety(GmLongitudinalBase, TestGmSafetyBase):
|
||||||
|
TX_MSGS = [[0x180, 0], [0x409, 0], [0x40A, 0], [0x2CB, 0], [0x370, 0], # pt bus
|
||||||
|
[0xA1, 1], [0x306, 1], [0x308, 1], [0x310, 1], # obs bus
|
||||||
|
[0x315, 2], # ch bus
|
||||||
|
[0x104c006c, 3], [0x10400060, 3]] # gmlan
|
||||||
|
FWD_BLACKLISTED_ADDRS: dict[int, list[int]] = {}
|
||||||
|
FWD_BUS_LOOKUP: dict[int, int] = {}
|
||||||
|
BRAKE_BUS = 2
|
||||||
|
|
||||||
|
MAX_GAS = 3072
|
||||||
|
MIN_GAS = 1404 # maximum regen
|
||||||
|
INACTIVE_GAS = 1404
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("gm_global_a_powertrain_generated")
|
||||||
|
self.packer_chassis = CANPackerPanda("gm_global_a_chassis")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_GM, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestGmCameraSafetyBase(TestGmSafetyBase):
|
||||||
|
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls.__name__ == "TestGmCameraSafetyBase":
|
||||||
|
cls.packer = None
|
||||||
|
cls.safety = None
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"BrakePressed": brake}
|
||||||
|
return self.packer.make_can_msg_panda("ECMEngineStatus", 0, values)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGmCameraSafety(TestGmCameraSafetyBase):
|
||||||
|
TX_MSGS = [[0x180, 0], # pt bus
|
||||||
|
[0x184, 2]] # camera bus
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x180], 0: [0x184]} # block LKAS message and PSCMStatus
|
||||||
|
BUTTONS_BUS = 2 # tx only
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("gm_global_a_powertrain_generated")
|
||||||
|
self.packer_chassis = CANPackerPanda("gm_global_a_chassis")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_GM, Panda.FLAG_GM_HW_CAM)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def test_buttons(self):
|
||||||
|
# Only CANCEL button is allowed while cruise is enabled
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
for btn in range(8):
|
||||||
|
self.assertFalse(self._tx(self._button_msg(btn)))
|
||||||
|
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
for btn in range(8):
|
||||||
|
self.assertFalse(self._tx(self._button_msg(btn)))
|
||||||
|
|
||||||
|
for enabled in (True, False):
|
||||||
|
self._rx(self._pcm_status_msg(enabled))
|
||||||
|
self.assertEqual(enabled, self._tx(self._button_msg(Buttons.CANCEL)))
|
||||||
|
|
||||||
|
|
||||||
|
class TestGmCameraLongitudinalSafety(GmLongitudinalBase, TestGmCameraSafetyBase):
|
||||||
|
TX_MSGS = [[0x180, 0], [0x315, 0], [0x2CB, 0], [0x370, 0], # pt bus
|
||||||
|
[0x184, 2]] # camera bus
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x180, 0x2CB, 0x370, 0x315], 0: [0x184]} # block LKAS, ACC messages and PSCMStatus
|
||||||
|
BUTTONS_BUS = 0 # rx only
|
||||||
|
|
||||||
|
MAX_GAS = 3400
|
||||||
|
MIN_GAS = 1514 # maximum regen
|
||||||
|
INACTIVE_GAS = 1554
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("gm_global_a_powertrain_generated")
|
||||||
|
self.packer_chassis = CANPackerPanda("gm_global_a_chassis")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_GM, Panda.FLAG_GM_HW_CAM | Panda.FLAG_GM_HW_CAM_LONG)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
618
panda/tests/safety/test_honda.py
Normal file
618
panda/tests/safety/test_honda.py
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda, MAX_WRONG_COUNTERS
|
||||||
|
|
||||||
|
HONDA_N_COMMON_TX_MSGS = [[0xE4, 0], [0x194, 0], [0x1FA, 0], [0x30C, 0], [0x33D, 0]]
|
||||||
|
|
||||||
|
class Btn:
|
||||||
|
NONE = 0
|
||||||
|
MAIN = 1
|
||||||
|
CANCEL = 2
|
||||||
|
SET = 3
|
||||||
|
RESUME = 4
|
||||||
|
|
||||||
|
HONDA_NIDEC = 0
|
||||||
|
HONDA_BOSCH = 1
|
||||||
|
|
||||||
|
|
||||||
|
# Honda safety has several different configurations tested here:
|
||||||
|
# * Nidec
|
||||||
|
# * normal (PCM-enable)
|
||||||
|
# * alt SCM messages (PCM-enable)
|
||||||
|
# * gas interceptor (button-enable)
|
||||||
|
# * gas interceptor with alt SCM messages (button-enable)
|
||||||
|
# * Bosch
|
||||||
|
# * Bosch with Longitudinal Support
|
||||||
|
# * Bosch Radarless
|
||||||
|
# * Bosch Radarless with Longitudinal Support
|
||||||
|
|
||||||
|
|
||||||
|
class HondaButtonEnableBase(common.PandaCarSafetyTest):
|
||||||
|
# pylint: disable=no-member,abstract-method
|
||||||
|
|
||||||
|
# override these inherited tests since we're using button enable
|
||||||
|
def test_disable_control_allowed_from_cruise(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_enable_control_allowed_from_cruise(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_cruise_engaged_prev(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_buttons_with_main_off(self):
|
||||||
|
for btn in (Btn.SET, Btn.RESUME, Btn.CANCEL):
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._acc_state_msg(False))
|
||||||
|
self._rx(self._button_msg(btn, main_on=False))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_set_resume_buttons(self):
|
||||||
|
"""
|
||||||
|
Both SET and RES should enter controls allowed on their falling edge.
|
||||||
|
"""
|
||||||
|
for main_on in (True, False):
|
||||||
|
self._rx(self._acc_state_msg(main_on))
|
||||||
|
for btn_prev in range(8):
|
||||||
|
for btn_cur in range(8):
|
||||||
|
self._rx(self._button_msg(Btn.NONE))
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
for _ in range(10):
|
||||||
|
self._rx(self._button_msg(btn_prev))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
# should enter controls allowed on falling edge and not transitioning to cancel or main
|
||||||
|
should_enable = (main_on and
|
||||||
|
btn_cur != btn_prev and
|
||||||
|
btn_prev in (Btn.RESUME, Btn.SET) and
|
||||||
|
btn_cur not in (Btn.CANCEL, Btn.MAIN))
|
||||||
|
|
||||||
|
self._rx(self._button_msg(btn_cur, main_on=main_on))
|
||||||
|
self.assertEqual(should_enable, self.safety.get_controls_allowed(), msg=f"{main_on=} {btn_prev=} {btn_cur=}")
|
||||||
|
|
||||||
|
def test_main_cancel_buttons(self):
|
||||||
|
"""
|
||||||
|
Both MAIN and CANCEL should exit controls immediately.
|
||||||
|
"""
|
||||||
|
for btn in (Btn.MAIN, Btn.CANCEL):
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._button_msg(btn, main_on=True))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_disengage_on_main(self):
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._acc_state_msg(True))
|
||||||
|
self.assertTrue(self.safety.get_controls_allowed())
|
||||||
|
self._rx(self._acc_state_msg(False))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_rx_hook(self):
|
||||||
|
|
||||||
|
# TODO: move this test to common
|
||||||
|
# checksum checks
|
||||||
|
for msg in ["btn", "gas", "speed"]:
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
if msg == "btn":
|
||||||
|
to_push = self._button_msg(Btn.SET)
|
||||||
|
if msg == "gas":
|
||||||
|
to_push = self._user_gas_msg(0)
|
||||||
|
if msg == "speed":
|
||||||
|
to_push = self._speed_msg(0)
|
||||||
|
self.assertTrue(self._rx(to_push))
|
||||||
|
if msg != "btn":
|
||||||
|
to_push[0].data[4] = 0 # invalidate checksum
|
||||||
|
to_push[0].data[5] = 0
|
||||||
|
to_push[0].data[6] = 0
|
||||||
|
to_push[0].data[7] = 0
|
||||||
|
self.assertFalse(self._rx(to_push))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
# counter
|
||||||
|
# reset wrong_counters to zero by sending valid messages
|
||||||
|
for i in range(MAX_WRONG_COUNTERS + 1):
|
||||||
|
self.__class__.cnt_speed += 1
|
||||||
|
self.__class__.cnt_button += 1
|
||||||
|
self.__class__.cnt_powertrain_data += 1
|
||||||
|
if i < MAX_WRONG_COUNTERS:
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._button_msg(Btn.SET))
|
||||||
|
self._rx(self._speed_msg(0))
|
||||||
|
self._rx(self._user_gas_msg(0))
|
||||||
|
else:
|
||||||
|
self.assertFalse(self._rx(self._button_msg(Btn.SET)))
|
||||||
|
self.assertFalse(self._rx(self._speed_msg(0)))
|
||||||
|
self.assertFalse(self._rx(self._user_gas_msg(0)))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
# restore counters for future tests with a couple of good messages
|
||||||
|
for _ in range(2):
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._button_msg(Btn.SET, main_on=True))
|
||||||
|
self._rx(self._speed_msg(0))
|
||||||
|
self._rx(self._user_gas_msg(0))
|
||||||
|
self._rx(self._button_msg(Btn.SET, main_on=True))
|
||||||
|
self.assertTrue(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
|
||||||
|
class HondaPcmEnableBase(common.PandaCarSafetyTest):
|
||||||
|
# pylint: disable=no-member,abstract-method
|
||||||
|
|
||||||
|
def test_buttons(self):
|
||||||
|
"""
|
||||||
|
Buttons should only cancel in this configuration,
|
||||||
|
since our state is tied to the PCM's cruise state.
|
||||||
|
"""
|
||||||
|
for controls_allowed in (True, False):
|
||||||
|
for main_on in (True, False):
|
||||||
|
# not a valid state
|
||||||
|
if controls_allowed and not main_on:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for btn in (Btn.SET, Btn.RESUME, Btn.CANCEL):
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
self._rx(self._acc_state_msg(main_on))
|
||||||
|
|
||||||
|
# btn + none for falling edge
|
||||||
|
self._rx(self._button_msg(btn, main_on=main_on))
|
||||||
|
self._rx(self._button_msg(Btn.NONE, main_on=main_on))
|
||||||
|
|
||||||
|
if btn == Btn.CANCEL:
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
else:
|
||||||
|
self.assertEqual(controls_allowed, self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
|
||||||
|
class HondaBase(common.PandaCarSafetyTest):
|
||||||
|
MAX_BRAKE = 255
|
||||||
|
PT_BUS: int | None = None # must be set when inherited
|
||||||
|
STEER_BUS: int | None = None # must be set when inherited
|
||||||
|
BUTTONS_BUS: int | None = None # must be set when inherited, tx on this bus, rx on PT_BUS
|
||||||
|
|
||||||
|
STANDSTILL_THRESHOLD = 0
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x194)} # STEERING_CONTROL
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
cnt_speed = 0
|
||||||
|
cnt_button = 0
|
||||||
|
cnt_brake = 0
|
||||||
|
cnt_powertrain_data = 0
|
||||||
|
cnt_acc_state = 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls.__name__.endswith("Base"):
|
||||||
|
cls.packer = None
|
||||||
|
cls.safety = None
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
def _powertrain_data_msg(self, cruise_on=None, brake_pressed=None, gas_pressed=None):
|
||||||
|
# preserve the state
|
||||||
|
if cruise_on is None:
|
||||||
|
# or'd with controls allowed since the tests use it to "enable" cruise
|
||||||
|
cruise_on = self.safety.get_cruise_engaged_prev() or self.safety.get_controls_allowed()
|
||||||
|
if brake_pressed is None:
|
||||||
|
brake_pressed = self.safety.get_brake_pressed_prev()
|
||||||
|
if gas_pressed is None:
|
||||||
|
gas_pressed = self.safety.get_gas_pressed_prev()
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"ACC_STATUS": cruise_on,
|
||||||
|
"BRAKE_PRESSED": brake_pressed,
|
||||||
|
"PEDAL_GAS": gas_pressed,
|
||||||
|
"COUNTER": self.cnt_powertrain_data % 4
|
||||||
|
}
|
||||||
|
self.__class__.cnt_powertrain_data += 1
|
||||||
|
return self.packer.make_can_msg_panda("POWERTRAIN_DATA", self.PT_BUS, values)
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
return self._powertrain_data_msg(cruise_on=enable)
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {"XMISSION_SPEED": speed, "COUNTER": self.cnt_speed % 4}
|
||||||
|
self.__class__.cnt_speed += 1
|
||||||
|
return self.packer.make_can_msg_panda("ENGINE_DATA", self.PT_BUS, values)
|
||||||
|
|
||||||
|
def _acc_state_msg(self, main_on):
|
||||||
|
values = {"MAIN_ON": main_on, "COUNTER": self.cnt_acc_state % 4}
|
||||||
|
self.__class__.cnt_acc_state += 1
|
||||||
|
return self.packer.make_can_msg_panda("SCM_FEEDBACK", self.PT_BUS, values)
|
||||||
|
|
||||||
|
def _button_msg(self, buttons, main_on=False, bus=None):
|
||||||
|
bus = self.PT_BUS if bus is None else bus
|
||||||
|
values = {"CRUISE_BUTTONS": buttons, "COUNTER": self.cnt_button % 4}
|
||||||
|
self.__class__.cnt_button += 1
|
||||||
|
return self.packer.make_can_msg_panda("SCM_BUTTONS", bus, values)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
return self._powertrain_data_msg(brake_pressed=brake)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
return self._powertrain_data_msg(gas_pressed=gas)
|
||||||
|
|
||||||
|
def _send_steer_msg(self, steer):
|
||||||
|
values = {"STEER_TORQUE": steer}
|
||||||
|
return self.packer.make_can_msg_panda("STEERING_CONTROL", self.STEER_BUS, values)
|
||||||
|
|
||||||
|
def _send_brake_msg(self, brake):
|
||||||
|
# must be implemented when inherited
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def test_disengage_on_brake(self):
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._user_brake_msg(1))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_steer_safety_check(self):
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
self.assertTrue(self._tx(self._send_steer_msg(0x0000)))
|
||||||
|
self.assertFalse(self._tx(self._send_steer_msg(0x1000)))
|
||||||
|
|
||||||
|
|
||||||
|
# ********************* Honda Nidec **********************
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaNidecSafetyBase(HondaBase):
|
||||||
|
TX_MSGS = HONDA_N_COMMON_TX_MSGS
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0x194, 0x33D, 0x30C]}
|
||||||
|
|
||||||
|
PT_BUS = 0
|
||||||
|
STEER_BUS = 0
|
||||||
|
BUTTONS_BUS = 0
|
||||||
|
|
||||||
|
MAX_GAS = 198
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("honda_civic_touring_2016_can_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HONDA_NIDEC, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _send_brake_msg(self, brake, aeb_req=0, bus=0):
|
||||||
|
values = {"COMPUTER_BRAKE": brake, "AEB_REQ_1": aeb_req}
|
||||||
|
return self.packer.make_can_msg_panda("BRAKE_COMMAND", bus, values)
|
||||||
|
|
||||||
|
def _rx_brake_msg(self, brake, aeb_req=0):
|
||||||
|
return self._send_brake_msg(brake, aeb_req, bus=2)
|
||||||
|
|
||||||
|
def _send_acc_hud_msg(self, pcm_gas, pcm_speed):
|
||||||
|
# Used to control ACC on Nidec without pedal
|
||||||
|
values = {"PCM_GAS": pcm_gas, "PCM_SPEED": pcm_speed}
|
||||||
|
return self.packer.make_can_msg_panda("ACC_HUD", 0, values)
|
||||||
|
|
||||||
|
def test_acc_hud_safety_check(self):
|
||||||
|
for controls_allowed in [True, False]:
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
for pcm_gas in range(255):
|
||||||
|
for pcm_speed in range(100):
|
||||||
|
send = (controls_allowed and pcm_gas <= self.MAX_GAS) or (pcm_gas == 0 and pcm_speed == 0)
|
||||||
|
self.assertEqual(send, self._tx(self._send_acc_hud_msg(pcm_gas, pcm_speed)))
|
||||||
|
|
||||||
|
def test_fwd_hook(self):
|
||||||
|
# normal operation, not forwarding AEB
|
||||||
|
self.FWD_BLACKLISTED_ADDRS[2].append(0x1FA)
|
||||||
|
self.safety.set_honda_fwd_brake(False)
|
||||||
|
super().test_fwd_hook()
|
||||||
|
|
||||||
|
# forwarding AEB brake signal
|
||||||
|
self.FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0x194, 0x33D, 0x30C]}
|
||||||
|
self.safety.set_honda_fwd_brake(True)
|
||||||
|
super().test_fwd_hook()
|
||||||
|
|
||||||
|
def test_honda_fwd_brake_latching(self):
|
||||||
|
# Shouldn't fwd stock Honda requesting brake without AEB
|
||||||
|
self.assertTrue(self._rx(self._rx_brake_msg(self.MAX_BRAKE, aeb_req=0)))
|
||||||
|
self.assertFalse(self.safety.get_honda_fwd_brake())
|
||||||
|
|
||||||
|
# Now allow controls and request some brake
|
||||||
|
openpilot_brake = round(self.MAX_BRAKE / 2.0)
|
||||||
|
self.safety.set_controls_allowed(True)
|
||||||
|
self.assertTrue(self._tx(self._send_brake_msg(openpilot_brake)))
|
||||||
|
|
||||||
|
# Still shouldn't fwd stock Honda brake until it's more than openpilot's
|
||||||
|
for stock_honda_brake in range(self.MAX_BRAKE + 1):
|
||||||
|
self.assertTrue(self._rx(self._rx_brake_msg(stock_honda_brake, aeb_req=1)))
|
||||||
|
should_fwd_brake = stock_honda_brake >= openpilot_brake
|
||||||
|
self.assertEqual(should_fwd_brake, self.safety.get_honda_fwd_brake())
|
||||||
|
|
||||||
|
# Shouldn't stop fwding until AEB event is over
|
||||||
|
for stock_honda_brake in range(self.MAX_BRAKE + 1)[::-1]:
|
||||||
|
self.assertTrue(self._rx(self._rx_brake_msg(stock_honda_brake, aeb_req=1)))
|
||||||
|
self.assertTrue(self.safety.get_honda_fwd_brake())
|
||||||
|
|
||||||
|
self.assertTrue(self._rx(self._rx_brake_msg(0, aeb_req=0)))
|
||||||
|
self.assertFalse(self.safety.get_honda_fwd_brake())
|
||||||
|
|
||||||
|
def test_brake_safety_check(self):
|
||||||
|
for fwd_brake in [False, True]:
|
||||||
|
self.safety.set_honda_fwd_brake(fwd_brake)
|
||||||
|
for brake in np.arange(0, self.MAX_BRAKE + 10, 1):
|
||||||
|
for controls_allowed in [True, False]:
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
if fwd_brake:
|
||||||
|
send = False # block openpilot brake msg when fwd'ing stock msg
|
||||||
|
elif controls_allowed:
|
||||||
|
send = self.MAX_BRAKE >= brake >= 0
|
||||||
|
else:
|
||||||
|
send = brake == 0
|
||||||
|
self.assertEqual(send, self._tx(self._send_brake_msg(brake)))
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaNidecPcmSafety(HondaPcmEnableBase, TestHondaNidecSafetyBase):
|
||||||
|
"""
|
||||||
|
Covers the Honda Nidec safety mode
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Nidec doesn't disengage on falling edge of cruise. See comment in safety_honda.h
|
||||||
|
def test_disable_control_allowed_from_cruise(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaNidecGasInterceptorSafety(common.GasInterceptorSafetyTest, HondaButtonEnableBase, TestHondaNidecSafetyBase):
|
||||||
|
"""
|
||||||
|
Covers the Honda Nidec safety mode with a gas interceptor, switches to a button-enable car
|
||||||
|
"""
|
||||||
|
|
||||||
|
TX_MSGS = HONDA_N_COMMON_TX_MSGS + [[0x200, 0]]
|
||||||
|
INTERCEPTOR_THRESHOLD = 492
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("honda_civic_touring_2016_can_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HONDA_NIDEC, Panda.FLAG_HONDA_GAS_INTERCEPTOR)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaNidecPcmAltSafety(TestHondaNidecPcmSafety):
|
||||||
|
"""
|
||||||
|
Covers the Honda Nidec safety mode with alt SCM messages
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("acura_ilx_2016_can_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HONDA_NIDEC, Panda.FLAG_HONDA_NIDEC_ALT)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _acc_state_msg(self, main_on):
|
||||||
|
values = {"MAIN_ON": main_on, "COUNTER": self.cnt_acc_state % 4}
|
||||||
|
self.__class__.cnt_acc_state += 1
|
||||||
|
return self.packer.make_can_msg_panda("SCM_BUTTONS", self.PT_BUS, values)
|
||||||
|
|
||||||
|
def _button_msg(self, buttons, main_on=False, bus=None):
|
||||||
|
bus = self.PT_BUS if bus is None else bus
|
||||||
|
values = {"CRUISE_BUTTONS": buttons, "MAIN_ON": main_on, "COUNTER": self.cnt_button % 4}
|
||||||
|
self.__class__.cnt_button += 1
|
||||||
|
return self.packer.make_can_msg_panda("SCM_BUTTONS", bus, values)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaNidecAltGasInterceptorSafety(common.GasInterceptorSafetyTest, HondaButtonEnableBase, TestHondaNidecSafetyBase):
|
||||||
|
"""
|
||||||
|
Covers the Honda Nidec safety mode with alt SCM messages and gas interceptor, switches to a button-enable car
|
||||||
|
"""
|
||||||
|
|
||||||
|
TX_MSGS = HONDA_N_COMMON_TX_MSGS + [[0x200, 0]]
|
||||||
|
INTERCEPTOR_THRESHOLD = 492
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("acura_ilx_2016_can_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HONDA_NIDEC, Panda.FLAG_HONDA_NIDEC_ALT | Panda.FLAG_HONDA_GAS_INTERCEPTOR)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _acc_state_msg(self, main_on):
|
||||||
|
values = {"MAIN_ON": main_on, "COUNTER": self.cnt_acc_state % 4}
|
||||||
|
self.__class__.cnt_acc_state += 1
|
||||||
|
return self.packer.make_can_msg_panda("SCM_BUTTONS", self.PT_BUS, values)
|
||||||
|
|
||||||
|
def _button_msg(self, buttons, main_on=False, bus=None):
|
||||||
|
bus = self.PT_BUS if bus is None else bus
|
||||||
|
values = {"CRUISE_BUTTONS": buttons, "MAIN_ON": main_on, "COUNTER": self.cnt_button % 4}
|
||||||
|
self.__class__.cnt_button += 1
|
||||||
|
return self.packer.make_can_msg_panda("SCM_BUTTONS", bus, values)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ********************* Honda Bosch **********************
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaBoschSafetyBase(HondaBase):
|
||||||
|
PT_BUS = 1
|
||||||
|
STEER_BUS = 0
|
||||||
|
BUTTONS_BUS = 1
|
||||||
|
|
||||||
|
TX_MSGS = [[0xE4, 0], [0xE5, 0], [0x296, 1], [0x33D, 0], [0x33DA, 0], [0x33DB, 0]]
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0xE5, 0x33D, 0x33DA, 0x33DB]}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("honda_accord_2018_can_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
|
||||||
|
def _alt_brake_msg(self, brake):
|
||||||
|
values = {"BRAKE_PRESSED": brake, "COUNTER": self.cnt_brake % 4}
|
||||||
|
self.__class__.cnt_brake += 1
|
||||||
|
return self.packer.make_can_msg_panda("BRAKE_MODULE", self.PT_BUS, values)
|
||||||
|
|
||||||
|
def _send_brake_msg(self, brake):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_alt_disengage_on_brake(self):
|
||||||
|
self.safety.set_honda_alt_brake_msg(1)
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._alt_brake_msg(1))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
self.safety.set_honda_alt_brake_msg(0)
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._alt_brake_msg(1))
|
||||||
|
self.assertTrue(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
def test_spam_cancel_safety_check(self):
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
self.assertTrue(self._tx(self._button_msg(Btn.CANCEL, bus=self.BUTTONS_BUS)))
|
||||||
|
self.assertFalse(self._tx(self._button_msg(Btn.RESUME, bus=self.BUTTONS_BUS)))
|
||||||
|
self.assertFalse(self._tx(self._button_msg(Btn.SET, bus=self.BUTTONS_BUS)))
|
||||||
|
# do not block resume if we are engaged already
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self.assertTrue(self._tx(self._button_msg(Btn.RESUME, bus=self.BUTTONS_BUS)))
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaBoschAltBrakeSafetyBase(TestHondaBoschSafetyBase):
|
||||||
|
"""
|
||||||
|
Base Bosch safety test class with an alternate brake message
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HONDA_BOSCH, Panda.FLAG_HONDA_ALT_BRAKE)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
return self._alt_brake_msg(brake)
|
||||||
|
|
||||||
|
def test_alt_brake_rx_hook(self):
|
||||||
|
self.safety.set_honda_alt_brake_msg(1)
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
to_push = self._alt_brake_msg(0)
|
||||||
|
self.assertTrue(self._rx(to_push))
|
||||||
|
to_push[0].data[2] = to_push[0].data[2] & 0xF0 # invalidate checksum
|
||||||
|
self.assertFalse(self._rx(to_push))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaBoschSafety(HondaPcmEnableBase, TestHondaBoschSafetyBase):
|
||||||
|
"""
|
||||||
|
Covers the Honda Bosch safety mode with stock longitudinal
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HONDA_BOSCH, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaBoschAltBrakeSafety(HondaPcmEnableBase, TestHondaBoschAltBrakeSafetyBase):
|
||||||
|
"""
|
||||||
|
Covers the Honda Bosch safety mode with stock longitudinal and an alternate brake message
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaBoschLongSafety(HondaButtonEnableBase, TestHondaBoschSafetyBase):
|
||||||
|
"""
|
||||||
|
Covers the Honda Bosch safety mode with longitudinal control
|
||||||
|
"""
|
||||||
|
NO_GAS = -30000
|
||||||
|
MAX_GAS = 2000
|
||||||
|
MAX_ACCEL = 2.0 # accel is used for brakes, but openpilot can set positive values
|
||||||
|
MIN_ACCEL = -3.5
|
||||||
|
|
||||||
|
STEER_BUS = 1
|
||||||
|
TX_MSGS = [[0xE4, 1], [0x1DF, 1], [0x1EF, 1], [0x1FA, 1], [0x30C, 1], [0x33D, 1], [0x33DA, 1], [0x33DB, 1], [0x39F, 1], [0x18DAB0F1, 1]]
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0xE5, 0x33D, 0x33DA, 0x33DB]}
|
||||||
|
# 0x1DF is to test that radar is disabled
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x194), 1: (0x1DF,)} # STEERING_CONTROL, ACC_CONTROL
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HONDA_BOSCH, Panda.FLAG_HONDA_BOSCH_LONG)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _send_gas_brake_msg(self, gas, accel):
|
||||||
|
values = {
|
||||||
|
"GAS_COMMAND": gas,
|
||||||
|
"ACCEL_COMMAND": accel,
|
||||||
|
"BRAKE_REQUEST": accel < 0,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("ACC_CONTROL", self.PT_BUS, values)
|
||||||
|
|
||||||
|
# Longitudinal doesn't need to send buttons
|
||||||
|
def test_spam_cancel_safety_check(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_diagnostics(self):
|
||||||
|
tester_present = libpanda_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x02\x3E\x80\x00\x00\x00\x00\x00")
|
||||||
|
self.assertTrue(self._tx(tester_present))
|
||||||
|
|
||||||
|
not_tester_present = libpanda_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x03\xAA\xAA\x00\x00\x00\x00\x00")
|
||||||
|
self.assertFalse(self._tx(not_tester_present))
|
||||||
|
|
||||||
|
def test_gas_safety_check(self):
|
||||||
|
for controls_allowed in [True, False]:
|
||||||
|
for gas in np.arange(self.NO_GAS, self.MAX_GAS + 2000, 100):
|
||||||
|
accel = 0 if gas < 0 else gas / 1000
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
send = (controls_allowed and 0 <= gas <= self.MAX_GAS) or gas == self.NO_GAS
|
||||||
|
self.assertEqual(send, self._tx(self._send_gas_brake_msg(gas, accel)), (controls_allowed, gas, accel))
|
||||||
|
|
||||||
|
def test_brake_safety_check(self):
|
||||||
|
for controls_allowed in [True, False]:
|
||||||
|
for accel in np.arange(self.MIN_ACCEL - 1, self.MAX_ACCEL + 1, 0.01):
|
||||||
|
accel = round(accel, 2) # floats might not hit exact boundary conditions without rounding
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
send = self.MIN_ACCEL <= accel <= self.MAX_ACCEL if controls_allowed else accel == 0
|
||||||
|
self.assertEqual(send, self._tx(self._send_gas_brake_msg(self.NO_GAS, accel)), (controls_allowed, accel))
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaBoschRadarlessSafetyBase(TestHondaBoschSafetyBase):
|
||||||
|
"""Base class for radarless Honda Bosch"""
|
||||||
|
PT_BUS = 0
|
||||||
|
STEER_BUS = 0
|
||||||
|
BUTTONS_BUS = 2 # camera controls ACC, need to send buttons on bus 2
|
||||||
|
|
||||||
|
TX_MSGS = [[0xE4, 0], [0x296, 2], [0x33D, 0]]
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0xE5, 0x33D, 0x33DA, 0x33DB]}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("honda_civic_ex_2022_can_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaBoschRadarlessSafety(HondaPcmEnableBase, TestHondaBoschRadarlessSafetyBase):
|
||||||
|
"""
|
||||||
|
Covers the Honda Bosch Radarless safety mode with stock longitudinal
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HONDA_BOSCH, Panda.FLAG_HONDA_RADARLESS)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaBoschRadarlessAltBrakeSafety(HondaPcmEnableBase, TestHondaBoschRadarlessSafetyBase, TestHondaBoschAltBrakeSafetyBase):
|
||||||
|
"""
|
||||||
|
Covers the Honda Bosch Radarless safety mode with stock longitudinal and an alternate brake message
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HONDA_BOSCH, Panda.FLAG_HONDA_RADARLESS | Panda.FLAG_HONDA_ALT_BRAKE)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHondaBoschRadarlessLongSafety(common.LongitudinalAccelSafetyTest, HondaButtonEnableBase,
|
||||||
|
TestHondaBoschRadarlessSafetyBase):
|
||||||
|
"""
|
||||||
|
Covers the Honda Bosch Radarless safety mode with longitudinal control
|
||||||
|
"""
|
||||||
|
TX_MSGS = [[0xE4, 0], [0x33D, 0], [0x1C8, 0], [0x30C, 0]]
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0xE5, 0x33D, 0x33DA, 0x33DB, 0x1C8, 0x30C]}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HONDA_BOSCH, Panda.FLAG_HONDA_RADARLESS | Panda.FLAG_HONDA_BOSCH_LONG)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _accel_msg(self, accel):
|
||||||
|
values = {
|
||||||
|
"ACCEL_COMMAND": accel,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("ACC_CONTROL", self.PT_BUS, values)
|
||||||
|
|
||||||
|
# Longitudinal doesn't need to send buttons
|
||||||
|
def test_spam_cancel_safety_check(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
216
panda/tests/safety/test_hyundai.py
Normal file
216
panda/tests/safety/test_hyundai.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import random
|
||||||
|
import unittest
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
from panda.tests.safety.hyundai_common import HyundaiButtonBase, HyundaiLongitudinalBase
|
||||||
|
|
||||||
|
|
||||||
|
# 4 bit checkusm used in some hyundai messages
|
||||||
|
# lives outside the can packer because we never send this msg
|
||||||
|
def checksum(msg):
|
||||||
|
addr, t, dat, bus = msg
|
||||||
|
|
||||||
|
chksum = 0
|
||||||
|
if addr == 0x386:
|
||||||
|
for i, b in enumerate(dat):
|
||||||
|
for j in range(8):
|
||||||
|
# exclude checksum and counter bits
|
||||||
|
if (i != 1 or j < 6) and (i != 3 or j < 6) and (i != 5 or j < 6) and (i != 7 or j < 6):
|
||||||
|
bit = (b >> j) & 1
|
||||||
|
else:
|
||||||
|
bit = 0
|
||||||
|
chksum += bit
|
||||||
|
chksum = (chksum ^ 9) & 0xF
|
||||||
|
ret = bytearray(dat)
|
||||||
|
ret[5] |= (chksum & 0x3) << 6
|
||||||
|
ret[7] |= (chksum & 0xc) << 4
|
||||||
|
else:
|
||||||
|
for i, b in enumerate(dat):
|
||||||
|
if addr in [0x260, 0x421] and i == 7:
|
||||||
|
b &= 0x0F if addr == 0x421 else 0xF0
|
||||||
|
elif addr == 0x394 and i == 6:
|
||||||
|
b &= 0xF0
|
||||||
|
elif addr == 0x394 and i == 7:
|
||||||
|
continue
|
||||||
|
chksum += sum(divmod(b, 16))
|
||||||
|
chksum = (16 - chksum) % 16
|
||||||
|
ret = bytearray(dat)
|
||||||
|
ret[6 if addr == 0x394 else 7] |= chksum << (4 if addr == 0x421 else 0)
|
||||||
|
|
||||||
|
return addr, t, ret, bus
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyundaiSafety(HyundaiButtonBase, common.PandaCarSafetyTest, common.DriverTorqueSteeringSafetyTest, common.SteerRequestCutSafetyTest):
|
||||||
|
TX_MSGS = [[0x340, 0], [0x4F1, 0], [0x485, 0]]
|
||||||
|
STANDSTILL_THRESHOLD = 12 # 0.375 kph
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x340,)} # LKAS11
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x340, 0x485]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
MAX_RATE_UP = 3
|
||||||
|
MAX_RATE_DOWN = 7
|
||||||
|
MAX_TORQUE = 384
|
||||||
|
MAX_RT_DELTA = 112
|
||||||
|
RT_INTERVAL = 250000
|
||||||
|
DRIVER_TORQUE_ALLOWANCE = 50
|
||||||
|
DRIVER_TORQUE_FACTOR = 2
|
||||||
|
|
||||||
|
# Safety around steering req bit
|
||||||
|
MIN_VALID_STEERING_FRAMES = 89
|
||||||
|
MAX_INVALID_STEERING_FRAMES = 2
|
||||||
|
MIN_VALID_STEERING_RT_INTERVAL = 810000 # a ~10% buffer, can send steer up to 110Hz
|
||||||
|
|
||||||
|
cnt_gas = 0
|
||||||
|
cnt_speed = 0
|
||||||
|
cnt_brake = 0
|
||||||
|
cnt_cruise = 0
|
||||||
|
cnt_button = 0
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_kia_generic")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _button_msg(self, buttons, main_button=0, bus=0):
|
||||||
|
values = {"CF_Clu_CruiseSwState": buttons, "CF_Clu_CruiseSwMain": main_button, "CF_Clu_AliveCnt1": self.cnt_button}
|
||||||
|
self.__class__.cnt_button += 1
|
||||||
|
return self.packer.make_can_msg_panda("CLU11", bus, values)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"CF_Ems_AclAct": gas, "AliveCounter": self.cnt_gas % 4}
|
||||||
|
self.__class__.cnt_gas += 1
|
||||||
|
return self.packer.make_can_msg_panda("EMS16", 0, values, fix_checksum=checksum)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"DriverOverride": 2 if brake else random.choice((0, 1, 3)),
|
||||||
|
"AliveCounterTCS": self.cnt_brake % 8}
|
||||||
|
self.__class__.cnt_brake += 1
|
||||||
|
return self.packer.make_can_msg_panda("TCS13", 0, values, fix_checksum=checksum)
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
# panda safety doesn't scale, so undo the scaling
|
||||||
|
values = {"WHL_SPD_%s" % s: speed * 0.03125 for s in ["FL", "FR", "RL", "RR"]}
|
||||||
|
values["WHL_SPD_AliveCounter_LSB"] = (self.cnt_speed % 16) & 0x3
|
||||||
|
values["WHL_SPD_AliveCounter_MSB"] = (self.cnt_speed % 16) >> 2
|
||||||
|
self.__class__.cnt_speed += 1
|
||||||
|
return self.packer.make_can_msg_panda("WHL_SPD11", 0, values, fix_checksum=checksum)
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
values = {"ACCMode": enable, "CR_VSM_Alive": self.cnt_cruise % 16}
|
||||||
|
self.__class__.cnt_cruise += 1
|
||||||
|
return self.packer.make_can_msg_panda("SCC12", self.SCC_BUS, values, fix_checksum=checksum)
|
||||||
|
|
||||||
|
def _torque_driver_msg(self, torque):
|
||||||
|
values = {"CR_Mdps_StrColTq": torque}
|
||||||
|
return self.packer.make_can_msg_panda("MDPS12", 0, values)
|
||||||
|
|
||||||
|
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||||
|
values = {"CR_Lkas_StrToqReq": torque, "CF_Lkas_ActToi": steer_req}
|
||||||
|
return self.packer.make_can_msg_panda("LKAS11", 0, values)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyundaiSafetyAltLimits(TestHyundaiSafety):
|
||||||
|
MAX_RATE_UP = 2
|
||||||
|
MAX_RATE_DOWN = 3
|
||||||
|
MAX_TORQUE = 270
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_kia_generic")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI, Panda.FLAG_HYUNDAI_ALT_LIMITS)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyundaiSafetyCameraSCC(TestHyundaiSafety):
|
||||||
|
BUTTONS_TX_BUS = 2 # tx on 2, rx on 0
|
||||||
|
SCC_BUS = 2 # rx on 2
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_kia_generic")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI, Panda.FLAG_HYUNDAI_CAMERA_SCC)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyundaiLegacySafety(TestHyundaiSafety):
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_kia_generic")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI_LEGACY, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyundaiLegacySafetyEV(TestHyundaiSafety):
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_kia_generic")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI_LEGACY, 1)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"Accel_Pedal_Pos": gas}
|
||||||
|
return self.packer.make_can_msg_panda("E_EMS11", 0, values, fix_checksum=checksum)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyundaiLegacySafetyHEV(TestHyundaiSafety):
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_kia_generic")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI_LEGACY, 2)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"CR_Vcu_AccPedDep_Pos": gas}
|
||||||
|
return self.packer.make_can_msg_panda("E_EMS11", 0, values, fix_checksum=checksum)
|
||||||
|
|
||||||
|
class TestHyundaiLongitudinalSafety(HyundaiLongitudinalBase, TestHyundaiSafety):
|
||||||
|
TX_MSGS = [[0x340, 0], [0x4F1, 0], [0x485, 0], [0x420, 0], [0x421, 0], [0x50A, 0], [0x389, 0], [0x4A2, 0], [0x38D, 0], [0x483, 0], [0x7D0, 0]]
|
||||||
|
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x340, 0x421)} # LKAS11, SCC12
|
||||||
|
|
||||||
|
DISABLED_ECU_UDS_MSG = (0x7D0, 0)
|
||||||
|
DISABLED_ECU_ACTUATION_MSG = (0x421, 0)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_kia_generic")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI, Panda.FLAG_HYUNDAI_LONG)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _accel_msg(self, accel, aeb_req=False, aeb_decel=0):
|
||||||
|
values = {
|
||||||
|
"aReqRaw": accel,
|
||||||
|
"aReqValue": accel,
|
||||||
|
"AEB_CmdAct": int(aeb_req),
|
||||||
|
"CR_VSM_DecCmd": aeb_decel,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("SCC12", self.SCC_BUS, values)
|
||||||
|
|
||||||
|
def _fca11_msg(self, idx=0, vsm_aeb_req=False, fca_aeb_req=False, aeb_decel=0):
|
||||||
|
values = {
|
||||||
|
"CR_FCA_Alive": idx % 0xF,
|
||||||
|
"FCA_Status": 2,
|
||||||
|
"CR_VSM_DecCmd": aeb_decel,
|
||||||
|
"CF_VSM_DecCmdAct": int(vsm_aeb_req),
|
||||||
|
"FCA_CmdAct": int(fca_aeb_req),
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("FCA11", 0, values)
|
||||||
|
|
||||||
|
def test_no_aeb_fca11(self):
|
||||||
|
self.assertTrue(self._tx(self._fca11_msg()))
|
||||||
|
self.assertFalse(self._tx(self._fca11_msg(vsm_aeb_req=True)))
|
||||||
|
self.assertFalse(self._tx(self._fca11_msg(fca_aeb_req=True)))
|
||||||
|
self.assertFalse(self._tx(self._fca11_msg(aeb_decel=1.0)))
|
||||||
|
|
||||||
|
def test_no_aeb_scc12(self):
|
||||||
|
self.assertTrue(self._tx(self._accel_msg(0)))
|
||||||
|
self.assertFalse(self._tx(self._accel_msg(0, aeb_req=True)))
|
||||||
|
self.assertFalse(self._tx(self._accel_msg(0, aeb_decel=1.0)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
272
panda/tests/safety/test_hyundai_canfd.py
Normal file
272
panda/tests/safety/test_hyundai_canfd.py
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from parameterized import parameterized_class
|
||||||
|
import unittest
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
from panda.tests.safety.hyundai_common import HyundaiButtonBase, HyundaiLongitudinalBase
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyundaiCanfdBase(HyundaiButtonBase, common.PandaCarSafetyTest, common.DriverTorqueSteeringSafetyTest, common.SteerRequestCutSafetyTest):
|
||||||
|
|
||||||
|
TX_MSGS = [[0x50, 0], [0x1CF, 1], [0x2A4, 0]]
|
||||||
|
STANDSTILL_THRESHOLD = 12 # 0.375 kph
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x50, 0x2a4]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
MAX_RATE_UP = 2
|
||||||
|
MAX_RATE_DOWN = 3
|
||||||
|
MAX_TORQUE = 270
|
||||||
|
|
||||||
|
MAX_RT_DELTA = 112
|
||||||
|
RT_INTERVAL = 250000
|
||||||
|
|
||||||
|
DRIVER_TORQUE_ALLOWANCE = 250
|
||||||
|
DRIVER_TORQUE_FACTOR = 2
|
||||||
|
|
||||||
|
# Safety around steering req bit
|
||||||
|
MIN_VALID_STEERING_FRAMES = 89
|
||||||
|
MAX_INVALID_STEERING_FRAMES = 2
|
||||||
|
MIN_VALID_STEERING_RT_INTERVAL = 810000 # a ~10% buffer, can send steer up to 110Hz
|
||||||
|
|
||||||
|
PT_BUS = 0
|
||||||
|
SCC_BUS = 2
|
||||||
|
STEER_BUS = 0
|
||||||
|
STEER_MSG = ""
|
||||||
|
GAS_MSG = ("", "")
|
||||||
|
BUTTONS_TX_BUS = 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
if cls.__name__ == "TestHyundaiCanfdBase":
|
||||||
|
cls.packer = None
|
||||||
|
cls.safety = None
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
def _torque_driver_msg(self, torque):
|
||||||
|
values = {"STEERING_COL_TORQUE": torque}
|
||||||
|
return self.packer.make_can_msg_panda("MDPS", self.PT_BUS, values)
|
||||||
|
|
||||||
|
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||||
|
values = {"TORQUE_REQUEST": torque, "STEER_REQ": steer_req}
|
||||||
|
return self.packer.make_can_msg_panda(self.STEER_MSG, self.STEER_BUS, values)
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {f"WHEEL_SPEED_{i}": speed * 0.03125 for i in range(1, 5)}
|
||||||
|
return self.packer.make_can_msg_panda("WHEEL_SPEEDS", self.PT_BUS, values)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"DriverBraking": brake}
|
||||||
|
return self.packer.make_can_msg_panda("TCS", self.PT_BUS, values)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {self.GAS_MSG[1]: gas}
|
||||||
|
return self.packer.make_can_msg_panda(self.GAS_MSG[0], self.PT_BUS, values)
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
values = {"ACCMode": 1 if enable else 0}
|
||||||
|
return self.packer.make_can_msg_panda("SCC_CONTROL", self.SCC_BUS, values)
|
||||||
|
|
||||||
|
def _button_msg(self, buttons, main_button=0, bus=None):
|
||||||
|
if bus is None:
|
||||||
|
bus = self.PT_BUS
|
||||||
|
values = {
|
||||||
|
"CRUISE_BUTTONS": buttons,
|
||||||
|
"ADAPTIVE_CRUISE_MAIN_BTN": main_button,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("CRUISE_BUTTONS", bus, values)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyundaiCanfdHDA1Base(TestHyundaiCanfdBase):
|
||||||
|
|
||||||
|
TX_MSGS = [[0x12A, 0], [0x1A0, 1], [0x1CF, 0], [0x1E0, 0]]
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x12A,)} # LFA
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x12A, 0x1E0]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
STEER_MSG = "LFA"
|
||||||
|
BUTTONS_TX_BUS = 2
|
||||||
|
SAFETY_PARAM: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
if cls.__name__ in ("TestHyundaiCanfdHDA1", "TestHyundaiCanfdHDA1AltButtons"):
|
||||||
|
cls.packer = None
|
||||||
|
cls.safety = None
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_canfd")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI_CANFD, self.SAFETY_PARAM)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
@parameterized_class([
|
||||||
|
# Radar SCC, test with long flag to ensure flag is not respected until it is supported
|
||||||
|
{"GAS_MSG": ("ACCELERATOR_BRAKE_ALT", "ACCELERATOR_PEDAL_PRESSED"), "SCC_BUS": 0, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_LONG},
|
||||||
|
{"GAS_MSG": ("ACCELERATOR", "ACCELERATOR_PEDAL"), "SCC_BUS": 0, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_EV_GAS | Panda.FLAG_HYUNDAI_LONG},
|
||||||
|
{"GAS_MSG": ("ACCELERATOR_ALT", "ACCELERATOR_PEDAL"), "SCC_BUS": 0, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_HYBRID_GAS | Panda.FLAG_HYUNDAI_LONG},
|
||||||
|
# Camera SCC
|
||||||
|
{"GAS_MSG": ("ACCELERATOR_BRAKE_ALT", "ACCELERATOR_PEDAL_PRESSED"), "SCC_BUS": 2, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_CAMERA_SCC},
|
||||||
|
{"GAS_MSG": ("ACCELERATOR", "ACCELERATOR_PEDAL"), "SCC_BUS": 2, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_EV_GAS | Panda.FLAG_HYUNDAI_CAMERA_SCC},
|
||||||
|
{"GAS_MSG": ("ACCELERATOR_ALT", "ACCELERATOR_PEDAL"), "SCC_BUS": 2, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_HYBRID_GAS | Panda.FLAG_HYUNDAI_CAMERA_SCC},
|
||||||
|
])
|
||||||
|
class TestHyundaiCanfdHDA1(TestHyundaiCanfdHDA1Base):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@parameterized_class([
|
||||||
|
# Radar SCC, test with long flag to ensure flag is not respected until it is supported
|
||||||
|
{"GAS_MSG": ("ACCELERATOR_BRAKE_ALT", "ACCELERATOR_PEDAL_PRESSED"), "SCC_BUS": 0, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_LONG},
|
||||||
|
{"GAS_MSG": ("ACCELERATOR", "ACCELERATOR_PEDAL"), "SCC_BUS": 0, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_EV_GAS | Panda.FLAG_HYUNDAI_LONG},
|
||||||
|
{"GAS_MSG": ("ACCELERATOR_ALT", "ACCELERATOR_PEDAL"), "SCC_BUS": 0, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_HYBRID_GAS | Panda.FLAG_HYUNDAI_LONG},
|
||||||
|
# Camera SCC
|
||||||
|
{"GAS_MSG": ("ACCELERATOR_BRAKE_ALT", "ACCELERATOR_PEDAL_PRESSED"), "SCC_BUS": 2, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_CAMERA_SCC},
|
||||||
|
{"GAS_MSG": ("ACCELERATOR", "ACCELERATOR_PEDAL"), "SCC_BUS": 2, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_EV_GAS | Panda.FLAG_HYUNDAI_CAMERA_SCC},
|
||||||
|
{"GAS_MSG": ("ACCELERATOR_ALT", "ACCELERATOR_PEDAL"), "SCC_BUS": 2, "SAFETY_PARAM": Panda.FLAG_HYUNDAI_HYBRID_GAS | Panda.FLAG_HYUNDAI_CAMERA_SCC},
|
||||||
|
])
|
||||||
|
class TestHyundaiCanfdHDA1AltButtons(TestHyundaiCanfdHDA1Base):
|
||||||
|
|
||||||
|
SAFETY_PARAM: int
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_canfd")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI_CANFD, Panda.FLAG_HYUNDAI_CANFD_ALT_BUTTONS | self.SAFETY_PARAM)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _button_msg(self, buttons, main_button=0, bus=1):
|
||||||
|
values = {
|
||||||
|
"CRUISE_BUTTONS": buttons,
|
||||||
|
"ADAPTIVE_CRUISE_MAIN_BTN": main_button,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("CRUISE_BUTTONS_ALT", self.PT_BUS, values)
|
||||||
|
|
||||||
|
def test_button_sends(self):
|
||||||
|
"""
|
||||||
|
No button send allowed with alt buttons.
|
||||||
|
"""
|
||||||
|
for enabled in (True, False):
|
||||||
|
for btn in range(8):
|
||||||
|
self.safety.set_controls_allowed(enabled)
|
||||||
|
self.assertFalse(self._tx(self._button_msg(btn)))
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyundaiCanfdHDA2EV(TestHyundaiCanfdBase):
|
||||||
|
|
||||||
|
TX_MSGS = [[0x50, 0], [0x1CF, 1], [0x2A4, 0]]
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x50,)} # LKAS
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x50, 0x2a4]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
PT_BUS = 1
|
||||||
|
SCC_BUS = 1
|
||||||
|
STEER_MSG = "LKAS"
|
||||||
|
GAS_MSG = ("ACCELERATOR", "ACCELERATOR_PEDAL")
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_canfd")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI_CANFD, Panda.FLAG_HYUNDAI_CANFD_HDA2 | Panda.FLAG_HYUNDAI_EV_GAS)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Handle ICE and HEV configurations once we see cars that use the new messages
|
||||||
|
class TestHyundaiCanfdHDA2EVAltSteering(TestHyundaiCanfdBase):
|
||||||
|
|
||||||
|
TX_MSGS = [[0x110, 0], [0x1CF, 1], [0x362, 0]]
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x110,)} # LKAS_ALT
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x110, 0x362]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
PT_BUS = 1
|
||||||
|
SCC_BUS = 1
|
||||||
|
STEER_MSG = "LKAS_ALT"
|
||||||
|
GAS_MSG = ("ACCELERATOR", "ACCELERATOR_PEDAL")
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_canfd")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI_CANFD, Panda.FLAG_HYUNDAI_CANFD_HDA2 | Panda.FLAG_HYUNDAI_EV_GAS |
|
||||||
|
Panda.FLAG_HYUNDAI_CANFD_HDA2_ALT_STEERING)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyundaiCanfdHDA2LongEV(HyundaiLongitudinalBase, TestHyundaiCanfdHDA2EV):
|
||||||
|
|
||||||
|
TX_MSGS = [[0x50, 0], [0x1CF, 1], [0x2A4, 0], [0x51, 0], [0x730, 1], [0x12a, 1], [0x160, 1],
|
||||||
|
[0x1e0, 1], [0x1a0, 1], [0x1ea, 1], [0x200, 1], [0x345, 1], [0x1da, 1]]
|
||||||
|
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x50,), 1: (0x1a0,)} # LKAS, SCC_CONTROL
|
||||||
|
|
||||||
|
DISABLED_ECU_UDS_MSG = (0x730, 1)
|
||||||
|
DISABLED_ECU_ACTUATION_MSG = (0x1a0, 1)
|
||||||
|
|
||||||
|
STEER_MSG = "LFA"
|
||||||
|
GAS_MSG = ("ACCELERATOR", "ACCELERATOR_PEDAL")
|
||||||
|
STEER_BUS = 1
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_canfd")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI_CANFD, Panda.FLAG_HYUNDAI_CANFD_HDA2 | Panda.FLAG_HYUNDAI_LONG | Panda.FLAG_HYUNDAI_EV_GAS)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _accel_msg(self, accel, aeb_req=False, aeb_decel=0):
|
||||||
|
values = {
|
||||||
|
"aReqRaw": accel,
|
||||||
|
"aReqValue": accel,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("SCC_CONTROL", 1, values)
|
||||||
|
|
||||||
|
|
||||||
|
# Tests HDA1 longitudinal for ICE, hybrid, EV
|
||||||
|
@parameterized_class([
|
||||||
|
# Camera SCC is the only supported configuration for HDA1 longitudinal, TODO: allow radar SCC
|
||||||
|
{"GAS_MSG": ("ACCELERATOR_BRAKE_ALT", "ACCELERATOR_PEDAL_PRESSED"), "SAFETY_PARAM": Panda.FLAG_HYUNDAI_LONG},
|
||||||
|
{"GAS_MSG": ("ACCELERATOR", "ACCELERATOR_PEDAL"), "SAFETY_PARAM": Panda.FLAG_HYUNDAI_LONG | Panda.FLAG_HYUNDAI_EV_GAS},
|
||||||
|
{"GAS_MSG": ("ACCELERATOR_ALT", "ACCELERATOR_PEDAL"), "SAFETY_PARAM": Panda.FLAG_HYUNDAI_LONG | Panda.FLAG_HYUNDAI_HYBRID_GAS},
|
||||||
|
])
|
||||||
|
class TestHyundaiCanfdHDA1Long(HyundaiLongitudinalBase, TestHyundaiCanfdHDA1Base):
|
||||||
|
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x12a, 0x1e0, 0x1a0]}
|
||||||
|
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x12A, 0x1a0)} # LFA, SCC_CONTROL
|
||||||
|
|
||||||
|
DISABLED_ECU_UDS_MSG = (0x730, 1)
|
||||||
|
DISABLED_ECU_ACTUATION_MSG = (0x1a0, 0)
|
||||||
|
|
||||||
|
STEER_MSG = "LFA"
|
||||||
|
STEER_BUS = 0
|
||||||
|
SCC_BUS = 2
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls.__name__ == "TestHyundaiCanfdHDA1Long":
|
||||||
|
cls.safety = None
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("hyundai_canfd")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_HYUNDAI_CANFD, Panda.FLAG_HYUNDAI_CAMERA_SCC | self.SAFETY_PARAM)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _accel_msg(self, accel, aeb_req=False, aeb_decel=0):
|
||||||
|
values = {
|
||||||
|
"aReqRaw": accel,
|
||||||
|
"aReqValue": accel,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("SCC_CONTROL", 0, values)
|
||||||
|
|
||||||
|
# no knockout
|
||||||
|
def test_tester_present_allowed(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
86
panda/tests/safety/test_mazda.py
Normal file
86
panda/tests/safety/test_mazda.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
|
||||||
|
|
||||||
|
class TestMazdaSafety(common.PandaCarSafetyTest, common.DriverTorqueSteeringSafetyTest):
|
||||||
|
|
||||||
|
TX_MSGS = [[0x243, 0], [0x09d, 0], [0x440, 0]]
|
||||||
|
STANDSTILL_THRESHOLD = .1
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x243,)}
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x243, 0x440]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
MAX_RATE_UP = 10
|
||||||
|
MAX_RATE_DOWN = 25
|
||||||
|
MAX_TORQUE = 800
|
||||||
|
|
||||||
|
MAX_RT_DELTA = 300
|
||||||
|
RT_INTERVAL = 250000
|
||||||
|
|
||||||
|
DRIVER_TORQUE_ALLOWANCE = 15
|
||||||
|
DRIVER_TORQUE_FACTOR = 1
|
||||||
|
|
||||||
|
# Mazda actually does not set any bit when requesting torque
|
||||||
|
NO_STEER_REQ_BIT = True
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("mazda_2017")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_MAZDA, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _torque_meas_msg(self, torque):
|
||||||
|
values = {"STEER_TORQUE_MOTOR": torque}
|
||||||
|
return self.packer.make_can_msg_panda("STEER_TORQUE", 0, values)
|
||||||
|
|
||||||
|
def _torque_driver_msg(self, torque):
|
||||||
|
values = {"STEER_TORQUE_SENSOR": torque}
|
||||||
|
return self.packer.make_can_msg_panda("STEER_TORQUE", 0, values)
|
||||||
|
|
||||||
|
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||||
|
values = {"LKAS_REQUEST": torque}
|
||||||
|
return self.packer.make_can_msg_panda("CAM_LKAS", 0, values)
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {"SPEED": speed}
|
||||||
|
return self.packer.make_can_msg_panda("ENGINE_DATA", 0, values)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"BRAKE_ON": brake}
|
||||||
|
return self.packer.make_can_msg_panda("PEDALS", 0, values)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"PEDAL_GAS": gas}
|
||||||
|
return self.packer.make_can_msg_panda("ENGINE_DATA", 0, values)
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
values = {"CRZ_ACTIVE": enable}
|
||||||
|
return self.packer.make_can_msg_panda("CRZ_CTRL", 0, values)
|
||||||
|
|
||||||
|
def _button_msg(self, resume=False, cancel=False):
|
||||||
|
values = {
|
||||||
|
"CAN_OFF": cancel,
|
||||||
|
"CAN_OFF_INV": (cancel + 1) % 2,
|
||||||
|
"RES": resume,
|
||||||
|
"RES_INV": (resume + 1) % 2,
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("CRZ_BTNS", 0, values)
|
||||||
|
|
||||||
|
def test_buttons(self):
|
||||||
|
# only cancel allows while controls not allowed
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
self.assertTrue(self._tx(self._button_msg(cancel=True)))
|
||||||
|
self.assertFalse(self._tx(self._button_msg(resume=True)))
|
||||||
|
|
||||||
|
# do not block resume if we are engaged already
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self.assertTrue(self._tx(self._button_msg(cancel=True)))
|
||||||
|
self.assertTrue(self._tx(self._button_msg(resume=True)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
117
panda/tests/safety/test_nissan.py
Normal file
117
panda/tests/safety/test_nissan.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
|
||||||
|
|
||||||
|
class TestNissanSafety(common.PandaCarSafetyTest, common.AngleSteeringSafetyTest):
|
||||||
|
|
||||||
|
TX_MSGS = [[0x169, 0], [0x2b1, 0], [0x4cc, 0], [0x20b, 2], [0x280, 2]]
|
||||||
|
STANDSTILL_THRESHOLD = 0
|
||||||
|
GAS_PRESSED_THRESHOLD = 3
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x169,)}
|
||||||
|
FWD_BLACKLISTED_ADDRS = {0: [0x280], 2: [0x169, 0x2b1, 0x4cc]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
EPS_BUS = 0
|
||||||
|
CRUISE_BUS = 2
|
||||||
|
|
||||||
|
# Angle control limits
|
||||||
|
DEG_TO_CAN = 100
|
||||||
|
|
||||||
|
ANGLE_RATE_BP = [0., 5., 15.]
|
||||||
|
ANGLE_RATE_UP = [5., .8, .15] # windup limit
|
||||||
|
ANGLE_RATE_DOWN = [5., 3.5, .4] # unwind limit
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("nissan_x_trail_2017_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_NISSAN, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _angle_cmd_msg(self, angle: float, enabled: bool):
|
||||||
|
values = {"DESIRED_ANGLE": angle, "LKA_ACTIVE": 1 if enabled else 0}
|
||||||
|
return self.packer.make_can_msg_panda("LKAS", 0, values)
|
||||||
|
|
||||||
|
def _angle_meas_msg(self, angle: float):
|
||||||
|
values = {"STEER_ANGLE": angle}
|
||||||
|
return self.packer.make_can_msg_panda("STEER_ANGLE_SENSOR", self.EPS_BUS, values)
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
values = {"CRUISE_ENABLED": enable}
|
||||||
|
return self.packer.make_can_msg_panda("CRUISE_STATE", self.CRUISE_BUS, values)
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {"WHEEL_SPEED_%s" % s: speed * 3.6 for s in ["RR", "RL"]}
|
||||||
|
return self.packer.make_can_msg_panda("WHEEL_SPEEDS_REAR", self.EPS_BUS, values)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"USER_BRAKE_PRESSED": brake}
|
||||||
|
return self.packer.make_can_msg_panda("DOORS_LIGHTS", self.EPS_BUS, values)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"GAS_PEDAL": gas}
|
||||||
|
return self.packer.make_can_msg_panda("GAS_PEDAL", self.EPS_BUS, values)
|
||||||
|
|
||||||
|
def _acc_button_cmd(self, cancel=0, propilot=0, flw_dist=0, _set=0, res=0):
|
||||||
|
no_button = not any([cancel, propilot, flw_dist, _set, res])
|
||||||
|
values = {"CANCEL_BUTTON": cancel, "PROPILOT_BUTTON": propilot,
|
||||||
|
"FOLLOW_DISTANCE_BUTTON": flw_dist, "SET_BUTTON": _set,
|
||||||
|
"RES_BUTTON": res, "NO_BUTTON_PRESSED": no_button}
|
||||||
|
return self.packer.make_can_msg_panda("CRUISE_THROTTLE", 2, values)
|
||||||
|
|
||||||
|
def test_acc_buttons(self):
|
||||||
|
btns = [
|
||||||
|
("cancel", True),
|
||||||
|
("propilot", False),
|
||||||
|
("flw_dist", False),
|
||||||
|
("_set", False),
|
||||||
|
("res", False),
|
||||||
|
(None, False),
|
||||||
|
]
|
||||||
|
for controls_allowed in (True, False):
|
||||||
|
for btn, should_tx in btns:
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
args = {} if btn is None else {btn: 1}
|
||||||
|
tx = self._tx(self._acc_button_cmd(**args))
|
||||||
|
self.assertEqual(tx, should_tx)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNissanSafetyAltEpsBus(TestNissanSafety):
|
||||||
|
"""Altima uses different buses"""
|
||||||
|
|
||||||
|
EPS_BUS = 1
|
||||||
|
CRUISE_BUS = 1
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("nissan_x_trail_2017_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_NISSAN, Panda.FLAG_NISSAN_ALT_EPS_BUS)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestNissanLeafSafety(TestNissanSafety):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("nissan_leaf_2018_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_NISSAN, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"USER_BRAKE_PRESSED": brake}
|
||||||
|
return self.packer.make_can_msg_panda("CRUISE_THROTTLE", 0, values)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"GAS_PEDAL": gas}
|
||||||
|
return self.packer.make_can_msg_panda("CRUISE_THROTTLE", 0, values)
|
||||||
|
|
||||||
|
# TODO: leaf should use its own safety param
|
||||||
|
def test_acc_buttons(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
228
panda/tests/safety/test_subaru.py
Normal file
228
panda/tests/safety/test_subaru.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import enum
|
||||||
|
import unittest
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
class SubaruMsg(enum.IntEnum):
|
||||||
|
Brake_Status = 0x13c
|
||||||
|
CruiseControl = 0x240
|
||||||
|
Throttle = 0x40
|
||||||
|
Steering_Torque = 0x119
|
||||||
|
Wheel_Speeds = 0x13a
|
||||||
|
ES_LKAS = 0x122
|
||||||
|
ES_LKAS_ANGLE = 0x124
|
||||||
|
ES_Brake = 0x220
|
||||||
|
ES_Distance = 0x221
|
||||||
|
ES_Status = 0x222
|
||||||
|
ES_DashStatus = 0x321
|
||||||
|
ES_LKAS_State = 0x322
|
||||||
|
ES_Infotainment = 0x323
|
||||||
|
ES_UDS_Request = 0x787
|
||||||
|
ES_HighBeamAssist = 0x121
|
||||||
|
ES_STATIC_1 = 0x22a
|
||||||
|
ES_STATIC_2 = 0x325
|
||||||
|
|
||||||
|
|
||||||
|
SUBARU_MAIN_BUS = 0
|
||||||
|
SUBARU_ALT_BUS = 1
|
||||||
|
SUBARU_CAM_BUS = 2
|
||||||
|
|
||||||
|
|
||||||
|
def lkas_tx_msgs(alt_bus, lkas_msg=SubaruMsg.ES_LKAS):
|
||||||
|
return [[lkas_msg, SUBARU_MAIN_BUS],
|
||||||
|
[SubaruMsg.ES_Distance, alt_bus],
|
||||||
|
[SubaruMsg.ES_DashStatus, SUBARU_MAIN_BUS],
|
||||||
|
[SubaruMsg.ES_LKAS_State, SUBARU_MAIN_BUS],
|
||||||
|
[SubaruMsg.ES_Infotainment, SUBARU_MAIN_BUS]]
|
||||||
|
|
||||||
|
def long_tx_msgs(alt_bus):
|
||||||
|
return [[SubaruMsg.ES_Brake, alt_bus],
|
||||||
|
[SubaruMsg.ES_Status, alt_bus]]
|
||||||
|
|
||||||
|
def gen2_long_additional_tx_msgs():
|
||||||
|
return [[SubaruMsg.ES_UDS_Request, SUBARU_CAM_BUS],
|
||||||
|
[SubaruMsg.ES_HighBeamAssist, SUBARU_MAIN_BUS],
|
||||||
|
[SubaruMsg.ES_STATIC_1, SUBARU_MAIN_BUS],
|
||||||
|
[SubaruMsg.ES_STATIC_2, SUBARU_MAIN_BUS]]
|
||||||
|
|
||||||
|
def fwd_blacklisted_addr(lkas_msg=SubaruMsg.ES_LKAS):
|
||||||
|
return {SUBARU_CAM_BUS: [lkas_msg, SubaruMsg.ES_DashStatus, SubaruMsg.ES_LKAS_State, SubaruMsg.ES_Infotainment]}
|
||||||
|
|
||||||
|
class TestSubaruSafetyBase(common.PandaCarSafetyTest):
|
||||||
|
FLAGS = 0
|
||||||
|
STANDSTILL_THRESHOLD = 0 # kph
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {SUBARU_MAIN_BUS: (SubaruMsg.ES_LKAS,)}
|
||||||
|
FWD_BUS_LOOKUP = {SUBARU_MAIN_BUS: SUBARU_CAM_BUS, SUBARU_CAM_BUS: SUBARU_MAIN_BUS}
|
||||||
|
FWD_BLACKLISTED_ADDRS = fwd_blacklisted_addr()
|
||||||
|
|
||||||
|
MAX_RT_DELTA = 940
|
||||||
|
RT_INTERVAL = 250000
|
||||||
|
|
||||||
|
DRIVER_TORQUE_ALLOWANCE = 60
|
||||||
|
DRIVER_TORQUE_FACTOR = 50
|
||||||
|
|
||||||
|
ALT_MAIN_BUS = SUBARU_MAIN_BUS
|
||||||
|
ALT_CAM_BUS = SUBARU_CAM_BUS
|
||||||
|
|
||||||
|
DEG_TO_CAN = 100
|
||||||
|
|
||||||
|
INACTIVE_GAS = 1818
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("subaru_global_2017_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_SUBARU, self.FLAGS)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _set_prev_torque(self, t):
|
||||||
|
self.safety.set_desired_torque_last(t)
|
||||||
|
self.safety.set_rt_torque_last(t)
|
||||||
|
|
||||||
|
def _torque_driver_msg(self, torque):
|
||||||
|
values = {"Steer_Torque_Sensor": torque}
|
||||||
|
return self.packer.make_can_msg_panda("Steering_Torque", 0, values)
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {s: speed for s in ["FR", "FL", "RR", "RL"]}
|
||||||
|
return self.packer.make_can_msg_panda("Wheel_Speeds", self.ALT_MAIN_BUS, values)
|
||||||
|
|
||||||
|
def _angle_meas_msg(self, angle):
|
||||||
|
values = {"Steering_Angle": angle}
|
||||||
|
return self.packer.make_can_msg_panda("Steering_Torque", 0, values)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"Brake": brake}
|
||||||
|
return self.packer.make_can_msg_panda("Brake_Status", self.ALT_MAIN_BUS, values)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"Throttle_Pedal": gas}
|
||||||
|
return self.packer.make_can_msg_panda("Throttle", 0, values)
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
values = {"Cruise_Activated": enable}
|
||||||
|
return self.packer.make_can_msg_panda("CruiseControl", self.ALT_MAIN_BUS, values)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubaruStockLongitudinalSafetyBase(TestSubaruSafetyBase):
|
||||||
|
def _cancel_msg(self, cancel, cruise_throttle=0):
|
||||||
|
values = {"Cruise_Cancel": cancel, "Cruise_Throttle": cruise_throttle}
|
||||||
|
return self.packer.make_can_msg_panda("ES_Distance", self.ALT_MAIN_BUS, values)
|
||||||
|
|
||||||
|
def test_cancel_message(self):
|
||||||
|
# test that we can only send the cancel message (ES_Distance) with inactive throttle (1818) and Cruise_Cancel=1
|
||||||
|
for cancel in [True, False]:
|
||||||
|
self._generic_limit_safety_check(partial(self._cancel_msg, cancel), self.INACTIVE_GAS, self.INACTIVE_GAS, 0, 2**12, 1, self.INACTIVE_GAS, cancel)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubaruLongitudinalSafetyBase(TestSubaruSafetyBase, common.LongitudinalGasBrakeSafetyTest):
|
||||||
|
MIN_GAS = 808
|
||||||
|
MAX_GAS = 3400
|
||||||
|
INACTIVE_GAS = 1818
|
||||||
|
MAX_POSSIBLE_GAS = 2**13
|
||||||
|
|
||||||
|
MIN_BRAKE = 0
|
||||||
|
MAX_BRAKE = 600
|
||||||
|
MAX_POSSIBLE_BRAKE = 2**16
|
||||||
|
|
||||||
|
MIN_RPM = 0
|
||||||
|
MAX_RPM = 2400
|
||||||
|
MAX_POSSIBLE_RPM = 2**13
|
||||||
|
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [SubaruMsg.ES_LKAS, SubaruMsg.ES_Brake, SubaruMsg.ES_Distance,
|
||||||
|
SubaruMsg.ES_Status, SubaruMsg.ES_DashStatus,
|
||||||
|
SubaruMsg.ES_LKAS_State, SubaruMsg.ES_Infotainment]}
|
||||||
|
|
||||||
|
def test_rpm_safety_check(self):
|
||||||
|
self._generic_limit_safety_check(self._send_rpm_msg, self.MIN_RPM, self.MAX_RPM, 0, self.MAX_POSSIBLE_RPM, 1)
|
||||||
|
|
||||||
|
def _send_brake_msg(self, brake):
|
||||||
|
values = {"Brake_Pressure": brake}
|
||||||
|
return self.packer.make_can_msg_panda("ES_Brake", self.ALT_MAIN_BUS, values)
|
||||||
|
|
||||||
|
def _send_gas_msg(self, gas):
|
||||||
|
values = {"Cruise_Throttle": gas}
|
||||||
|
return self.packer.make_can_msg_panda("ES_Distance", self.ALT_MAIN_BUS, values)
|
||||||
|
|
||||||
|
def _send_rpm_msg(self, rpm):
|
||||||
|
values = {"Cruise_RPM": rpm}
|
||||||
|
return self.packer.make_can_msg_panda("ES_Status", self.ALT_MAIN_BUS, values)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubaruTorqueSafetyBase(TestSubaruSafetyBase, common.DriverTorqueSteeringSafetyTest, common.SteerRequestCutSafetyTest):
|
||||||
|
MAX_RATE_UP = 50
|
||||||
|
MAX_RATE_DOWN = 70
|
||||||
|
MAX_TORQUE = 2047
|
||||||
|
|
||||||
|
# Safety around steering req bit
|
||||||
|
MIN_VALID_STEERING_FRAMES = 7
|
||||||
|
MAX_INVALID_STEERING_FRAMES = 1
|
||||||
|
MIN_VALID_STEERING_RT_INTERVAL = 144000
|
||||||
|
|
||||||
|
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||||
|
values = {"LKAS_Output": torque, "LKAS_Request": steer_req}
|
||||||
|
return self.packer.make_can_msg_panda("ES_LKAS", SUBARU_MAIN_BUS, values)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubaruGen1TorqueStockLongitudinalSafety(TestSubaruStockLongitudinalSafetyBase, TestSubaruTorqueSafetyBase):
|
||||||
|
FLAGS = 0
|
||||||
|
TX_MSGS = lkas_tx_msgs(SUBARU_MAIN_BUS)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubaruGen2TorqueSafetyBase(TestSubaruTorqueSafetyBase):
|
||||||
|
ALT_MAIN_BUS = SUBARU_ALT_BUS
|
||||||
|
ALT_CAM_BUS = SUBARU_ALT_BUS
|
||||||
|
|
||||||
|
MAX_RATE_UP = 40
|
||||||
|
MAX_RATE_DOWN = 40
|
||||||
|
MAX_TORQUE = 1000
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubaruGen2TorqueStockLongitudinalSafety(TestSubaruStockLongitudinalSafetyBase, TestSubaruGen2TorqueSafetyBase):
|
||||||
|
FLAGS = Panda.FLAG_SUBARU_GEN2
|
||||||
|
TX_MSGS = lkas_tx_msgs(SUBARU_ALT_BUS)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubaruGen1LongitudinalSafety(TestSubaruLongitudinalSafetyBase, TestSubaruTorqueSafetyBase):
|
||||||
|
FLAGS = Panda.FLAG_SUBARU_LONG
|
||||||
|
TX_MSGS = lkas_tx_msgs(SUBARU_MAIN_BUS) + long_tx_msgs(SUBARU_MAIN_BUS)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubaruGen2LongitudinalSafety(TestSubaruLongitudinalSafetyBase, TestSubaruGen2TorqueSafetyBase):
|
||||||
|
FLAGS = Panda.FLAG_SUBARU_LONG | Panda.FLAG_SUBARU_GEN2
|
||||||
|
TX_MSGS = lkas_tx_msgs(SUBARU_ALT_BUS) + long_tx_msgs(SUBARU_ALT_BUS) + gen2_long_additional_tx_msgs()
|
||||||
|
|
||||||
|
def _rdbi_msg(self, did: int):
|
||||||
|
return b'\x03\x22' + did.to_bytes(2) + b'\x00\x00\x00\x00'
|
||||||
|
|
||||||
|
def _es_uds_msg(self, msg: bytes):
|
||||||
|
return libpanda_py.make_CANPacket(SubaruMsg.ES_UDS_Request, 2, msg)
|
||||||
|
|
||||||
|
def test_es_uds_message(self):
|
||||||
|
tester_present = b'\x02\x3E\x80\x00\x00\x00\x00\x00'
|
||||||
|
not_tester_present = b"\x03\xAA\xAA\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
button_did = 0x1130
|
||||||
|
|
||||||
|
# Tester present is allowed for gen2 long to keep eyesight disabled
|
||||||
|
self.assertTrue(self._tx(self._es_uds_msg(tester_present)))
|
||||||
|
|
||||||
|
# Non-Tester present is not allowed
|
||||||
|
self.assertFalse(self._tx(self._es_uds_msg(not_tester_present)))
|
||||||
|
|
||||||
|
# Only button_did is allowed to be read via UDS
|
||||||
|
for did in range(0xFFFF):
|
||||||
|
should_tx = (did == button_did)
|
||||||
|
self.assertEqual(self._tx(self._es_uds_msg(self._rdbi_msg(did))), should_tx)
|
||||||
|
|
||||||
|
# any other msg is not allowed
|
||||||
|
for sid in range(0xFF):
|
||||||
|
msg = b'\x03' + sid.to_bytes(1) + b'\x00' * 6
|
||||||
|
self.assertFalse(self._tx(self._es_uds_msg(msg)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
70
panda/tests/safety/test_subaru_preglobal.py
Normal file
70
panda/tests/safety/test_subaru_preglobal.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubaruPreglobalSafety(common.PandaCarSafetyTest, common.DriverTorqueSteeringSafetyTest):
|
||||||
|
FLAGS = 0
|
||||||
|
DBC = "subaru_outback_2015_generated"
|
||||||
|
TX_MSGS = [[0x161, 0], [0x164, 0]]
|
||||||
|
STANDSTILL_THRESHOLD = 0 # kph
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x164,)}
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x161, 0x164]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
MAX_RATE_UP = 50
|
||||||
|
MAX_RATE_DOWN = 70
|
||||||
|
MAX_TORQUE = 2047
|
||||||
|
|
||||||
|
MAX_RT_DELTA = 940
|
||||||
|
RT_INTERVAL = 250000
|
||||||
|
|
||||||
|
DRIVER_TORQUE_ALLOWANCE = 75
|
||||||
|
DRIVER_TORQUE_FACTOR = 10
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda(self.DBC)
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_SUBARU_PREGLOBAL, self.FLAGS)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _set_prev_torque(self, t):
|
||||||
|
self.safety.set_desired_torque_last(t)
|
||||||
|
self.safety.set_rt_torque_last(t)
|
||||||
|
|
||||||
|
def _torque_driver_msg(self, torque):
|
||||||
|
values = {"Steer_Torque_Sensor": torque}
|
||||||
|
return self.packer.make_can_msg_panda("Steering_Torque", 0, values)
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
# subaru safety doesn't use the scaled value, so undo the scaling
|
||||||
|
values = {s: speed*0.0592 for s in ["FR", "FL", "RR", "RL"]}
|
||||||
|
return self.packer.make_can_msg_panda("Wheel_Speeds", 0, values)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"Brake_Pedal": brake}
|
||||||
|
return self.packer.make_can_msg_panda("Brake_Pedal", 0, values)
|
||||||
|
|
||||||
|
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||||
|
values = {"LKAS_Command": torque, "LKAS_Active": steer_req}
|
||||||
|
return self.packer.make_can_msg_panda("ES_LKAS", 0, values)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"Throttle_Pedal": gas}
|
||||||
|
return self.packer.make_can_msg_panda("Throttle", 0, values)
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
values = {"Cruise_Activated": enable}
|
||||||
|
return self.packer.make_can_msg_panda("CruiseControl", 0, values)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubaruPreglobalReversedDriverTorqueSafety(TestSubaruPreglobalSafety):
|
||||||
|
FLAGS = Panda.FLAG_SUBARU_PREGLOBAL_REVERSED_DRIVER_TORQUE
|
||||||
|
DBC = "subaru_outback_2019_generated"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
185
panda/tests/safety/test_tesla.py
Normal file
185
panda/tests/safety/test_tesla.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
import numpy as np
|
||||||
|
from panda import Panda
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
|
||||||
|
MAX_ACCEL = 2.0
|
||||||
|
MIN_ACCEL = -3.5
|
||||||
|
|
||||||
|
|
||||||
|
class CONTROL_LEVER_STATE:
|
||||||
|
DN_1ST = 32
|
||||||
|
UP_1ST = 16
|
||||||
|
DN_2ND = 8
|
||||||
|
UP_2ND = 4
|
||||||
|
RWD = 2
|
||||||
|
FWD = 1
|
||||||
|
IDLE = 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestTeslaSafety(common.PandaCarSafetyTest):
|
||||||
|
STANDSTILL_THRESHOLD = 0
|
||||||
|
GAS_PRESSED_THRESHOLD = 3
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = None
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {"DI_vehicleSpeed": speed / 0.447}
|
||||||
|
return self.packer.make_can_msg_panda("DI_torque2", 0, values)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"driverBrakeStatus": 2 if brake else 1}
|
||||||
|
return self.packer.make_can_msg_panda("BrakeMessage", 0, values)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"DI_pedalPos": gas}
|
||||||
|
return self.packer.make_can_msg_panda("DI_torque1", 0, values)
|
||||||
|
|
||||||
|
def _control_lever_cmd(self, command):
|
||||||
|
values = {"SpdCtrlLvr_Stat": command}
|
||||||
|
return self.packer.make_can_msg_panda("STW_ACTN_RQ", 0, values)
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
values = {"DI_cruiseState": 2 if enable else 0}
|
||||||
|
return self.packer.make_can_msg_panda("DI_state", 0, values)
|
||||||
|
|
||||||
|
def _long_control_msg(self, set_speed, acc_val=0, jerk_limits=(0, 0), accel_limits=(0, 0), aeb_event=0, bus=0):
|
||||||
|
values = {
|
||||||
|
"DAS_setSpeed": set_speed,
|
||||||
|
"DAS_accState": acc_val,
|
||||||
|
"DAS_aebEvent": aeb_event,
|
||||||
|
"DAS_jerkMin": jerk_limits[0],
|
||||||
|
"DAS_jerkMax": jerk_limits[1],
|
||||||
|
"DAS_accelMin": accel_limits[0],
|
||||||
|
"DAS_accelMax": accel_limits[1],
|
||||||
|
}
|
||||||
|
return self.packer.make_can_msg_panda("DAS_control", bus, values)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTeslaSteeringSafety(TestTeslaSafety, common.AngleSteeringSafetyTest):
|
||||||
|
TX_MSGS = [[0x488, 0], [0x45, 0], [0x45, 2]]
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x488,)}
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x488]}
|
||||||
|
|
||||||
|
# Angle control limits
|
||||||
|
DEG_TO_CAN = 10
|
||||||
|
|
||||||
|
ANGLE_RATE_BP = [0., 5., 15.]
|
||||||
|
ANGLE_RATE_UP = [10., 1.6, .3] # windup limit
|
||||||
|
ANGLE_RATE_DOWN = [10., 7.0, .8] # unwind limit
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("tesla_can")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_TESLA, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _angle_cmd_msg(self, angle: float, enabled: bool):
|
||||||
|
values = {"DAS_steeringAngleRequest": angle, "DAS_steeringControlType": 1 if enabled else 0}
|
||||||
|
return self.packer.make_can_msg_panda("DAS_steeringControl", 0, values)
|
||||||
|
|
||||||
|
def _angle_meas_msg(self, angle: float):
|
||||||
|
values = {"EPAS_internalSAS": angle}
|
||||||
|
return self.packer.make_can_msg_panda("EPAS_sysStatus", 0, values)
|
||||||
|
|
||||||
|
def test_acc_buttons(self):
|
||||||
|
"""
|
||||||
|
FWD (cancel) always allowed.
|
||||||
|
"""
|
||||||
|
btns = [
|
||||||
|
(CONTROL_LEVER_STATE.FWD, True),
|
||||||
|
(CONTROL_LEVER_STATE.RWD, False),
|
||||||
|
(CONTROL_LEVER_STATE.UP_1ST, False),
|
||||||
|
(CONTROL_LEVER_STATE.UP_2ND, False),
|
||||||
|
(CONTROL_LEVER_STATE.DN_1ST, False),
|
||||||
|
(CONTROL_LEVER_STATE.DN_2ND, False),
|
||||||
|
(CONTROL_LEVER_STATE.IDLE, False),
|
||||||
|
]
|
||||||
|
for btn, should_tx in btns:
|
||||||
|
for controls_allowed in (True, False):
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
tx = self._tx(self._control_lever_cmd(btn))
|
||||||
|
self.assertEqual(tx, should_tx)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTeslaRavenSteeringSafety(TestTeslaSteeringSafety):
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("tesla_can")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_TESLA, Panda.FLAG_TESLA_RAVEN)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _angle_meas_msg(self, angle: float):
|
||||||
|
values = {"EPAS_internalSAS": angle}
|
||||||
|
return self.packer.make_can_msg_panda("EPAS3P_sysStatus", 2, values)
|
||||||
|
|
||||||
|
class TestTeslaLongitudinalSafety(TestTeslaSafety):
|
||||||
|
def setUp(self):
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
def test_no_aeb(self):
|
||||||
|
for aeb_event in range(4):
|
||||||
|
self.assertEqual(self._tx(self._long_control_msg(10, aeb_event=aeb_event)), aeb_event == 0)
|
||||||
|
|
||||||
|
def test_stock_aeb_passthrough(self):
|
||||||
|
no_aeb_msg = self._long_control_msg(10, aeb_event=0)
|
||||||
|
no_aeb_msg_cam = self._long_control_msg(10, aeb_event=0, bus=2)
|
||||||
|
aeb_msg_cam = self._long_control_msg(10, aeb_event=1, bus=2)
|
||||||
|
|
||||||
|
# stock system sends no AEB -> no forwarding, and OP is allowed to TX
|
||||||
|
self.assertEqual(1, self._rx(no_aeb_msg_cam))
|
||||||
|
self.assertEqual(-1, self.safety.safety_fwd_hook(2, no_aeb_msg_cam.addr))
|
||||||
|
self.assertEqual(True, self._tx(no_aeb_msg))
|
||||||
|
|
||||||
|
# stock system sends AEB -> forwarding, and OP is not allowed to TX
|
||||||
|
self.assertEqual(1, self._rx(aeb_msg_cam))
|
||||||
|
self.assertEqual(0, self.safety.safety_fwd_hook(2, aeb_msg_cam.addr))
|
||||||
|
self.assertEqual(False, self._tx(no_aeb_msg))
|
||||||
|
|
||||||
|
def test_acc_accel_limits(self):
|
||||||
|
for controls_allowed in [True, False]:
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
for min_accel in np.arange(MIN_ACCEL - 1, MAX_ACCEL + 1, 0.1):
|
||||||
|
for max_accel in np.arange(MIN_ACCEL - 1, MAX_ACCEL + 1, 0.1):
|
||||||
|
# floats might not hit exact boundary conditions without rounding
|
||||||
|
min_accel = round(min_accel, 2)
|
||||||
|
max_accel = round(max_accel, 2)
|
||||||
|
if controls_allowed:
|
||||||
|
send = (MIN_ACCEL <= min_accel <= MAX_ACCEL) and (MIN_ACCEL <= max_accel <= MAX_ACCEL)
|
||||||
|
else:
|
||||||
|
send = np.all(np.isclose([min_accel, max_accel], 0, atol=0.0001))
|
||||||
|
self.assertEqual(send, self._tx(self._long_control_msg(10, acc_val=4, accel_limits=[min_accel, max_accel])))
|
||||||
|
|
||||||
|
|
||||||
|
class TestTeslaChassisLongitudinalSafety(TestTeslaLongitudinalSafety):
|
||||||
|
TX_MSGS = [[0x488, 0], [0x45, 0], [0x45, 2], [0x2B9, 0]]
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x488,)}
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x2B9, 0x488]}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("tesla_can")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_TESLA, Panda.FLAG_TESLA_LONG_CONTROL)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestTeslaPTLongitudinalSafety(TestTeslaLongitudinalSafety):
|
||||||
|
TX_MSGS = [[0x2BF, 0]]
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x2BF,)}
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x2BF]}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("tesla_powertrain")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_TESLA, Panda.FLAG_TESLA_LONG_CONTROL | Panda.FLAG_TESLA_POWERTRAIN)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
367
panda/tests/safety/test_toyota.py
Normal file
367
panda/tests/safety/test_toyota.py
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import numpy as np
|
||||||
|
import random
|
||||||
|
import unittest
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
|
||||||
|
TOYOTA_COMMON_TX_MSGS = [[0x2E4, 0], [0x191, 0], [0x412, 0], [0x343, 0], [0x1D2, 0]] # LKAS + LTA + ACC & PCM cancel cmds
|
||||||
|
TOYOTA_COMMON_LONG_TX_MSGS = [[0x283, 0], [0x2E6, 0], [0x2E7, 0], [0x33E, 0], [0x344, 0], [0x365, 0], [0x366, 0], [0x4CB, 0], # DSU bus 0
|
||||||
|
[0x128, 1], [0x141, 1], [0x160, 1], [0x161, 1], [0x470, 1], # DSU bus 1
|
||||||
|
[0x411, 0], # PCS_HUD
|
||||||
|
[0x750, 0]] # radar diagnostic address
|
||||||
|
GAS_INTERCEPTOR_TX_MSGS = [[0x200, 0]]
|
||||||
|
|
||||||
|
|
||||||
|
class TestToyotaSafetyBase(common.PandaCarSafetyTest, common.LongitudinalAccelSafetyTest):
|
||||||
|
|
||||||
|
TX_MSGS = TOYOTA_COMMON_TX_MSGS + TOYOTA_COMMON_LONG_TX_MSGS
|
||||||
|
STANDSTILL_THRESHOLD = 0 # kph
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x2E4, 0x343)}
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x2E4, 0x412, 0x191, 0x343]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
EPS_SCALE = 73
|
||||||
|
|
||||||
|
packer: CANPackerPanda
|
||||||
|
safety: libpanda_py.Panda
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls.__name__.endswith("Base"):
|
||||||
|
cls.packer = None
|
||||||
|
cls.safety = None
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
def _torque_meas_msg(self, torque: int, driver_torque: int | None = None):
|
||||||
|
values = {"STEER_TORQUE_EPS": (torque / self.EPS_SCALE) * 100.}
|
||||||
|
if driver_torque is not None:
|
||||||
|
values["STEER_TORQUE_DRIVER"] = driver_torque
|
||||||
|
return self.packer.make_can_msg_panda("STEER_TORQUE_SENSOR", 0, values)
|
||||||
|
|
||||||
|
# Both torque and angle safety modes test with each other's steering commands
|
||||||
|
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||||
|
values = {"STEER_TORQUE_CMD": torque, "STEER_REQUEST": steer_req}
|
||||||
|
return self.packer.make_can_msg_panda("STEERING_LKA", 0, values)
|
||||||
|
|
||||||
|
def _angle_meas_msg(self, angle: float, steer_angle_initializing: bool = False):
|
||||||
|
# This creates a steering torque angle message. Not set on all platforms,
|
||||||
|
# relative to init angle on some older TSS2 platforms. Only to be used with LTA
|
||||||
|
values = {"STEER_ANGLE": angle, "STEER_ANGLE_INITIALIZING": int(steer_angle_initializing)}
|
||||||
|
return self.packer.make_can_msg_panda("STEER_TORQUE_SENSOR", 0, values)
|
||||||
|
|
||||||
|
def _angle_cmd_msg(self, angle: float, enabled: bool):
|
||||||
|
return self._lta_msg(int(enabled), int(enabled), angle, torque_wind_down=100 if enabled else 0)
|
||||||
|
|
||||||
|
def _lta_msg(self, req, req2, angle_cmd, torque_wind_down=100):
|
||||||
|
values = {"STEER_REQUEST": req, "STEER_REQUEST_2": req2, "STEER_ANGLE_CMD": angle_cmd, "TORQUE_WIND_DOWN": torque_wind_down}
|
||||||
|
return self.packer.make_can_msg_panda("STEERING_LTA", 0, values)
|
||||||
|
|
||||||
|
def _accel_msg(self, accel, cancel_req=0):
|
||||||
|
values = {"ACCEL_CMD": accel, "CANCEL_REQ": cancel_req}
|
||||||
|
return self.packer.make_can_msg_panda("ACC_CONTROL", 0, values)
|
||||||
|
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {("WHEEL_SPEED_%s" % n): speed * 3.6 for n in ["FR", "FL", "RR", "RL"]}
|
||||||
|
return self.packer.make_can_msg_panda("WHEEL_SPEEDS", 0, values)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"BRAKE_PRESSED": brake}
|
||||||
|
return self.packer.make_can_msg_panda("BRAKE_MODULE", 0, values)
|
||||||
|
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
cruise_active = self.safety.get_controls_allowed()
|
||||||
|
values = {"GAS_RELEASED": not gas, "CRUISE_ACTIVE": cruise_active}
|
||||||
|
return self.packer.make_can_msg_panda("PCM_CRUISE", 0, values)
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
values = {"CRUISE_ACTIVE": enable}
|
||||||
|
return self.packer.make_can_msg_panda("PCM_CRUISE", 0, values)
|
||||||
|
|
||||||
|
def test_diagnostics(self, stock_longitudinal: bool = False):
|
||||||
|
for should_tx, msg in ((False, b"\x6D\x02\x3E\x00\x00\x00\x00\x00"), # fwdCamera tester present
|
||||||
|
(False, b"\x0F\x03\xAA\xAA\x00\x00\x00\x00"), # non-tester present
|
||||||
|
(True, b"\x0F\x02\x3E\x00\x00\x00\x00\x00")):
|
||||||
|
tester_present = libpanda_py.make_CANPacket(0x750, 0, msg)
|
||||||
|
self.assertEqual(should_tx and not stock_longitudinal, self._tx(tester_present))
|
||||||
|
|
||||||
|
def test_block_aeb(self, stock_longitudinal: bool = False):
|
||||||
|
for controls_allowed in (True, False):
|
||||||
|
for bad in (True, False):
|
||||||
|
for _ in range(10):
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
dat = [random.randint(1, 255) for _ in range(7)]
|
||||||
|
if not bad:
|
||||||
|
dat = [0]*6 + dat[-1:]
|
||||||
|
msg = libpanda_py.make_CANPacket(0x283, 0, bytes(dat))
|
||||||
|
self.assertEqual(not bad and not stock_longitudinal, self._tx(msg))
|
||||||
|
|
||||||
|
# Only allow LTA msgs with no actuation
|
||||||
|
def test_lta_steer_cmd(self):
|
||||||
|
for engaged, req, req2, torque_wind_down, angle in itertools.product([True, False],
|
||||||
|
[0, 1], [0, 1],
|
||||||
|
[0, 50, 100],
|
||||||
|
np.linspace(-20, 20, 5)):
|
||||||
|
self.safety.set_controls_allowed(engaged)
|
||||||
|
|
||||||
|
should_tx = not req and not req2 and angle == 0 and torque_wind_down == 0
|
||||||
|
self.assertEqual(should_tx, self._tx(self._lta_msg(req, req2, angle, torque_wind_down)))
|
||||||
|
|
||||||
|
def test_rx_hook(self):
|
||||||
|
# checksum checks
|
||||||
|
for msg in ["trq", "pcm"]:
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
if msg == "trq":
|
||||||
|
to_push = self._torque_meas_msg(0)
|
||||||
|
if msg == "pcm":
|
||||||
|
to_push = self._pcm_status_msg(True)
|
||||||
|
self.assertTrue(self._rx(to_push))
|
||||||
|
to_push[0].data[4] = 0
|
||||||
|
to_push[0].data[5] = 0
|
||||||
|
to_push[0].data[6] = 0
|
||||||
|
to_push[0].data[7] = 0
|
||||||
|
self.assertFalse(self._rx(to_push))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
|
||||||
|
class TestToyotaSafetyGasInterceptorBase(common.GasInterceptorSafetyTest, TestToyotaSafetyBase):
|
||||||
|
|
||||||
|
TX_MSGS = TOYOTA_COMMON_TX_MSGS + TOYOTA_COMMON_LONG_TX_MSGS + GAS_INTERCEPTOR_TX_MSGS
|
||||||
|
INTERCEPTOR_THRESHOLD = 805
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_TOYOTA, self.safety.get_current_safety_param() |
|
||||||
|
Panda.FLAG_TOYOTA_GAS_INTERCEPTOR)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def test_stock_longitudinal(self):
|
||||||
|
# If stock longitudinal is set, the gas interceptor safety param should not be respected
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_TOYOTA, self.safety.get_current_safety_param() |
|
||||||
|
Panda.FLAG_TOYOTA_STOCK_LONGITUDINAL)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
# Spot check a few gas interceptor tests: (1) reading interceptor,
|
||||||
|
# (2) behavior around interceptor, and (3) txing interceptor msgs
|
||||||
|
for test in (self.test_prev_gas_interceptor, self.test_disengage_on_gas_interceptor,
|
||||||
|
self.test_gas_interceptor_safety_check):
|
||||||
|
with self.subTest(test=test.__name__):
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
test()
|
||||||
|
|
||||||
|
|
||||||
|
class TestToyotaSafetyTorque(TestToyotaSafetyBase, common.MotorTorqueSteeringSafetyTest, common.SteerRequestCutSafetyTest):
|
||||||
|
|
||||||
|
MAX_RATE_UP = 15
|
||||||
|
MAX_RATE_DOWN = 25
|
||||||
|
MAX_TORQUE = 1500
|
||||||
|
MAX_RT_DELTA = 450
|
||||||
|
RT_INTERVAL = 250000
|
||||||
|
MAX_TORQUE_ERROR = 350
|
||||||
|
TORQUE_MEAS_TOLERANCE = 1 # toyota safety adds one to be conservative for rounding
|
||||||
|
|
||||||
|
# Safety around steering req bit
|
||||||
|
MIN_VALID_STEERING_FRAMES = 18
|
||||||
|
MAX_INVALID_STEERING_FRAMES = 1
|
||||||
|
MIN_VALID_STEERING_RT_INTERVAL = 170000 # a ~10% buffer, can send steer up to 110Hz
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("toyota_nodsu_pt_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_TOYOTA, self.EPS_SCALE)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestToyotaSafetyTorqueGasInterceptor(TestToyotaSafetyGasInterceptorBase, TestToyotaSafetyTorque):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestToyotaSafetyAngle(TestToyotaSafetyBase, common.AngleSteeringSafetyTest):
|
||||||
|
|
||||||
|
# Angle control limits
|
||||||
|
DEG_TO_CAN = 17.452007 # 1 / 0.0573 deg to can
|
||||||
|
|
||||||
|
ANGLE_RATE_BP = [5., 25., 25.]
|
||||||
|
ANGLE_RATE_UP = [0.3, 0.15, 0.15] # windup limit
|
||||||
|
ANGLE_RATE_DOWN = [0.36, 0.26, 0.26] # unwind limit
|
||||||
|
|
||||||
|
MAX_LTA_ANGLE = 94.9461 # PCS faults if commanding above this, deg
|
||||||
|
MAX_MEAS_TORQUE = 1500 # max allowed measured EPS torque before wind down
|
||||||
|
MAX_LTA_DRIVER_TORQUE = 150 # max allowed driver torque before wind down
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("toyota_nodsu_pt_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_TOYOTA, self.EPS_SCALE | Panda.FLAG_TOYOTA_LTA)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
# Only allow LKA msgs with no actuation
|
||||||
|
def test_lka_steer_cmd(self):
|
||||||
|
for engaged, steer_req, torque in itertools.product([True, False],
|
||||||
|
[0, 1],
|
||||||
|
np.linspace(-1500, 1500, 7)):
|
||||||
|
self.safety.set_controls_allowed(engaged)
|
||||||
|
torque = int(torque)
|
||||||
|
self.safety.set_rt_torque_last(torque)
|
||||||
|
self.safety.set_torque_meas(torque, torque)
|
||||||
|
self.safety.set_desired_torque_last(torque)
|
||||||
|
|
||||||
|
should_tx = not steer_req and torque == 0
|
||||||
|
self.assertEqual(should_tx, self._tx(self._torque_cmd_msg(torque, steer_req)))
|
||||||
|
|
||||||
|
def test_lta_steer_cmd(self):
|
||||||
|
"""
|
||||||
|
Tests the LTA steering command message
|
||||||
|
controls_allowed:
|
||||||
|
* STEER_REQUEST and STEER_REQUEST_2 do not mismatch
|
||||||
|
* TORQUE_WIND_DOWN is only set to 0 or 100 when STEER_REQUEST and STEER_REQUEST_2 are both 1
|
||||||
|
* Full torque messages are blocked if either EPS torque or driver torque is above the threshold
|
||||||
|
|
||||||
|
not controls_allowed:
|
||||||
|
* STEER_REQUEST, STEER_REQUEST_2, and TORQUE_WIND_DOWN are all 0
|
||||||
|
"""
|
||||||
|
for controls_allowed in (True, False):
|
||||||
|
for angle in np.arange(-90, 90, 1):
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
self._reset_angle_measurement(angle)
|
||||||
|
self._set_prev_desired_angle(angle)
|
||||||
|
|
||||||
|
self.assertTrue(self._tx(self._lta_msg(0, 0, angle, 0)))
|
||||||
|
if controls_allowed:
|
||||||
|
# Test the two steer request bits and TORQUE_WIND_DOWN torque wind down signal
|
||||||
|
for req, req2, torque_wind_down in itertools.product([0, 1], [0, 1], [0, 50, 100]):
|
||||||
|
mismatch = not (req or req2) and torque_wind_down != 0
|
||||||
|
should_tx = req == req2 and (torque_wind_down in (0, 100)) and not mismatch
|
||||||
|
self.assertEqual(should_tx, self._tx(self._lta_msg(req, req2, angle, torque_wind_down)))
|
||||||
|
|
||||||
|
# Test max EPS torque and driver override thresholds
|
||||||
|
cases = itertools.product(
|
||||||
|
(0, self.MAX_MEAS_TORQUE - 1, self.MAX_MEAS_TORQUE, self.MAX_MEAS_TORQUE + 1, self.MAX_MEAS_TORQUE * 2),
|
||||||
|
(0, self.MAX_LTA_DRIVER_TORQUE - 1, self.MAX_LTA_DRIVER_TORQUE, self.MAX_LTA_DRIVER_TORQUE + 1, self.MAX_LTA_DRIVER_TORQUE * 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
for eps_torque, driver_torque in cases:
|
||||||
|
for sign in (-1, 1):
|
||||||
|
for _ in range(6):
|
||||||
|
self._rx(self._torque_meas_msg(sign * eps_torque, sign * driver_torque))
|
||||||
|
|
||||||
|
# Toyota adds 1 to EPS torque since it is rounded after EPS factor
|
||||||
|
should_tx = (eps_torque - 1) <= self.MAX_MEAS_TORQUE and driver_torque <= self.MAX_LTA_DRIVER_TORQUE
|
||||||
|
self.assertEqual(should_tx, self._tx(self._lta_msg(1, 1, angle, 100)))
|
||||||
|
self.assertTrue(self._tx(self._lta_msg(1, 1, angle, 0))) # should tx if we wind down torque
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Controls not allowed
|
||||||
|
for req, req2, torque_wind_down in itertools.product([0, 1], [0, 1], [0, 50, 100]):
|
||||||
|
should_tx = not (req or req2) and torque_wind_down == 0
|
||||||
|
self.assertEqual(should_tx, self._tx(self._lta_msg(req, req2, angle, torque_wind_down)))
|
||||||
|
|
||||||
|
def test_steering_angle_measurements(self, max_angle=None):
|
||||||
|
# Measurement test tests max angle + 0.5 which will fail
|
||||||
|
super().test_steering_angle_measurements(max_angle=self.MAX_LTA_ANGLE - 0.5)
|
||||||
|
|
||||||
|
def test_angle_cmd_when_enabled(self, max_angle=None):
|
||||||
|
super().test_angle_cmd_when_enabled(max_angle=self.MAX_LTA_ANGLE)
|
||||||
|
|
||||||
|
def test_angle_measurements(self):
|
||||||
|
"""
|
||||||
|
* Tests angle meas quality flag dictates whether angle measurement is parsed, and if rx is valid
|
||||||
|
* Tests rx hook correctly clips the angle measurement, since it is to be compared to LTA cmd when inactive
|
||||||
|
"""
|
||||||
|
for steer_angle_initializing in (True, False):
|
||||||
|
for angle in np.arange(0, self.MAX_LTA_ANGLE * 2, 1):
|
||||||
|
# If init flag is set, do not rx or parse any angle measurements
|
||||||
|
for a in (angle, -angle, 0, 0, 0, 0):
|
||||||
|
self.assertEqual(not steer_angle_initializing,
|
||||||
|
self._rx(self._angle_meas_msg(a, steer_angle_initializing)))
|
||||||
|
|
||||||
|
final_angle = (0 if steer_angle_initializing else
|
||||||
|
round(min(angle, self.MAX_LTA_ANGLE) * self.DEG_TO_CAN))
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_min(), -final_angle)
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_max(), final_angle)
|
||||||
|
|
||||||
|
self._rx(self._angle_meas_msg(0))
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_min(), -final_angle)
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_max(), 0)
|
||||||
|
|
||||||
|
self._rx(self._angle_meas_msg(0))
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_min(), 0)
|
||||||
|
self.assertEqual(self.safety.get_angle_meas_max(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestToyotaSafetyAngleGasInterceptor(TestToyotaSafetyGasInterceptorBase, TestToyotaSafetyAngle):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestToyotaAltBrakeSafety(TestToyotaSafetyTorque):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("toyota_new_mc_pt_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_TOYOTA, self.EPS_SCALE | Panda.FLAG_TOYOTA_ALT_BRAKE)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
values = {"BRAKE_PRESSED": brake}
|
||||||
|
return self.packer.make_can_msg_panda("BRAKE_MODULE", 0, values)
|
||||||
|
|
||||||
|
# No LTA message in the DBC
|
||||||
|
def test_lta_steer_cmd(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestToyotaAltBrakeSafetyGasInterceptor(TestToyotaSafetyGasInterceptorBase, TestToyotaAltBrakeSafety):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestToyotaStockLongitudinalBase(TestToyotaSafetyBase):
|
||||||
|
|
||||||
|
TX_MSGS = TOYOTA_COMMON_TX_MSGS
|
||||||
|
# Base addresses minus ACC_CONTROL (0x343)
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (0x2E4,)}
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [0x2E4, 0x412, 0x191]}
|
||||||
|
|
||||||
|
def test_diagnostics(self, stock_longitudinal: bool = True):
|
||||||
|
super().test_diagnostics(stock_longitudinal=stock_longitudinal)
|
||||||
|
|
||||||
|
def test_block_aeb(self, stock_longitudinal: bool = True):
|
||||||
|
super().test_block_aeb(stock_longitudinal=stock_longitudinal)
|
||||||
|
|
||||||
|
def test_accel_actuation_limits(self, stock_longitudinal=True):
|
||||||
|
super().test_accel_actuation_limits(stock_longitudinal=stock_longitudinal)
|
||||||
|
|
||||||
|
def test_acc_cancel(self):
|
||||||
|
"""
|
||||||
|
Regardless of controls allowed, never allow ACC_CONTROL if cancel bit isn't set
|
||||||
|
"""
|
||||||
|
for controls_allowed in [True, False]:
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
for accel in np.arange(self.MIN_ACCEL - 1, self.MAX_ACCEL + 1, 0.1):
|
||||||
|
self.assertFalse(self._tx(self._accel_msg(accel)))
|
||||||
|
should_tx = np.isclose(accel, 0, atol=0.0001)
|
||||||
|
self.assertEqual(should_tx, self._tx(self._accel_msg(accel, cancel_req=1)))
|
||||||
|
|
||||||
|
|
||||||
|
class TestToyotaStockLongitudinalTorque(TestToyotaStockLongitudinalBase, TestToyotaSafetyTorque):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("toyota_nodsu_pt_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_TOYOTA, self.EPS_SCALE | Panda.FLAG_TOYOTA_STOCK_LONGITUDINAL)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
class TestToyotaStockLongitudinalAngle(TestToyotaStockLongitudinalBase, TestToyotaSafetyAngle):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("toyota_nodsu_pt_generated")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_TOYOTA, self.EPS_SCALE | Panda.FLAG_TOYOTA_STOCK_LONGITUDINAL | Panda.FLAG_TOYOTA_LTA)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
225
panda/tests/safety/test_volkswagen_mqb.py
Normal file
225
panda/tests/safety/test_volkswagen_mqb.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
import numpy as np
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
|
||||||
|
MAX_ACCEL = 2.0
|
||||||
|
MIN_ACCEL = -3.5
|
||||||
|
|
||||||
|
MSG_ESP_19 = 0xB2 # RX from ABS, for wheel speeds
|
||||||
|
MSG_LH_EPS_03 = 0x9F # RX from EPS, for driver steering torque
|
||||||
|
MSG_ESP_05 = 0x106 # RX from ABS, for brake light state
|
||||||
|
MSG_TSK_06 = 0x120 # RX from ECU, for ACC status from drivetrain coordinator
|
||||||
|
MSG_MOTOR_20 = 0x121 # RX from ECU, for driver throttle input
|
||||||
|
MSG_ACC_06 = 0x122 # TX by OP, ACC control instructions to the drivetrain coordinator
|
||||||
|
MSG_HCA_01 = 0x126 # TX by OP, Heading Control Assist steering torque
|
||||||
|
MSG_GRA_ACC_01 = 0x12B # TX by OP, ACC control buttons for cancel/resume
|
||||||
|
MSG_ACC_07 = 0x12E # TX by OP, ACC control instructions to the drivetrain coordinator
|
||||||
|
MSG_ACC_02 = 0x30C # TX by OP, ACC HUD data to the instrument cluster
|
||||||
|
MSG_LDW_02 = 0x397 # TX by OP, Lane line recognition and text alerts
|
||||||
|
|
||||||
|
|
||||||
|
class TestVolkswagenMqbSafety(common.PandaCarSafetyTest, common.DriverTorqueSteeringSafetyTest):
|
||||||
|
STANDSTILL_THRESHOLD = 0
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (MSG_HCA_01,)}
|
||||||
|
|
||||||
|
MAX_RATE_UP = 4
|
||||||
|
MAX_RATE_DOWN = 10
|
||||||
|
MAX_TORQUE = 300
|
||||||
|
MAX_RT_DELTA = 75
|
||||||
|
RT_INTERVAL = 250000
|
||||||
|
|
||||||
|
DRIVER_TORQUE_ALLOWANCE = 80
|
||||||
|
DRIVER_TORQUE_FACTOR = 3
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls.__name__ == "TestVolkswagenMqbSafety":
|
||||||
|
cls.packer = None
|
||||||
|
cls.safety = None
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
# Wheel speeds _esp_19_msg
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {"ESP_%s_Radgeschw_02" % s: speed for s in ["HL", "HR", "VL", "VR"]}
|
||||||
|
return self.packer.make_can_msg_panda("ESP_19", 0, values)
|
||||||
|
|
||||||
|
# Driver brake pressure over threshold
|
||||||
|
def _esp_05_msg(self, brake):
|
||||||
|
values = {"ESP_Fahrer_bremst": brake}
|
||||||
|
return self.packer.make_can_msg_panda("ESP_05", 0, values)
|
||||||
|
|
||||||
|
# Brake pedal switch
|
||||||
|
def _motor_14_msg(self, brake):
|
||||||
|
values = {"MO_Fahrer_bremst": brake}
|
||||||
|
return self.packer.make_can_msg_panda("Motor_14", 0, values)
|
||||||
|
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
return self._motor_14_msg(brake)
|
||||||
|
|
||||||
|
# Driver throttle input
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"MO_Fahrpedalrohwert_01": gas}
|
||||||
|
return self.packer.make_can_msg_panda("Motor_20", 0, values)
|
||||||
|
|
||||||
|
# ACC engagement status
|
||||||
|
def _tsk_status_msg(self, enable, main_switch=True):
|
||||||
|
if main_switch:
|
||||||
|
tsk_status = 3 if enable else 2
|
||||||
|
else:
|
||||||
|
tsk_status = 0
|
||||||
|
values = {"TSK_Status": tsk_status}
|
||||||
|
return self.packer.make_can_msg_panda("TSK_06", 0, values)
|
||||||
|
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
return self._tsk_status_msg(enable)
|
||||||
|
|
||||||
|
# Driver steering input torque
|
||||||
|
def _torque_driver_msg(self, torque):
|
||||||
|
values = {"EPS_Lenkmoment": abs(torque), "EPS_VZ_Lenkmoment": torque < 0}
|
||||||
|
return self.packer.make_can_msg_panda("LH_EPS_03", 0, values)
|
||||||
|
|
||||||
|
# openpilot steering output torque
|
||||||
|
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||||
|
values = {"HCA_01_LM_Offset": abs(torque), "HCA_01_LM_OffSign": torque < 0, "HCA_01_Sendestatus": steer_req}
|
||||||
|
return self.packer.make_can_msg_panda("HCA_01", 0, values)
|
||||||
|
|
||||||
|
# Cruise control buttons
|
||||||
|
def _gra_acc_01_msg(self, cancel=0, resume=0, _set=0, bus=2):
|
||||||
|
values = {"GRA_Abbrechen": cancel, "GRA_Tip_Setzen": _set, "GRA_Tip_Wiederaufnahme": resume}
|
||||||
|
return self.packer.make_can_msg_panda("GRA_ACC_01", bus, values)
|
||||||
|
|
||||||
|
# Acceleration request to drivetrain coordinator
|
||||||
|
def _acc_06_msg(self, accel):
|
||||||
|
values = {"ACC_Sollbeschleunigung_02": accel}
|
||||||
|
return self.packer.make_can_msg_panda("ACC_06", 0, values)
|
||||||
|
|
||||||
|
# Acceleration request to drivetrain coordinator
|
||||||
|
def _acc_07_msg(self, accel, secondary_accel=3.02):
|
||||||
|
values = {"ACC_Sollbeschleunigung_02": accel, "ACC_Folgebeschl": secondary_accel}
|
||||||
|
return self.packer.make_can_msg_panda("ACC_07", 0, values)
|
||||||
|
|
||||||
|
# Verify brake_pressed is true if either the switch or pressure threshold signals are true
|
||||||
|
def test_redundant_brake_signals(self):
|
||||||
|
test_combinations = [(True, True, True), (True, True, False), (True, False, True), (False, False, False)]
|
||||||
|
for brake_pressed, motor_14_signal, esp_05_signal in test_combinations:
|
||||||
|
self._rx(self._motor_14_msg(False))
|
||||||
|
self._rx(self._esp_05_msg(False))
|
||||||
|
self.assertFalse(self.safety.get_brake_pressed_prev())
|
||||||
|
self._rx(self._motor_14_msg(motor_14_signal))
|
||||||
|
self._rx(self._esp_05_msg(esp_05_signal))
|
||||||
|
self.assertEqual(brake_pressed, self.safety.get_brake_pressed_prev(),
|
||||||
|
f"expected {brake_pressed=} with {motor_14_signal=} and {esp_05_signal=}")
|
||||||
|
|
||||||
|
def test_torque_measurements(self):
|
||||||
|
# TODO: make this test work with all cars
|
||||||
|
self._rx(self._torque_driver_msg(50))
|
||||||
|
self._rx(self._torque_driver_msg(-50))
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
|
||||||
|
self.assertEqual(-50, self.safety.get_torque_driver_min())
|
||||||
|
self.assertEqual(50, self.safety.get_torque_driver_max())
|
||||||
|
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
self.assertEqual(0, self.safety.get_torque_driver_max())
|
||||||
|
self.assertEqual(-50, self.safety.get_torque_driver_min())
|
||||||
|
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
self.assertEqual(0, self.safety.get_torque_driver_max())
|
||||||
|
self.assertEqual(0, self.safety.get_torque_driver_min())
|
||||||
|
|
||||||
|
|
||||||
|
class TestVolkswagenMqbStockSafety(TestVolkswagenMqbSafety):
|
||||||
|
TX_MSGS = [[MSG_HCA_01, 0], [MSG_LDW_02, 0], [MSG_LH_EPS_03, 2], [MSG_GRA_ACC_01, 0], [MSG_GRA_ACC_01, 2]]
|
||||||
|
FWD_BLACKLISTED_ADDRS = {0: [MSG_LH_EPS_03], 2: [MSG_HCA_01, MSG_LDW_02]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("vw_mqb_2010")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_VOLKSWAGEN_MQB, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def test_spam_cancel_safety_check(self):
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
self.assertTrue(self._tx(self._gra_acc_01_msg(cancel=1)))
|
||||||
|
self.assertFalse(self._tx(self._gra_acc_01_msg(resume=1)))
|
||||||
|
self.assertFalse(self._tx(self._gra_acc_01_msg(_set=1)))
|
||||||
|
# do not block resume if we are engaged already
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self.assertTrue(self._tx(self._gra_acc_01_msg(resume=1)))
|
||||||
|
|
||||||
|
|
||||||
|
class TestVolkswagenMqbLongSafety(TestVolkswagenMqbSafety):
|
||||||
|
TX_MSGS = [[MSG_HCA_01, 0], [MSG_LDW_02, 0], [MSG_LH_EPS_03, 2], [MSG_ACC_02, 0], [MSG_ACC_06, 0], [MSG_ACC_07, 0]]
|
||||||
|
FWD_BLACKLISTED_ADDRS = {0: [MSG_LH_EPS_03], 2: [MSG_HCA_01, MSG_LDW_02, MSG_ACC_02, MSG_ACC_06, MSG_ACC_07]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
INACTIVE_ACCEL = 3.01
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("vw_mqb_2010")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_VOLKSWAGEN_MQB, Panda.FLAG_VOLKSWAGEN_LONG_CONTROL)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
# stock cruise controls are entirely bypassed under openpilot longitudinal control
|
||||||
|
def test_disable_control_allowed_from_cruise(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_enable_control_allowed_from_cruise(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_cruise_engaged_prev(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_set_and_resume_buttons(self):
|
||||||
|
for button in ["set", "resume"]:
|
||||||
|
# ACC main switch must be on, engage on falling edge
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
self._rx(self._tsk_status_msg(False, main_switch=False))
|
||||||
|
self._rx(self._gra_acc_01_msg(_set=(button == "set"), resume=(button == "resume"), bus=0))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed(), f"controls allowed on {button} with main switch off")
|
||||||
|
self._rx(self._tsk_status_msg(False, main_switch=True))
|
||||||
|
self._rx(self._gra_acc_01_msg(_set=(button == "set"), resume=(button == "resume"), bus=0))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed(), f"controls allowed on {button} rising edge")
|
||||||
|
self._rx(self._gra_acc_01_msg(bus=0))
|
||||||
|
self.assertTrue(self.safety.get_controls_allowed(), f"controls not allowed on {button} falling edge")
|
||||||
|
|
||||||
|
def test_cancel_button(self):
|
||||||
|
# Disable on rising edge of cancel button
|
||||||
|
self._rx(self._tsk_status_msg(False, main_switch=True))
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._gra_acc_01_msg(cancel=True, bus=0))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed(), "controls allowed after cancel")
|
||||||
|
|
||||||
|
def test_main_switch(self):
|
||||||
|
# Disable as soon as main switch turns off
|
||||||
|
self._rx(self._tsk_status_msg(False, main_switch=True))
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._tsk_status_msg(False, main_switch=False))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed(), "controls allowed after ACC main switch off")
|
||||||
|
|
||||||
|
def test_accel_safety_check(self):
|
||||||
|
for controls_allowed in [True, False]:
|
||||||
|
# enforce we don't skip over 0 or inactive accel
|
||||||
|
for accel in np.concatenate((np.arange(MIN_ACCEL - 2, MAX_ACCEL + 2, 0.03), [0, self.INACTIVE_ACCEL])):
|
||||||
|
accel = round(accel, 2) # floats might not hit exact boundary conditions without rounding
|
||||||
|
is_inactive_accel = accel == self.INACTIVE_ACCEL
|
||||||
|
send = (controls_allowed and MIN_ACCEL <= accel <= MAX_ACCEL) or is_inactive_accel
|
||||||
|
self.safety.set_controls_allowed(controls_allowed)
|
||||||
|
# primary accel request used by ECU
|
||||||
|
self.assertEqual(send, self._tx(self._acc_06_msg(accel)), (controls_allowed, accel))
|
||||||
|
# additional accel request used by ABS/ESP
|
||||||
|
self.assertEqual(send, self._tx(self._acc_07_msg(accel)), (controls_allowed, accel))
|
||||||
|
# ensure the optional secondary accel field remains inactive for now
|
||||||
|
self.assertEqual(is_inactive_accel, self._tx(self._acc_07_msg(accel, secondary_accel=accel)), (controls_allowed, accel))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
199
panda/tests/safety/test_volkswagen_pq.py
Normal file
199
panda/tests/safety/test_volkswagen_pq.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
from panda import Panda
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
import panda.tests.safety.common as common
|
||||||
|
from panda.tests.safety.common import CANPackerPanda
|
||||||
|
|
||||||
|
MSG_LENKHILFE_3 = 0x0D0 # RX from EPS, for steering angle and driver steering torque
|
||||||
|
MSG_HCA_1 = 0x0D2 # TX by OP, Heading Control Assist steering torque
|
||||||
|
MSG_BREMSE_1 = 0x1A0 # RX from ABS, for ego speed
|
||||||
|
MSG_MOTOR_2 = 0x288 # RX from ECU, for CC state and brake switch state
|
||||||
|
MSG_ACC_SYSTEM = 0x368 # TX by OP, longitudinal acceleration controls
|
||||||
|
MSG_MOTOR_3 = 0x380 # RX from ECU, for driver throttle input
|
||||||
|
MSG_GRA_NEU = 0x38A # TX by OP, ACC control buttons for cancel/resume
|
||||||
|
MSG_MOTOR_5 = 0x480 # RX from ECU, for ACC main switch state
|
||||||
|
MSG_ACC_GRA_ANZEIGE = 0x56A # TX by OP, ACC HUD
|
||||||
|
MSG_LDW_1 = 0x5BE # TX by OP, Lane line recognition and text alerts
|
||||||
|
|
||||||
|
|
||||||
|
class TestVolkswagenPqSafety(common.PandaCarSafetyTest, common.DriverTorqueSteeringSafetyTest):
|
||||||
|
cruise_engaged = False
|
||||||
|
|
||||||
|
STANDSTILL_THRESHOLD = 0
|
||||||
|
RELAY_MALFUNCTION_ADDRS = {0: (MSG_HCA_1,)}
|
||||||
|
|
||||||
|
MAX_RATE_UP = 6
|
||||||
|
MAX_RATE_DOWN = 10
|
||||||
|
MAX_TORQUE = 300
|
||||||
|
MAX_RT_DELTA = 113
|
||||||
|
RT_INTERVAL = 250000
|
||||||
|
|
||||||
|
DRIVER_TORQUE_ALLOWANCE = 80
|
||||||
|
DRIVER_TORQUE_FACTOR = 3
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if cls.__name__ == "TestVolkswagenPqSafety":
|
||||||
|
cls.packer = None
|
||||||
|
cls.safety = None
|
||||||
|
raise unittest.SkipTest
|
||||||
|
|
||||||
|
def _set_prev_torque(self, t):
|
||||||
|
self.safety.set_desired_torque_last(t)
|
||||||
|
self.safety.set_rt_torque_last(t)
|
||||||
|
|
||||||
|
# Ego speed (Bremse_1)
|
||||||
|
def _speed_msg(self, speed):
|
||||||
|
values = {"Geschwindigkeit_neu__Bremse_1_": speed}
|
||||||
|
return self.packer.make_can_msg_panda("Bremse_1", 0, values)
|
||||||
|
|
||||||
|
# Brake light switch (shared message Motor_2)
|
||||||
|
def _user_brake_msg(self, brake):
|
||||||
|
# since this signal is used for engagement status, preserve current state
|
||||||
|
return self._motor_2_msg(brake_pressed=brake, cruise_engaged=self.safety.get_controls_allowed())
|
||||||
|
|
||||||
|
# ACC engaged status (shared message Motor_2)
|
||||||
|
def _pcm_status_msg(self, enable):
|
||||||
|
self.__class__.cruise_engaged = enable
|
||||||
|
return self._motor_2_msg(cruise_engaged=enable)
|
||||||
|
|
||||||
|
# Acceleration request to drivetrain coordinator
|
||||||
|
def _accel_msg(self, accel):
|
||||||
|
values = {"ACS_Sollbeschl": accel}
|
||||||
|
return self.packer.make_can_msg_panda("ACC_System", 0, values)
|
||||||
|
|
||||||
|
# Driver steering input torque
|
||||||
|
def _torque_driver_msg(self, torque):
|
||||||
|
values = {"LH3_LM": abs(torque), "LH3_LMSign": torque < 0}
|
||||||
|
return self.packer.make_can_msg_panda("Lenkhilfe_3", 0, values)
|
||||||
|
|
||||||
|
# openpilot steering output torque
|
||||||
|
def _torque_cmd_msg(self, torque, steer_req=1, hca_status=5):
|
||||||
|
values = {"LM_Offset": abs(torque), "LM_OffSign": torque < 0, "HCA_Status": hca_status if steer_req else 3}
|
||||||
|
return self.packer.make_can_msg_panda("HCA_1", 0, values)
|
||||||
|
|
||||||
|
# ACC engagement and brake light switch status
|
||||||
|
# Called indirectly for compatibility with common.py tests
|
||||||
|
def _motor_2_msg(self, brake_pressed=False, cruise_engaged=False):
|
||||||
|
values = {"Bremslichtschalter": brake_pressed,
|
||||||
|
"GRA_Status": cruise_engaged}
|
||||||
|
return self.packer.make_can_msg_panda("Motor_2", 0, values)
|
||||||
|
|
||||||
|
# ACC main switch status
|
||||||
|
def _motor_5_msg(self, main_switch=False):
|
||||||
|
values = {"GRA_Hauptschalter": main_switch}
|
||||||
|
return self.packer.make_can_msg_panda("Motor_5", 0, values)
|
||||||
|
|
||||||
|
# Driver throttle input (Motor_3)
|
||||||
|
def _user_gas_msg(self, gas):
|
||||||
|
values = {"Fahrpedal_Rohsignal": gas}
|
||||||
|
return self.packer.make_can_msg_panda("Motor_3", 0, values)
|
||||||
|
|
||||||
|
# Cruise control buttons (GRA_Neu)
|
||||||
|
def _button_msg(self, _set=False, resume=False, cancel=False, bus=2):
|
||||||
|
values = {"GRA_Neu_Setzen": _set, "GRA_Recall": resume, "GRA_Abbrechen": cancel}
|
||||||
|
return self.packer.make_can_msg_panda("GRA_Neu", bus, values)
|
||||||
|
|
||||||
|
def test_torque_measurements(self):
|
||||||
|
# TODO: make this test work with all cars
|
||||||
|
self._rx(self._torque_driver_msg(50))
|
||||||
|
self._rx(self._torque_driver_msg(-50))
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
|
||||||
|
self.assertEqual(-50, self.safety.get_torque_driver_min())
|
||||||
|
self.assertEqual(50, self.safety.get_torque_driver_max())
|
||||||
|
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
self.assertEqual(0, self.safety.get_torque_driver_max())
|
||||||
|
self.assertEqual(-50, self.safety.get_torque_driver_min())
|
||||||
|
|
||||||
|
self._rx(self._torque_driver_msg(0))
|
||||||
|
self.assertEqual(0, self.safety.get_torque_driver_max())
|
||||||
|
self.assertEqual(0, self.safety.get_torque_driver_min())
|
||||||
|
|
||||||
|
|
||||||
|
class TestVolkswagenPqStockSafety(TestVolkswagenPqSafety):
|
||||||
|
# Transmit of GRA_Neu is allowed on bus 0 and 2 to keep compatibility with gateway and camera integration
|
||||||
|
TX_MSGS = [[MSG_HCA_1, 0], [MSG_GRA_NEU, 0], [MSG_GRA_NEU, 2], [MSG_LDW_1, 0]]
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [MSG_HCA_1, MSG_LDW_1]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("vw_golf_mk4")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_VOLKSWAGEN_PQ, 0)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
def test_spam_cancel_safety_check(self):
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
self.assertTrue(self._tx(self._button_msg(cancel=True)))
|
||||||
|
self.assertFalse(self._tx(self._button_msg(resume=True)))
|
||||||
|
self.assertFalse(self._tx(self._button_msg(_set=True)))
|
||||||
|
# do not block resume if we are engaged already
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self.assertTrue(self._tx(self._button_msg(resume=True)))
|
||||||
|
|
||||||
|
|
||||||
|
class TestVolkswagenPqLongSafety(TestVolkswagenPqSafety, common.LongitudinalAccelSafetyTest):
|
||||||
|
TX_MSGS = [[MSG_HCA_1, 0], [MSG_LDW_1, 0], [MSG_ACC_SYSTEM, 0], [MSG_ACC_GRA_ANZEIGE, 0]]
|
||||||
|
FWD_BLACKLISTED_ADDRS = {2: [MSG_HCA_1, MSG_LDW_1, MSG_ACC_SYSTEM, MSG_ACC_GRA_ANZEIGE]}
|
||||||
|
FWD_BUS_LOOKUP = {0: 2, 2: 0}
|
||||||
|
INACTIVE_ACCEL = 3.01
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.packer = CANPackerPanda("vw_golf_mk4")
|
||||||
|
self.safety = libpanda_py.libpanda
|
||||||
|
self.safety.set_safety_hooks(Panda.SAFETY_VOLKSWAGEN_PQ, Panda.FLAG_VOLKSWAGEN_LONG_CONTROL)
|
||||||
|
self.safety.init_tests()
|
||||||
|
|
||||||
|
# stock cruise controls are entirely bypassed under openpilot longitudinal control
|
||||||
|
def test_disable_control_allowed_from_cruise(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_enable_control_allowed_from_cruise(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_cruise_engaged_prev(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_set_and_resume_buttons(self):
|
||||||
|
for button in ["set", "resume"]:
|
||||||
|
# ACC main switch must be on, engage on falling edge
|
||||||
|
self.safety.set_controls_allowed(0)
|
||||||
|
self._rx(self._motor_5_msg(main_switch=False))
|
||||||
|
self._rx(self._button_msg(_set=(button == "set"), resume=(button == "resume"), bus=0))
|
||||||
|
self._rx(self._button_msg(bus=0))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed(), f"controls allowed on {button} with main switch off")
|
||||||
|
self._rx(self._motor_5_msg(main_switch=True))
|
||||||
|
self._rx(self._button_msg(_set=(button == "set"), resume=(button == "resume"), bus=0))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed(), f"controls allowed on {button} rising edge")
|
||||||
|
self._rx(self._button_msg(bus=0))
|
||||||
|
self.assertTrue(self.safety.get_controls_allowed(), f"controls not allowed on {button} falling edge")
|
||||||
|
|
||||||
|
def test_cancel_button(self):
|
||||||
|
# Disable on rising edge of cancel button
|
||||||
|
self._rx(self._motor_5_msg(main_switch=True))
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._button_msg(cancel=True, bus=0))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed(), "controls allowed after cancel")
|
||||||
|
|
||||||
|
def test_main_switch(self):
|
||||||
|
# Disable as soon as main switch turns off
|
||||||
|
self._rx(self._motor_5_msg(main_switch=True))
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
self._rx(self._motor_5_msg(main_switch=False))
|
||||||
|
self.assertFalse(self.safety.get_controls_allowed(), "controls allowed after ACC main switch off")
|
||||||
|
|
||||||
|
def test_torque_cmd_enable_variants(self):
|
||||||
|
# The EPS rack accepts either 5 or 7 for an enabled status, with different low speed tuning behavior
|
||||||
|
self.safety.set_controls_allowed(1)
|
||||||
|
for enabled_status in (5, 7):
|
||||||
|
self.assertTrue(self._tx(self._torque_cmd_msg(self.MAX_RATE_UP, steer_req=1, hca_status=enabled_status)),
|
||||||
|
f"torque cmd rejected with {enabled_status=}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
1
panda/tests/safety_replay/.gitignore
vendored
Normal file
1
panda/tests/safety_replay/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.bz2
|
||||||
0
panda/tests/safety_replay/__init__.py
Normal file
0
panda/tests/safety_replay/__init__.py
Normal file
80
panda/tests/safety_replay/helpers.py
Normal file
80
panda/tests/safety_replay/helpers.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import panda.tests.libpanda.libpanda_py as libpanda_py
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
def to_signed(d, bits):
|
||||||
|
ret = d
|
||||||
|
if d >= (1 << (bits - 1)):
|
||||||
|
ret = d - (1 << bits)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def is_steering_msg(mode, param, addr):
|
||||||
|
ret = False
|
||||||
|
if mode in (Panda.SAFETY_HONDA_NIDEC, Panda.SAFETY_HONDA_BOSCH):
|
||||||
|
ret = (addr == 0xE4) or (addr == 0x194) or (addr == 0x33D) or (addr == 0x33DA) or (addr == 0x33DB)
|
||||||
|
elif mode == Panda.SAFETY_TOYOTA:
|
||||||
|
ret = addr == (0x191 if param & Panda.FLAG_TOYOTA_LTA else 0x2E4)
|
||||||
|
elif mode == Panda.SAFETY_GM:
|
||||||
|
ret = addr == 384
|
||||||
|
elif mode == Panda.SAFETY_HYUNDAI:
|
||||||
|
ret = addr == 832
|
||||||
|
elif mode == Panda.SAFETY_CHRYSLER:
|
||||||
|
ret = addr == 0x292
|
||||||
|
elif mode == Panda.SAFETY_SUBARU:
|
||||||
|
ret = addr == 0x122
|
||||||
|
elif mode == Panda.SAFETY_FORD:
|
||||||
|
ret = addr == 0x3d3
|
||||||
|
elif mode == Panda.SAFETY_NISSAN:
|
||||||
|
ret = addr == 0x169
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def get_steer_value(mode, param, to_send):
|
||||||
|
torque, angle = 0, 0
|
||||||
|
if mode in (Panda.SAFETY_HONDA_NIDEC, Panda.SAFETY_HONDA_BOSCH):
|
||||||
|
torque = (to_send.data[0] << 8) | to_send.data[1]
|
||||||
|
torque = to_signed(torque, 16)
|
||||||
|
elif mode == Panda.SAFETY_TOYOTA:
|
||||||
|
if param & Panda.FLAG_TOYOTA_LTA:
|
||||||
|
angle = (to_send.data[1] << 8) | to_send.data[2]
|
||||||
|
angle = to_signed(angle, 16)
|
||||||
|
else:
|
||||||
|
torque = (to_send.data[1] << 8) | (to_send.data[2])
|
||||||
|
torque = to_signed(torque, 16)
|
||||||
|
elif mode == Panda.SAFETY_GM:
|
||||||
|
torque = ((to_send.data[0] & 0x7) << 8) | to_send.data[1]
|
||||||
|
torque = to_signed(torque, 11)
|
||||||
|
elif mode == Panda.SAFETY_HYUNDAI:
|
||||||
|
torque = (((to_send.data[3] & 0x7) << 8) | to_send.data[2]) - 1024
|
||||||
|
elif mode == Panda.SAFETY_CHRYSLER:
|
||||||
|
torque = (((to_send.data[0] & 0x7) << 8) | to_send.data[1]) - 1024
|
||||||
|
elif mode == Panda.SAFETY_SUBARU:
|
||||||
|
torque = ((to_send.data[3] & 0x1F) << 8) | to_send.data[2]
|
||||||
|
torque = -to_signed(torque, 13)
|
||||||
|
elif mode == Panda.SAFETY_FORD:
|
||||||
|
angle = ((to_send.data[0] << 3) | (to_send.data[1] >> 5)) - 1000
|
||||||
|
elif mode == Panda.SAFETY_NISSAN:
|
||||||
|
angle = (to_send.data[0] << 10) | (to_send.data[1] << 2) | (to_send.data[2] >> 6)
|
||||||
|
angle = -angle + (1310 * 100)
|
||||||
|
return torque, angle
|
||||||
|
|
||||||
|
def package_can_msg(msg):
|
||||||
|
return libpanda_py.make_CANPacket(msg.address, msg.src % 4, msg.dat)
|
||||||
|
|
||||||
|
def init_segment(safety, lr, mode, param):
|
||||||
|
sendcan = (msg for msg in lr if msg.which() == 'sendcan')
|
||||||
|
steering_msgs = (can for msg in sendcan for can in msg.sendcan if is_steering_msg(mode, param, can.address))
|
||||||
|
|
||||||
|
msg = next(steering_msgs, None)
|
||||||
|
if msg is None:
|
||||||
|
# no steering msgs
|
||||||
|
return
|
||||||
|
|
||||||
|
to_send = package_can_msg(msg)
|
||||||
|
torque, angle = get_steer_value(mode, param, to_send)
|
||||||
|
if torque != 0:
|
||||||
|
safety.set_controls_allowed(1)
|
||||||
|
safety.set_desired_torque_last(torque)
|
||||||
|
elif angle != 0:
|
||||||
|
safety.set_controls_allowed(1)
|
||||||
|
safety.set_desired_angle_last(angle)
|
||||||
|
safety.set_angle_meas(angle, angle)
|
||||||
|
assert safety.safety_tx_hook(to_send), "failed to initialize panda safety for segment"
|
||||||
103
panda/tests/safety_replay/replay_drive.py
Normal file
103
panda/tests/safety_replay/replay_drive.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
from panda.tests.safety_replay.helpers import package_can_msg, init_segment
|
||||||
|
|
||||||
|
# replay a drive to check for safety violations
|
||||||
|
def replay_drive(lr, safety_mode, param, alternative_experience, segment=False):
|
||||||
|
safety = libpanda_py.libpanda
|
||||||
|
|
||||||
|
err = safety.set_safety_hooks(safety_mode, param)
|
||||||
|
assert err == 0, "invalid safety mode: %d" % safety_mode
|
||||||
|
safety.set_alternative_experience(alternative_experience)
|
||||||
|
|
||||||
|
if segment:
|
||||||
|
init_segment(safety, lr, safety_mode, param)
|
||||||
|
lr.reset()
|
||||||
|
|
||||||
|
rx_tot, rx_invalid, tx_tot, tx_blocked, tx_controls, tx_controls_blocked = 0, 0, 0, 0, 0, 0
|
||||||
|
safety_tick_rx_invalid = False
|
||||||
|
blocked_addrs = Counter()
|
||||||
|
invalid_addrs = set()
|
||||||
|
|
||||||
|
can_msgs = [m for m in lr if m.which() in ('can', 'sendcan')]
|
||||||
|
start_t = can_msgs[0].logMonoTime
|
||||||
|
end_t = can_msgs[-1].logMonoTime
|
||||||
|
for msg in can_msgs:
|
||||||
|
safety.set_timer((msg.logMonoTime // 1000) % 0xFFFFFFFF)
|
||||||
|
|
||||||
|
# skip start and end of route, warm up/down period
|
||||||
|
if msg.logMonoTime - start_t > 1e9 and end_t - msg.logMonoTime > 1e9:
|
||||||
|
safety.safety_tick_current_safety_config()
|
||||||
|
safety_tick_rx_invalid |= not safety.safety_config_valid() or safety_tick_rx_invalid
|
||||||
|
|
||||||
|
if msg.which() == 'sendcan':
|
||||||
|
for canmsg in msg.sendcan:
|
||||||
|
to_send = package_can_msg(canmsg)
|
||||||
|
sent = safety.safety_tx_hook(to_send)
|
||||||
|
if not sent:
|
||||||
|
tx_blocked += 1
|
||||||
|
tx_controls_blocked += safety.get_controls_allowed()
|
||||||
|
blocked_addrs[canmsg.address] += 1
|
||||||
|
|
||||||
|
if "DEBUG" in os.environ:
|
||||||
|
print("blocked bus %d msg %d at %f" % (canmsg.src, canmsg.address, (msg.logMonoTime - start_t) / 1e9))
|
||||||
|
tx_controls += safety.get_controls_allowed()
|
||||||
|
tx_tot += 1
|
||||||
|
elif msg.which() == 'can':
|
||||||
|
# ignore msgs we sent
|
||||||
|
for canmsg in filter(lambda m: m.src < 128, msg.can):
|
||||||
|
to_push = package_can_msg(canmsg)
|
||||||
|
recv = safety.safety_rx_hook(to_push)
|
||||||
|
if not recv:
|
||||||
|
rx_invalid += 1
|
||||||
|
invalid_addrs.add(canmsg.address)
|
||||||
|
rx_tot += 1
|
||||||
|
|
||||||
|
print("\nRX")
|
||||||
|
print("total rx msgs:", rx_tot)
|
||||||
|
print("invalid rx msgs:", rx_invalid)
|
||||||
|
print("safety tick rx invalid:", safety_tick_rx_invalid)
|
||||||
|
print("invalid addrs:", invalid_addrs)
|
||||||
|
print("\nTX")
|
||||||
|
print("total openpilot msgs:", tx_tot)
|
||||||
|
print("total msgs with controls allowed:", tx_controls)
|
||||||
|
print("blocked msgs:", tx_blocked)
|
||||||
|
print("blocked with controls allowed:", tx_controls_blocked)
|
||||||
|
print("blocked addrs:", blocked_addrs)
|
||||||
|
|
||||||
|
return tx_controls_blocked == 0 and rx_invalid == 0 and not safety_tick_rx_invalid
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from openpilot.tools.lib.logreader import LogReader
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Replay CAN messages from a route or segment through a safety mode",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("route_or_segment_name", nargs='+')
|
||||||
|
parser.add_argument("--mode", type=int, help="Override the safety mode from the log")
|
||||||
|
parser.add_argument("--param", type=int, help="Override the safety param from the log")
|
||||||
|
parser.add_argument("--alternative-experience", type=int, help="Override the alternative experience from the log")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
lr = LogReader(args.route_or_segment_name[0])
|
||||||
|
|
||||||
|
if None in (args.mode, args.param, args.alternative_experience):
|
||||||
|
for msg in lr:
|
||||||
|
if msg.which() == 'carParams':
|
||||||
|
if args.mode is None:
|
||||||
|
args.mode = msg.carParams.safetyConfigs[-1].safetyModel.raw
|
||||||
|
if args.param is None:
|
||||||
|
args.param = msg.carParams.safetyConfigs[-1].safetyParam
|
||||||
|
if args.alternative_experience is None:
|
||||||
|
args.alternative_experience = msg.carParams.alternativeExperience
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise Exception("carParams not found in log. Set safety mode and param manually.")
|
||||||
|
|
||||||
|
lr.reset()
|
||||||
|
|
||||||
|
print(f"replaying {args.route_or_segment_name[0]} with safety mode {args.mode}, param {args.param}, alternative experience {args.alternative_experience}")
|
||||||
|
replay_drive(lr, args.mode, args.param, args.alternative_experience, segment=len(lr.logreader_identifiers) == 1)
|
||||||
74
panda/tests/setup_device_ci.sh
Normal file
74
panda/tests/setup_device_ci.sh
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$SOURCE_DIR" ]; then
|
||||||
|
echo "SOURCE_DIR must be set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$GIT_COMMIT" ]; then
|
||||||
|
echo "GIT_COMMIT must be set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$TEST_DIR" ]; then
|
||||||
|
echo "TEST_DIR must be set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONTINUE_PATH="/data/continue.sh"
|
||||||
|
tee $CONTINUE_PATH << EOF
|
||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
sudo abctl --set_success
|
||||||
|
|
||||||
|
# patch sshd config
|
||||||
|
sudo mount -o rw,remount /
|
||||||
|
sudo sed -i "s,/data/params/d/GithubSshKeys,/usr/comma/setup_keys," /etc/ssh/sshd_config
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl restart ssh
|
||||||
|
sudo systemctl disable ssh-param-watcher.path
|
||||||
|
sudo systemctl disable ssh-param-watcher.service
|
||||||
|
sudo mount -o ro,remount /
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if ! sudo systemctl is-active -q ssh; then
|
||||||
|
sudo systemctl start ssh
|
||||||
|
fi
|
||||||
|
sleep 5s
|
||||||
|
done
|
||||||
|
|
||||||
|
sleep infinity
|
||||||
|
EOF
|
||||||
|
chmod +x $CONTINUE_PATH
|
||||||
|
|
||||||
|
|
||||||
|
# set up environment
|
||||||
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
|
git clone https://github.com/commaai/panda.git $SOURCE_DIR
|
||||||
|
fi
|
||||||
|
|
||||||
|
# setup device/SOM state
|
||||||
|
SOM_ST_IO=49
|
||||||
|
echo $SOM_ST_IO > /sys/class/gpio/export || true
|
||||||
|
echo out > /sys/class/gpio/gpio${SOM_ST_IO}/direction
|
||||||
|
echo 1 > /sys/class/gpio/gpio${SOM_ST_IO}/value
|
||||||
|
|
||||||
|
# checkout panda commit
|
||||||
|
cd $SOURCE_DIR
|
||||||
|
|
||||||
|
rm -f .git/index.lock
|
||||||
|
git reset --hard
|
||||||
|
git fetch --no-tags --no-recurse-submodules -j4 --verbose --depth 1 origin $GIT_COMMIT
|
||||||
|
find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \;
|
||||||
|
git reset --hard $GIT_COMMIT
|
||||||
|
git checkout $GIT_COMMIT
|
||||||
|
git clean -xdff
|
||||||
|
|
||||||
|
echo "git checkout done, t=$SECONDS"
|
||||||
|
du -hs $SOURCE_DIR $SOURCE_DIR/.git
|
||||||
|
|
||||||
|
rsync -a --delete $SOURCE_DIR $TEST_DIR
|
||||||
|
|
||||||
|
echo "$TEST_DIR synced with $GIT_COMMIT, t=$SECONDS"
|
||||||
24
panda/tests/som/on-device.py
Normal file
24
panda/tests/som/on-device.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
flag_set = False
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
with Panda(disable_checks=False) as p:
|
||||||
|
if not flag_set:
|
||||||
|
p.set_heartbeat_disabled()
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ELM327, 30)
|
||||||
|
flag_set = True
|
||||||
|
|
||||||
|
# shutdown when told
|
||||||
|
dt = p.get_datetime()
|
||||||
|
if dt.year == 2040 and dt.month == 8:
|
||||||
|
os.system("sudo poweroff")
|
||||||
|
except Exception as e:
|
||||||
|
print(str(e))
|
||||||
|
time.sleep(0.5)
|
||||||
154
panda/tests/som/test_bootkick.py
Normal file
154
panda/tests/som/test_bootkick.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from panda import Panda, PandaJungle
|
||||||
|
|
||||||
|
PANDA_SERIAL = "28002d000451323431333839"
|
||||||
|
JUNGLE_SERIAL = "26001c001451313236343430"
|
||||||
|
|
||||||
|
OBDC_PORT = 1
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True, scope="function")
|
||||||
|
def pj():
|
||||||
|
jungle = PandaJungle(JUNGLE_SERIAL)
|
||||||
|
jungle.flash()
|
||||||
|
|
||||||
|
jungle.reset()
|
||||||
|
jungle.set_ignition(False)
|
||||||
|
|
||||||
|
yield jungle
|
||||||
|
|
||||||
|
jungle.set_panda_power(False)
|
||||||
|
jungle.close()
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def p(pj):
|
||||||
|
# note that the 3X's panda lib isn't updated, which
|
||||||
|
# shold be fine since it only uses stable APIs
|
||||||
|
pj.set_panda_power(True)
|
||||||
|
assert Panda.wait_for_panda(PANDA_SERIAL, 10)
|
||||||
|
p = Panda(PANDA_SERIAL)
|
||||||
|
p.flash()
|
||||||
|
p.reset()
|
||||||
|
yield p
|
||||||
|
p.close()
|
||||||
|
|
||||||
|
def setup_state(panda, jungle, state):
|
||||||
|
jungle.set_panda_power(0)
|
||||||
|
|
||||||
|
if state == "off":
|
||||||
|
wait_for_full_poweroff(jungle)
|
||||||
|
elif state == "normal boot":
|
||||||
|
jungle.set_panda_individual_power(OBDC_PORT, 1)
|
||||||
|
elif state == "QDL":
|
||||||
|
time.sleep(0.5)
|
||||||
|
jungle.set_panda_individual_power(OBDC_PORT, 1)
|
||||||
|
elif state == "ready to bootkick":
|
||||||
|
wait_for_full_poweroff(jungle)
|
||||||
|
jungle.set_panda_individual_power(OBDC_PORT, 1)
|
||||||
|
wait_for_boot(panda, jungle)
|
||||||
|
set_som_shutdown_flag(panda)
|
||||||
|
panda.set_safety_mode(Panda.SAFETY_SILENT)
|
||||||
|
panda.send_heartbeat()
|
||||||
|
wait_for_som_shutdown(panda, jungle)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unkown state: {state}")
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_som_shutdown(panda, jungle):
|
||||||
|
st = time.monotonic()
|
||||||
|
while panda.read_som_gpio():
|
||||||
|
# can take a while for the SOM to fully shutdown
|
||||||
|
if time.monotonic() - st > 120:
|
||||||
|
raise Exception("SOM didn't shutdown in time")
|
||||||
|
if check_som_boot_flag(panda):
|
||||||
|
raise Exception(f"SOM rebooted instead of shutdown: {time.monotonic() - st}s")
|
||||||
|
time.sleep(0.5)
|
||||||
|
dt = time.monotonic() - st
|
||||||
|
print("waiting for shutdown", round(dt))
|
||||||
|
dt = time.monotonic() - st
|
||||||
|
print(f"took {dt:.2f}s for SOM to shutdown")
|
||||||
|
|
||||||
|
def wait_for_full_poweroff(jungle, timeout=30):
|
||||||
|
st = time.monotonic()
|
||||||
|
|
||||||
|
time.sleep(15)
|
||||||
|
while PANDA_SERIAL in Panda.list():
|
||||||
|
if time.monotonic() - st > timeout:
|
||||||
|
raise Exception("took too long for device to turn off")
|
||||||
|
|
||||||
|
health = jungle.health()
|
||||||
|
assert all(health[f"ch{i}_power"] < 0.1 for i in range(1, 7))
|
||||||
|
|
||||||
|
def check_som_boot_flag(panda):
|
||||||
|
h = panda.health()
|
||||||
|
return h['safety_mode'] == Panda.SAFETY_ELM327 and h['safety_param'] == 30
|
||||||
|
|
||||||
|
def set_som_shutdown_flag(panda):
|
||||||
|
panda.set_datetime(datetime.datetime(year=2040, month=8, day=23))
|
||||||
|
|
||||||
|
def wait_for_boot(panda, jungle, reset_expected=False, bootkick=False, timeout=120):
|
||||||
|
st = time.monotonic()
|
||||||
|
|
||||||
|
Panda.wait_for_panda(PANDA_SERIAL, timeout)
|
||||||
|
panda.reconnect()
|
||||||
|
if bootkick:
|
||||||
|
assert panda.health()['uptime'] > 20
|
||||||
|
else:
|
||||||
|
assert panda.health()['uptime'] < 3
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
assert not check_som_boot_flag(panda)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# wait for SOM to bootup
|
||||||
|
while not check_som_boot_flag(panda):
|
||||||
|
if time.monotonic() - st > timeout:
|
||||||
|
raise Exception("SOM didn't boot in time")
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
assert panda.health()['som_reset_triggered'] == reset_expected
|
||||||
|
|
||||||
|
def test_cold_boot(p, pj):
|
||||||
|
setup_state(p, pj, "off")
|
||||||
|
setup_state(p, pj, "normal boot")
|
||||||
|
wait_for_boot(p, pj)
|
||||||
|
|
||||||
|
def test_bootkick_ignition_line(p, pj):
|
||||||
|
setup_state(p, pj, "ready to bootkick")
|
||||||
|
pj.set_ignition(True)
|
||||||
|
wait_for_boot(p, pj, bootkick=True)
|
||||||
|
|
||||||
|
@pytest.mark.skip("test isn't reliable yet")
|
||||||
|
def test_bootkick_can_ignition(p, pj):
|
||||||
|
setup_state(p, pj, "ready to bootkick")
|
||||||
|
for _ in range(10):
|
||||||
|
# Mazda ignition signal
|
||||||
|
pj.can_send(0x9E, b'\xc0\x00\x00\x00\x00\x00\x00\x00', 0)
|
||||||
|
time.sleep(0.5)
|
||||||
|
wait_for_boot(p, pj, bootkick=True)
|
||||||
|
|
||||||
|
def test_recovery_from_qdl(p, pj):
|
||||||
|
setup_state(p, pj, "ready to bootkick")
|
||||||
|
|
||||||
|
# put into QDL using the FORCE_USB_BOOT pin
|
||||||
|
for i in range(10):
|
||||||
|
pj.set_header_pin(i, 1)
|
||||||
|
|
||||||
|
# try to boot
|
||||||
|
time.sleep(1)
|
||||||
|
pj.set_ignition(True)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# release FORCE_USB_BOOT
|
||||||
|
for i in range(10):
|
||||||
|
pj.set_header_pin(i, 0)
|
||||||
|
|
||||||
|
# normally, this GPIO is set immediately since it's first enabled in the ABL
|
||||||
|
for i in range(40):
|
||||||
|
assert not p.read_som_gpio()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# should boot after 45s
|
||||||
|
wait_for_boot(p, pj, reset_expected=True, bootkick=True, timeout=120)
|
||||||
7
panda/tests/som_debug.sh
Normal file
7
panda/tests/som_debug.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||||
|
cd $DIR
|
||||||
|
|
||||||
|
PYTHONUNBUFFERED=1 NO_COLOR=1 CLAIM=1 PORT=4 ./debug_console.py
|
||||||
20
panda/tests/spam_can.py
Normal file
20
panda/tests/spam_can.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
def get_test_string():
|
||||||
|
return b"test" + os.urandom(10)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = Panda()
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
print("Spamming all buses...")
|
||||||
|
while True:
|
||||||
|
at = random.randint(1, 2000)
|
||||||
|
st = get_test_string()[0:8]
|
||||||
|
bus = random.randint(0, 2)
|
||||||
|
p.can_send(at, st, bus)
|
||||||
|
# print("Sent message on bus: ", bus)
|
||||||
32
panda/tests/standalone_test.py
Normal file
32
panda/tests/standalone_test.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = Panda()
|
||||||
|
print(p.get_serial())
|
||||||
|
print(p.health())
|
||||||
|
|
||||||
|
t1 = time.time()
|
||||||
|
for _ in range(100):
|
||||||
|
p.get_serial()
|
||||||
|
t2 = time.time()
|
||||||
|
print("100 requests took %.2f ms" % ((t2 - t1) * 1000))
|
||||||
|
|
||||||
|
p.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
a = 0
|
||||||
|
while True:
|
||||||
|
# flood
|
||||||
|
msg = b"\xaa" * 4 + struct.pack("I", a)
|
||||||
|
p.can_send(0xaa, msg, 0)
|
||||||
|
p.can_send(0xaa, msg, 1)
|
||||||
|
p.can_send(0xaa, msg, 4)
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
dat = p.can_recv()
|
||||||
|
if len(dat) > 0:
|
||||||
|
print(dat)
|
||||||
|
a += 1
|
||||||
34
panda/tests/test_rsa.c
Normal file
34
panda/tests/test_rsa.c
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
gcc -DTEST_RSA test_rsa.c ../crypto/rsa.c ../crypto/sha.c && ./a.out
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#define MAX_LEN 0x40000
|
||||||
|
char buf[MAX_LEN];
|
||||||
|
|
||||||
|
#include "../crypto/sha.h"
|
||||||
|
#include "../crypto/rsa.h"
|
||||||
|
#include "../obj/cert.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
FILE *f = fopen("../obj/panda.bin", "rb");
|
||||||
|
int tlen = fread(buf, 1, MAX_LEN, f);
|
||||||
|
fclose(f);
|
||||||
|
printf("read %d\n", tlen);
|
||||||
|
uint32_t *_app_start = (uint32_t *)buf;
|
||||||
|
|
||||||
|
int len = _app_start[0];
|
||||||
|
char digest[SHA_DIGEST_SIZE];
|
||||||
|
SHA_hash(&_app_start[1], len-4, digest);
|
||||||
|
printf("SHA hash done\n");
|
||||||
|
|
||||||
|
if (!RSA_verify(&rsa_key, ((void*)&_app_start[0]) + len, RSANUMBYTES, digest, SHA_DIGEST_SIZE)) {
|
||||||
|
printf("RSA fail\n");
|
||||||
|
} else {
|
||||||
|
printf("RSA match!!!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
92
panda/tests/tucan_loopback.py
Normal file
92
panda/tests/tucan_loopback.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import argparse
|
||||||
|
from itertools import permutations
|
||||||
|
|
||||||
|
from panda import Panda
|
||||||
|
|
||||||
|
def get_test_string():
|
||||||
|
return b"test" + os.urandom(10)
|
||||||
|
|
||||||
|
def run_test(sleep_duration):
|
||||||
|
pandas = Panda.list()
|
||||||
|
print(pandas)
|
||||||
|
|
||||||
|
if len(pandas) < 2:
|
||||||
|
raise Exception("Two pandas are needed for test")
|
||||||
|
|
||||||
|
run_test_w_pandas(pandas, sleep_duration)
|
||||||
|
|
||||||
|
def run_test_w_pandas(pandas, sleep_duration):
|
||||||
|
h = [Panda(x) for x in pandas]
|
||||||
|
print("H", h)
|
||||||
|
|
||||||
|
for hh in h:
|
||||||
|
hh.set_safety_mode(Panda.SAFETY_ALLOUTPUT)
|
||||||
|
|
||||||
|
# test both directions
|
||||||
|
for ho in permutations(list(range(len(h))), r=2):
|
||||||
|
print("***************** TESTING", ho)
|
||||||
|
|
||||||
|
panda0, panda1 = h[ho[0]], h[ho[1]]
|
||||||
|
|
||||||
|
# **** test health packet ****
|
||||||
|
print("health", ho[0], h[ho[0]].health())
|
||||||
|
|
||||||
|
# **** test can line loopback ****
|
||||||
|
# for bus, gmlan in [(0, None), (1, False), (2, False), (1, True), (2, True)]:
|
||||||
|
for bus, gmlan in [(0, None), (1, None)]:
|
||||||
|
print("\ntest can", bus)
|
||||||
|
# flush
|
||||||
|
cans_echo = panda0.can_recv()
|
||||||
|
cans_loop = panda1.can_recv()
|
||||||
|
|
||||||
|
if gmlan is not None:
|
||||||
|
panda0.set_gmlan(gmlan, bus)
|
||||||
|
panda1.set_gmlan(gmlan, bus)
|
||||||
|
|
||||||
|
# send the characters
|
||||||
|
# pick addresses high enough to not conflict with honda code
|
||||||
|
at = random.randint(1024, 2000)
|
||||||
|
st = get_test_string()[0:8]
|
||||||
|
panda0.can_send(at, st, bus)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# check for receive
|
||||||
|
cans_echo = panda0.can_recv()
|
||||||
|
cans_loop = panda1.can_recv()
|
||||||
|
|
||||||
|
print("Bus", bus, "echo", cans_echo, "loop", cans_loop)
|
||||||
|
|
||||||
|
assert len(cans_echo) == 1
|
||||||
|
assert len(cans_loop) == 1
|
||||||
|
|
||||||
|
assert cans_echo[0][0] == at
|
||||||
|
assert cans_loop[0][0] == at
|
||||||
|
|
||||||
|
assert cans_echo[0][2] == st
|
||||||
|
assert cans_loop[0][2] == st
|
||||||
|
|
||||||
|
assert cans_echo[0][3] == 0x80 | bus
|
||||||
|
if cans_loop[0][3] != bus:
|
||||||
|
print("EXPECTED %d GOT %d" % (bus, cans_loop[0][3]))
|
||||||
|
assert cans_loop[0][3] == bus
|
||||||
|
|
||||||
|
print("CAN pass", bus, ho)
|
||||||
|
time.sleep(sleep_duration)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-n", type=int, help="Number of test iterations to run")
|
||||||
|
parser.add_argument("-sleep", type=int, help="Sleep time between tests", default=0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.n is None:
|
||||||
|
while True:
|
||||||
|
run_test(sleep_duration=args.sleep)
|
||||||
|
else:
|
||||||
|
for _ in range(args.n):
|
||||||
|
run_test(sleep_duration=args.sleep)
|
||||||
8
panda/tests/usbprotocol/test.sh
Normal file
8
panda/tests/usbprotocol/test.sh
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Loops over all HW_TYPEs, see board/boards/board_declarations.h
|
||||||
|
for hw_type in {0..7}; do
|
||||||
|
echo "Testing HW_TYPE: $hw_type"
|
||||||
|
HW_TYPE=$hw_type python -m unittest discover .
|
||||||
|
done
|
||||||
160
panda/tests/usbprotocol/test_comms.py
Normal file
160
panda/tests/usbprotocol/test_comms.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import random
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from panda import Panda, DLC_TO_LEN, USBPACKET_MAX_SIZE, pack_can_buffer, unpack_can_buffer
|
||||||
|
from panda.tests.libpanda import libpanda_py
|
||||||
|
|
||||||
|
lpp = libpanda_py.libpanda
|
||||||
|
|
||||||
|
CHUNK_SIZE = USBPACKET_MAX_SIZE
|
||||||
|
TX_QUEUES = (lpp.tx1_q, lpp.tx2_q, lpp.tx3_q, lpp.txgmlan_q)
|
||||||
|
|
||||||
|
|
||||||
|
def unpackage_can_msg(pkt):
|
||||||
|
dat_len = DLC_TO_LEN[pkt[0].data_len_code]
|
||||||
|
dat = bytes(pkt[0].data[0:dat_len])
|
||||||
|
return pkt[0].addr, 0, dat, pkt[0].bus
|
||||||
|
|
||||||
|
|
||||||
|
def random_can_messages(n, bus=None):
|
||||||
|
msgs = []
|
||||||
|
for _ in range(n):
|
||||||
|
if bus is None:
|
||||||
|
bus = random.randint(0, 3)
|
||||||
|
address = random.randint(1, (1 << 29) - 1)
|
||||||
|
data = bytes([random.getrandbits(8) for _ in range(DLC_TO_LEN[random.randrange(0, len(DLC_TO_LEN))])])
|
||||||
|
msgs.append((address, 0, data, bus))
|
||||||
|
return msgs
|
||||||
|
|
||||||
|
|
||||||
|
class TestPandaComms(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
lpp.comms_can_reset()
|
||||||
|
|
||||||
|
def test_tx_queues(self):
|
||||||
|
for bus in range(4):
|
||||||
|
message = (0x100, 0, b"test", bus)
|
||||||
|
|
||||||
|
can_pkt_tx = libpanda_py.make_CANPacket(message[0], message[3], message[2])
|
||||||
|
can_pkt_rx = libpanda_py.ffi.new('CANPacket_t *')
|
||||||
|
|
||||||
|
assert lpp.can_push(TX_QUEUES[bus], can_pkt_tx), "CAN push failed"
|
||||||
|
assert lpp.can_pop(TX_QUEUES[bus], can_pkt_rx), "CAN pop failed"
|
||||||
|
|
||||||
|
assert unpackage_can_msg(can_pkt_rx) == message
|
||||||
|
|
||||||
|
def test_comms_reset_rx(self):
|
||||||
|
# store some test messages in the queue
|
||||||
|
test_msg = (0x100, 0, b"test", 0)
|
||||||
|
for _ in range(100):
|
||||||
|
can_pkt_tx = libpanda_py.make_CANPacket(test_msg[0], test_msg[3], test_msg[2])
|
||||||
|
lpp.can_push(lpp.rx_q, can_pkt_tx)
|
||||||
|
|
||||||
|
# read a small chunk such that we have some overflow
|
||||||
|
TINY_CHUNK_SIZE = 6
|
||||||
|
dat = libpanda_py.ffi.new(f"uint8_t[{TINY_CHUNK_SIZE}]")
|
||||||
|
rx_len = lpp.comms_can_read(dat, TINY_CHUNK_SIZE)
|
||||||
|
assert rx_len == TINY_CHUNK_SIZE, "comms_can_read returned too little data"
|
||||||
|
|
||||||
|
_, overflow = unpack_can_buffer(bytes(dat))
|
||||||
|
assert len(overflow) > 0, "overflow buffer should not be empty"
|
||||||
|
|
||||||
|
# reset the comms to clear the overflow buffer on the panda side
|
||||||
|
lpp.comms_can_reset()
|
||||||
|
|
||||||
|
# read a large chunk, which should now contain valid messages
|
||||||
|
LARGE_CHUNK_SIZE = 512
|
||||||
|
dat = libpanda_py.ffi.new(f"uint8_t[{LARGE_CHUNK_SIZE}]")
|
||||||
|
rx_len = lpp.comms_can_read(dat, LARGE_CHUNK_SIZE)
|
||||||
|
assert rx_len == LARGE_CHUNK_SIZE, "comms_can_read returned too little data"
|
||||||
|
|
||||||
|
msgs, _ = unpack_can_buffer(bytes(dat))
|
||||||
|
assert len(msgs) > 0, "message buffer should not be empty"
|
||||||
|
for m in msgs:
|
||||||
|
assert m == test_msg, "message buffer should contain valid test messages"
|
||||||
|
|
||||||
|
def test_comms_reset_tx(self):
|
||||||
|
# store some test messages in the queue
|
||||||
|
test_msg = (0x100, 0, b"test", 0)
|
||||||
|
packed = pack_can_buffer([test_msg for _ in range(100)])
|
||||||
|
|
||||||
|
# write a small chunk such that we have some overflow
|
||||||
|
TINY_CHUNK_SIZE = 6
|
||||||
|
lpp.comms_can_write(packed[0][:TINY_CHUNK_SIZE], TINY_CHUNK_SIZE)
|
||||||
|
|
||||||
|
# reset the comms to clear the overflow buffer on the panda side
|
||||||
|
lpp.comms_can_reset()
|
||||||
|
|
||||||
|
# write a full valid chunk, which should now contain valid messages
|
||||||
|
lpp.comms_can_write(packed[1], len(packed[1]))
|
||||||
|
|
||||||
|
# read the messages from the queue and make sure they're valid
|
||||||
|
queue_msgs = []
|
||||||
|
pkt = libpanda_py.ffi.new('CANPacket_t *')
|
||||||
|
while lpp.can_pop(TX_QUEUES[0], pkt):
|
||||||
|
queue_msgs.append(unpackage_can_msg(pkt))
|
||||||
|
|
||||||
|
assert len(queue_msgs) > 0, "message buffer should not be empty"
|
||||||
|
for m in queue_msgs:
|
||||||
|
assert m == test_msg, "message buffer should contain valid test messages"
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_send_usb(self):
|
||||||
|
lpp.set_safety_hooks(Panda.SAFETY_ALLOUTPUT, 0)
|
||||||
|
|
||||||
|
for bus in range(3):
|
||||||
|
with self.subTest(bus=bus):
|
||||||
|
for _ in range(100):
|
||||||
|
msgs = random_can_messages(200, bus=bus)
|
||||||
|
packed = pack_can_buffer(msgs)
|
||||||
|
|
||||||
|
# Simulate USB bulk chunks
|
||||||
|
for buf in packed:
|
||||||
|
for i in range(0, len(buf), CHUNK_SIZE):
|
||||||
|
chunk_len = min(CHUNK_SIZE, len(buf) - i)
|
||||||
|
lpp.comms_can_write(buf[i:i+chunk_len], chunk_len)
|
||||||
|
|
||||||
|
# Check that they ended up in the right buffers
|
||||||
|
queue_msgs = []
|
||||||
|
pkt = libpanda_py.ffi.new('CANPacket_t *')
|
||||||
|
while lpp.can_pop(TX_QUEUES[bus], pkt):
|
||||||
|
queue_msgs.append(unpackage_can_msg(pkt))
|
||||||
|
|
||||||
|
self.assertEqual(len(queue_msgs), len(msgs))
|
||||||
|
self.assertEqual(queue_msgs, msgs)
|
||||||
|
|
||||||
|
def test_can_receive_usb(self):
|
||||||
|
msgs = random_can_messages(50000)
|
||||||
|
packets = [libpanda_py.make_CANPacket(m[0], m[3], m[2]) for m in msgs]
|
||||||
|
|
||||||
|
rx_msgs = []
|
||||||
|
overflow_buf = b""
|
||||||
|
while len(packets) > 0:
|
||||||
|
# Push into queue
|
||||||
|
while lpp.can_slots_empty(lpp.rx_q) > 0 and len(packets) > 0:
|
||||||
|
lpp.can_push(lpp.rx_q, packets.pop(0))
|
||||||
|
|
||||||
|
# Simulate USB bulk IN chunks
|
||||||
|
MAX_TRANSFER_SIZE = 16384
|
||||||
|
dat = libpanda_py.ffi.new(f"uint8_t[{CHUNK_SIZE}]")
|
||||||
|
while True:
|
||||||
|
buf = b""
|
||||||
|
while len(buf) < MAX_TRANSFER_SIZE:
|
||||||
|
max_size = min(CHUNK_SIZE, MAX_TRANSFER_SIZE - len(buf))
|
||||||
|
rx_len = lpp.comms_can_read(dat, max_size)
|
||||||
|
buf += bytes(dat[0:rx_len])
|
||||||
|
if rx_len < max_size:
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(buf) == 0:
|
||||||
|
break
|
||||||
|
unpacked_msgs, overflow_buf = unpack_can_buffer(overflow_buf + buf)
|
||||||
|
rx_msgs.extend(unpacked_msgs)
|
||||||
|
|
||||||
|
self.assertEqual(len(rx_msgs), len(msgs))
|
||||||
|
self.assertEqual(rx_msgs, msgs)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user