#!/usr/bin/env python3
import unittest
import numpy as np
from cereal import car , log
from common . realtime import DT_DMON
from selfdrive . controls . lib . events import Events
from selfdrive . monitoring . driver_monitor import DriverStatus , DRIVER_MONITOR_SETTINGS
EventName = car . CarEvent . EventName
dm_settings = DRIVER_MONITOR_SETTINGS ( )
TEST_TIMESPAN = 120 # seconds
DISTRACTED_SECONDS_TO_ORANGE = dm_settings . _DISTRACTED_TIME - dm_settings . _DISTRACTED_PROMPT_TIME_TILL_TERMINAL + 1
DISTRACTED_SECONDS_TO_RED = dm_settings . _DISTRACTED_TIME + 1
INVISIBLE_SECONDS_TO_ORANGE = dm_settings . _AWARENESS_TIME - dm_settings . _AWARENESS_PROMPT_TIME_TILL_TERMINAL + 1
INVISIBLE_SECONDS_TO_RED = dm_settings . _AWARENESS_TIME + 1
def make_msg ( face_detected , distracted = False , model_uncertain = False ) :
ds = log . DriverStateV2 . new_message ( )
ds . leftDriverData . faceOrientation = [ 0. , 0. , 0. ]
ds . leftDriverData . facePosition = [ 0. , 0. ]
ds . leftDriverData . faceProb = 1. * face_detected
ds . leftDriverData . leftEyeProb = 1.
ds . leftDriverData . rightEyeProb = 1.
ds . leftDriverData . leftBlinkProb = 1. * distracted
ds . leftDriverData . rightBlinkProb = 1. * distracted
ds . leftDriverData . faceOrientationStd = [ 1. * model_uncertain , 1. * model_uncertain , 1. * model_uncertain ]
ds . leftDriverData . facePositionStd = [ 1. * model_uncertain , 1. * model_uncertain ]
# TODO: test both separately when e2e is used
ds . leftDriverData . readyProb = [ 0. , 0. , 0. , 0. ]
ds . leftDriverData . notReadyProb = [ 0. , 0. ]
return ds
# driver state from neural net, 10Hz
msg_NO_FACE_DETECTED = make_msg ( False )
msg_ATTENTIVE = make_msg ( True )
msg_DISTRACTED = make_msg ( True , distracted = True )
msg_ATTENTIVE_UNCERTAIN = make_msg ( True , model_uncertain = True )
msg_DISTRACTED_UNCERTAIN = make_msg ( True , distracted = True , model_uncertain = True )
msg_DISTRACTED_BUT_SOMEHOW_UNCERTAIN = make_msg ( True , distracted = True , model_uncertain = dm_settings . _POSESTD_THRESHOLD * 1.5 )
# driver interaction with car
car_interaction_DETECTED = True
car_interaction_NOT_DETECTED = False
# some common state vectors
always_no_face = [ msg_NO_FACE_DETECTED ] * int ( TEST_TIMESPAN / DT_DMON )
always_attentive = [ msg_ATTENTIVE ] * int ( TEST_TIMESPAN / DT_DMON )
always_distracted = [ msg_DISTRACTED ] * int ( TEST_TIMESPAN / DT_DMON )
always_true = [ True ] * int ( TEST_TIMESPAN / DT_DMON )
always_false = [ False ] * int ( TEST_TIMESPAN / DT_DMON )
# TODO: this only tests DriverStatus
class TestMonitoring ( unittest . TestCase ) :
# pylint: disable=no-member
def _run_seq ( self , msgs , interaction , engaged , standstill ) :
DS = DriverStatus ( )
events = [ ]
for idx in range ( len ( msgs ) ) :
e = Events ( )
DS . update_states ( msgs [ idx ] , [ 0 , 0 , 0 ] , 0 , engaged [ idx ] )
# cal_rpy and car_speed don't matter here
# evaluate events at 10Hz for tests
DS . update_events ( e , interaction [ idx ] , engaged [ idx ] , standstill [ idx ] )
events . append ( e )
assert len ( events ) == len ( msgs ) , f " got { len ( events ) } for { len ( msgs ) } driverState input msgs "
return events , DS
def _assert_no_events ( self , events ) :
self . assertTrue ( all ( not len ( e ) for e in events ) )
# engaged, driver is attentive all the time
def test_fully_aware_driver ( self ) :
events , _ = self . _run_seq ( always_attentive , always_false , always_true , always_false )
self . _assert_no_events ( events )
# engaged, driver is distracted and does nothing
def test_fully_distracted_driver ( self ) :
events , d_status = self . _run_seq ( always_distracted , always_false , always_true , always_false )
self . assertEqual ( len ( events [ int ( ( d_status . settings . _DISTRACTED_TIME - d_status . settings . _DISTRACTED_PRE_TIME_TILL_TERMINAL ) / 2 / DT_DMON ) ] ) , 0 )
self . assertEqual ( events [ int ( ( d_status . settings . _DISTRACTED_TIME - d_status . settings . _DISTRACTED_PRE_TIME_TILL_TERMINAL +
( ( d_status . settings . _DISTRACTED_PRE_TIME_TILL_TERMINAL - d_status . settings . _DISTRACTED_PROMPT_TIME_TILL_TERMINAL ) / 2 ) ) / DT_DMON ) ] . names [ 0 ] ,
EventName . preDriverDistracted )
self . assertEqual ( events [ int ( ( d_status . settings . _DISTRACTED_TIME - d_status . settings . _DISTRACTED_PROMPT_TIME_TILL_TERMINAL +
( ( d_status . settings . _DISTRACTED_PROMPT_TIME_TILL_TERMINAL ) / 2 ) ) / DT_DMON ) ] . names [ 0 ] , EventName . promptDriverDistracted )
self . assertEqual ( events [ int ( ( d_status . settings . _DISTRACTED_TIME +
( ( TEST_TIMESPAN - 10 - d_status . settings . _DISTRACTED_TIME ) / 2 ) ) / DT_DMON ) ] . names [ 0 ] , EventName . driverDistracted )
self . assertIs ( type ( d_status . awareness ) , float )
# engaged, no face detected the whole time, no action
def test_fully_invisible_driver ( self ) :
events , d_status = self . _run_seq ( always_no_face , always_false , always_true , always_false )
self . assertTrue ( len ( events [ int ( ( d_status . settings . _AWARENESS_TIME - d_status . settings . _AWARENESS_PRE_TIME_TILL_TERMINAL ) / 2 / DT_DMON ) ] ) == 0 )
self . assertEqual ( events [ int ( ( d_status . settings . _AWARENESS_TIME - d_status . settings . _AWARENESS_PRE_TIME_TILL_TERMINAL +
( ( d_status . settings . _AWARENESS_PRE_TIME_TILL_TERMINAL - d_status . settings . _AWARENESS_PROMPT_TIME_TILL_TERMINAL ) / 2 ) ) / DT_DMON ) ] . names [ 0 ] ,
EventName . preDriverUnresponsive )
self . assertEqual ( events [ int ( ( d_status . settings . _AWARENESS_TIME - d_status . settings . _AWARENESS_PROMPT_TIME_TILL_TERMINAL +
( ( d_status . settings . _AWARENESS_PROMPT_TIME_TILL_TERMINAL ) / 2 ) ) / DT_DMON ) ] . names [ 0 ] , EventName . promptDriverUnresponsive )
self . assertEqual ( events [ int ( ( d_status . settings . _AWARENESS_TIME +
( ( TEST_TIMESPAN - 10 - d_status . settings . _AWARENESS_TIME ) / 2 ) ) / DT_DMON ) ] . names [ 0 ] , EventName . driverUnresponsive )
# engaged, down to orange, driver pays attention, back to normal; then down to orange, driver touches wheel
# - should have short orange recovery time and no green afterwards; wheel touch only recovers when paying attention
def test_normal_driver ( self ) :
ds_vector = [ msg_DISTRACTED ] * int ( DISTRACTED_SECONDS_TO_ORANGE / DT_DMON ) + \
[ msg_ATTENTIVE ] * int ( DISTRACTED_SECONDS_TO_ORANGE / DT_DMON ) + \
[ msg_DISTRACTED ] * int ( ( DISTRACTED_SECONDS_TO_ORANGE + 2 ) / DT_DMON ) + \
[ msg_ATTENTIVE ] * ( int ( TEST_TIMESPAN / DT_DMON ) - int ( ( DISTRACTED_SECONDS_TO_ORANGE * 3 + 2 ) / DT_DMON ) )
interaction_vector = [ car_interaction_NOT_DETECTED ] * int ( DISTRACTED_SECONDS_TO_ORANGE * 3 / DT_DMON ) + \
[ car_interaction_DETECTED ] * ( int ( TEST_TIMESPAN / DT_DMON ) - int ( DISTRACTED_SECONDS_TO_ORANGE * 3 / DT_DMON ) )
events , _ = self . _run_seq ( ds_vector , interaction_vector , always_true , always_false )
self . assertEqual ( len ( events [ int ( DISTRACTED_SECONDS_TO_ORANGE * 0.5 / DT_DMON ) ] ) , 0 )
self . assertEqual ( events [ int ( ( DISTRACTED_SECONDS_TO_ORANGE - 0.1 ) / DT_DMON ) ] . names [ 0 ] , EventName . promptDriverDistracted )
self . assertEqual ( len ( events [ int ( DISTRACTED_SECONDS_TO_ORANGE * 1.5 / DT_DMON ) ] ) , 0 )
self . assertEqual ( events [ int ( ( DISTRACTED_SECONDS_TO_ORANGE * 3 - 0.1 ) / DT_DMON ) ] . names [ 0 ] , EventName . promptDriverDistracted )
self . assertEqual ( events [ int ( ( DISTRACTED_SECONDS_TO_ORANGE * 3 + 0.1 ) / DT_DMON ) ] . names [ 0 ] , EventName . promptDriverDistracted )
self . assertEqual ( len ( events [ int ( ( DISTRACTED_SECONDS_TO_ORANGE * 3 + 2.5 ) / DT_DMON ) ] ) , 0 )
# engaged, down to orange, driver dodges camera, then comes back still distracted, down to red, \
# driver dodges, and then touches wheel to no avail, disengages and reengages
# - orange/red alert should remain after disappearance, and only disengaging clears red
def test_biggest_comma_fan ( self ) :
_invisible_time = 2 # seconds
ds_vector = always_distracted [ : ]
interaction_vector = always_false [ : ]
op_vector = always_true [ : ]
ds_vector [ int ( DISTRACTED_SECONDS_TO_ORANGE / DT_DMON ) : int ( ( DISTRACTED_SECONDS_TO_ORANGE + _invisible_time ) / DT_DMON ) ] \
= [ msg_NO_FACE_DETECTED ] * int ( _invisible_time / DT_DMON )
ds_vector [ int ( ( DISTRACTED_SECONDS_TO_RED + _invisible_time ) / DT_DMON ) : int ( ( DISTRACTED_SECONDS_TO_RED + 2 * _invisible_time ) / DT_DMON ) ] \
= [ msg_NO_FACE_DETECTED ] * int ( _invisible_time / DT_DMON )
interaction_vector [ int ( ( DISTRACTED_SECONDS_TO_RED + 2 * _invisible_time + 0.5 ) / DT_DMON ) : int ( ( DISTRACTED_SECONDS_TO_RED + 2 * _invisible_time + 1.5 ) / DT_DMON ) ] \
= [ True ] * int ( 1 / DT_DMON )
op_vector [ int ( ( DISTRACTED_SECONDS_TO_RED + 2 * _invisible_time + 2.5 ) / DT_DMON ) : int ( ( DISTRACTED_SECONDS_TO_RED + 2 * _invisible_time + 3 ) / DT_DMON ) ] \
= [ False ] * int ( 0.5 / DT_DMON )
events , _ = self . _run_seq ( ds_vector , interaction_vector , op_vector , always_false )
self . assertEqual ( events [ int ( ( DISTRACTED_SECONDS_TO_ORANGE + 0.5 * _invisible_time ) / DT_DMON ) ] . names [ 0 ] , EventName . promptDriverDistracted )
self . assertEqual ( events [ int ( ( DISTRACTED_SECONDS_TO_RED + 1.5 * _invisible_time ) / DT_DMON ) ] . names [ 0 ] , EventName . driverDistracted )
self . assertEqual ( events [ int ( ( DISTRACTED_SECONDS_TO_RED + 2 * _invisible_time + 1.5 ) / DT_DMON ) ] . names [ 0 ] , EventName . driverDistracted )
self . assertTrue ( len ( events [ int ( ( DISTRACTED_SECONDS_TO_RED + 2 * _invisible_time + 3.5 ) / DT_DMON ) ] ) == 0 )
# engaged, invisible driver, down to orange, driver touches wheel; then down to orange again, driver appears
# - both actions should clear the alert, but momentary appearance should not
def test_sometimes_transparent_commuter ( self ) :
_visible_time = np . random . choice ( [ 0.5 , 10 ] )
ds_vector = always_no_face [ : ] * 2
interaction_vector = always_false [ : ] * 2
ds_vector [ int ( ( 2 * INVISIBLE_SECONDS_TO_ORANGE + 1 ) / DT_DMON ) : int ( ( 2 * INVISIBLE_SECONDS_TO_ORANGE + 1 + _visible_time ) / DT_DMON ) ] = \
[ msg_ATTENTIVE ] * int ( _visible_time / DT_DMON )
interaction_vector [ int ( ( INVISIBLE_SECONDS_TO_ORANGE ) / DT_DMON ) : int ( ( INVISIBLE_SECONDS_TO_ORANGE + 1 ) / DT_DMON ) ] = [ True ] * int ( 1 / DT_DMON )
events , _ = self . _run_seq ( ds_vector , interaction_vector , 2 * always_true , 2 * always_false )
self . assertTrue ( len ( events [ int ( INVISIBLE_SECONDS_TO_ORANGE * 0.5 / DT_DMON ) ] ) == 0 )
self . assertEqual ( events [ int ( ( INVISIBLE_SECONDS_TO_ORANGE - 0.1 ) / DT_DMON ) ] . names [ 0 ] , EventName . promptDriverUnresponsive )
self . assertTrue ( len ( events [ int ( ( INVISIBLE_SECONDS_TO_ORANGE + 0.1 ) / DT_DMON ) ] ) == 0 )
if _visible_time == 0.5 :
self . assertEqual ( events [ int ( ( INVISIBLE_SECONDS_TO_ORANGE * 2 + 1 - 0.1 ) / DT_DMON ) ] . names [ 0 ] , EventName . promptDriverUnresponsive )
self . assertEqual ( events [ int ( ( INVISIBLE_SECONDS_TO_ORANGE * 2 + 1 + 0.1 + _visible_time ) / DT_DMON ) ] . names [ 0 ] , EventName . preDriverUnresponsive )
elif _visible_time == 10 :
self . assertEqual ( events [ int ( ( INVISIBLE_SECONDS_TO_ORANGE * 2 + 1 - 0.1 ) / DT_DMON ) ] . names [ 0 ] , EventName . promptDriverUnresponsive )
self . assertTrue ( len ( events [ int ( ( INVISIBLE_SECONDS_TO_ORANGE * 2 + 1 + 0.1 + _visible_time ) / DT_DMON ) ] ) == 0 )
# engaged, invisible driver, down to red, driver appears and then touches wheel, then disengages/reengages
# - only disengage will clear the alert
def test_last_second_responder ( self ) :
_visible_time = 2 # seconds
ds_vector = always_no_face [ : ]
interaction_vector = always_false [ : ]
op_vector = always_true [ : ]
ds_vector [ int ( INVISIBLE_SECONDS_TO_RED / DT_DMON ) : int ( ( INVISIBLE_SECONDS_TO_RED + _visible_time ) / DT_DMON ) ] = [ msg_ATTENTIVE ] * int ( _visible_time / DT_DMON )
interaction_vector [ int ( ( INVISIBLE_SECONDS_TO_RED + _visible_time ) / DT_DMON ) : int ( ( INVISIBLE_SECONDS_TO_RED + _visible_time + 1 ) / DT_DMON ) ] = [ True ] * int ( 1 / DT_DMON )
op_vector [ int ( ( INVISIBLE_SECONDS_TO_RED + _visible_time + 1 ) / DT_DMON ) : int ( ( INVISIBLE_SECONDS_TO_RED + _visible_time + 0.5 ) / DT_DMON ) ] = [ False ] * int ( 0.5 / DT_DMON )
events , _ = self . _run_seq ( ds_vector , interaction_vector , op_vector , always_false )
self . assertTrue ( len ( events [ int ( INVISIBLE_SECONDS_TO_ORANGE * 0.5 / DT_DMON ) ] ) == 0 )
self . assertEqual ( events [ int ( ( INVISIBLE_SECONDS_TO_ORANGE - 0.1 ) / DT_DMON ) ] . names [ 0 ] , EventName . promptDriverUnresponsive )
self . assertEqual ( events [ int ( ( INVISIBLE_SECONDS_TO_RED - 0.1 ) / DT_DMON ) ] . names [ 0 ] , EventName . driverUnresponsive )
self . assertEqual ( events [ int ( ( INVISIBLE_SECONDS_TO_RED + 0.5 * _visible_time ) / DT_DMON ) ] . names [ 0 ] , EventName . driverUnresponsive )
self . assertEqual ( events [ int ( ( INVISIBLE_SECONDS_TO_RED + _visible_time + 0.5 ) / DT_DMON ) ] . names [ 0 ] , EventName . driverUnresponsive )
self . assertTrue ( len ( events [ int ( ( INVISIBLE_SECONDS_TO_RED + _visible_time + 1 + 0.1 ) / DT_DMON ) ] ) == 0 )
# disengaged, always distracted driver
# - dm should stay quiet when not engaged
def test_pure_dashcam_user ( self ) :
events , _ = self . _run_seq ( always_distracted , always_false , always_false , always_false )
self . assertTrue ( sum ( len ( event ) for event in events ) == 0 )
# engaged, car stops at traffic light, down to orange, no action, then car starts moving
# - should only reach green when stopped, but continues counting down on launch
def test_long_traffic_light_victim ( self ) :
_redlight_time = 60 # seconds
standstill_vector = always_true [ : ]
standstill_vector [ int ( _redlight_time / DT_DMON ) : ] = [ False ] * int ( ( TEST_TIMESPAN - _redlight_time ) / DT_DMON )
events , d_status = self . _run_seq ( always_distracted , always_false , always_true , standstill_vector )
self . assertEqual ( events [ int ( ( d_status . settings . _DISTRACTED_TIME - d_status . settings . _DISTRACTED_PRE_TIME_TILL_TERMINAL + 1 ) / DT_DMON ) ] . names [ 0 ] ,
EventName . preDriverDistracted )
self . assertEqual ( events [ int ( ( _redlight_time - 0.1 ) / DT_DMON ) ] . names [ 0 ] , EventName . preDriverDistracted )
self . assertEqual ( events [ int ( ( _redlight_time + 0.5 ) / DT_DMON ) ] . names [ 0 ] , EventName . promptDriverDistracted )
# engaged, model is somehow uncertain and driver is distracted
# - should fall back to wheel touch after uncertain alert
def test_somehow_indecisive_model ( self ) :
ds_vector = [ msg_DISTRACTED_BUT_SOMEHOW_UNCERTAIN ] * int ( TEST_TIMESPAN / DT_DMON )
interaction_vector = always_false [ : ]
events , d_status = self . _run_seq ( ds_vector , interaction_vector , always_true , always_false )
self . assertTrue ( EventName . preDriverUnresponsive in
events [ int ( ( INVISIBLE_SECONDS_TO_ORANGE - 1 + DT_DMON * d_status . settings . _HI_STD_FALLBACK_TIME - 0.1 ) / DT_DMON ) ] . names )
self . assertTrue ( EventName . promptDriverUnresponsive in
events [ int ( ( INVISIBLE_SECONDS_TO_ORANGE - 1 + DT_DMON * d_status . settings . _HI_STD_FALLBACK_TIME + 0.1 ) / DT_DMON ) ] . names )
self . assertTrue ( EventName . driverUnresponsive in
events [ int ( ( INVISIBLE_SECONDS_TO_RED - 1 + DT_DMON * d_status . settings . _HI_STD_FALLBACK_TIME + 0.1 ) / DT_DMON ) ] . names )
if __name__ == " __main__ " :
unittest . main ( )