Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions opendbc/car/mazda/carstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ def get_can_parsers(CP, CP_SP):
("CAM_LANEINFO", float("nan")),
("CAM_TRAFFIC_SIGNS", float("nan")),
("CAM_EMPTY", float("nan")),
("CAM_PEDESTRIAN", float("nan")),
]
return {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, 0),
Expand Down
183 changes: 97 additions & 86 deletions opendbc/car/mazda/tests/test_mazda_carstate.py

Large diffs are not rendered by default.

1,082 changes: 589 additions & 493 deletions opendbc/car/mazda/tests/test_mazda_controller.py

Large diffs are not rendered by default.

47 changes: 24 additions & 23 deletions opendbc/car/mazda/tests/test_mazda_interface.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import pytest
import unittest

from opendbc.car import structs
from opendbc.car.common.conversions import Conversions as CV
Expand Down Expand Up @@ -26,7 +26,7 @@ def _params(candidate, car_fw=None, alpha_long=False):
alpha_long, is_release=False, docs=False)


class TestMazdaEpsSwap:
class TestMazdaEpsSwap(unittest.TestCase):
"""A 2022+ CX-5 EPS swapped into an older Mazda brings the EPS-derived behavior with it.

Pre-2022 Mazdas are dashcam only because their EPS locks steering out after ~5 s hands-off
Expand All @@ -36,47 +36,48 @@ class TestMazdaEpsSwap:

def test_stock_older_mazda_is_dashcam_only(self):
CP = _params(CAR.MAZDA_CX5, _eps_fw(STOCK_CX5_EPS_FW))
assert CP.dashcamOnly
assert CP.minSteerSpeed == pytest.approx(LKAS_LIMITS.DISABLE_SPEED * CV.KPH_TO_MS)
assert CP.steerActuatorDelay == pytest.approx(0.1)
self.assertTrue(CP.dashcamOnly)
self.assertAlmostEqual(CP.minSteerSpeed, LKAS_LIMITS.DISABLE_SPEED * CV.KPH_TO_MS)
self.assertAlmostEqual(CP.steerActuatorDelay, 0.1)

def test_swapped_eps_lifts_dashcam_and_the_speed_floor(self):
CP = _params(CAR.MAZDA_CX5, _eps_fw(SWAPPED_EPS_FW))
assert not CP.dashcamOnly
assert CP.minSteerSpeed == 0
assert CP.steerActuatorDelay == pytest.approx(0.14)
self.assertFalse(CP.dashcamOnly)
self.assertEqual(CP.minSteerSpeed, 0)
self.assertAlmostEqual(CP.steerActuatorDelay, 0.14)

def test_swapped_eps_does_not_unlock_longitudinal(self):
# the radar and camera are not part of an EPS swap, and this car keeps its own pre-2022 pair
CP = _params(CAR.MAZDA_CX5, _eps_fw(SWAPPED_EPS_FW), alpha_long=True)
assert not CP.alphaLongitudinalAvailable
assert not CP.openpilotLongitudinalControl
self.assertFalse(CP.alphaLongitudinalAvailable)
self.assertFalse(CP.openpilotLongitudinalControl)

def test_swapped_eps_keeps_the_real_vehicle_specs(self):
# the whole point of fixing detection is that the user no longer forces MAZDA_CX5_2022 and
# inherits its mass, steer ratio and tire stiffness
swapped = _params(CAR.MAZDA_CX5, _eps_fw(SWAPPED_EPS_FW))
cx5_2022 = _params(CAR.MAZDA_CX5_2022)
assert swapped.mass != cx5_2022.mass
assert swapped.steerRatio != cx5_2022.steerRatio
assert swapped.tireStiffnessFactor != cx5_2022.tireStiffnessFactor
self.assertNotEqual(swapped.mass, cx5_2022.mass)
self.assertNotEqual(swapped.steerRatio, cx5_2022.steerRatio)
self.assertNotEqual(swapped.tireStiffnessFactor, cx5_2022.tireStiffnessFactor)

def test_supported_platforms_are_unchanged(self):
cx5_2022 = _params(CAR.MAZDA_CX5_2022)
assert not cx5_2022.dashcamOnly
assert cx5_2022.minSteerSpeed == 0
assert cx5_2022.steerActuatorDelay == pytest.approx(0.14)
assert cx5_2022.alphaLongitudinalAvailable
self.assertFalse(cx5_2022.dashcamOnly)
self.assertEqual(cx5_2022.minSteerSpeed, 0)
self.assertAlmostEqual(cx5_2022.steerActuatorDelay, 0.14)
self.assertTrue(cx5_2022.alphaLongitudinalAvailable)

# the CX-9 2021 is supported without the CX-5 EPS, so it keeps the 45 kph floor
cx9_2021 = _params(CAR.MAZDA_CX9_2021)
assert not cx9_2021.dashcamOnly
assert cx9_2021.minSteerSpeed == pytest.approx(LKAS_LIMITS.DISABLE_SPEED * CV.KPH_TO_MS)
assert cx9_2021.steerActuatorDelay == pytest.approx(0.1)
self.assertFalse(cx9_2021.dashcamOnly)
self.assertAlmostEqual(cx9_2021.minSteerSpeed, LKAS_LIMITS.DISABLE_SPEED * CV.KPH_TO_MS)
self.assertAlmostEqual(cx9_2021.steerActuatorDelay, 0.1)

def test_docs_are_generated_without_firmware(self):
# car_fw is empty when building CARS.md, so the docs must keep advertising dashcam mode
for candidate in (CAR.MAZDA_CX5, CAR.MAZDA_CX9, CAR.MAZDA_3, CAR.MAZDA_6):
CP = CarInterface.get_params(candidate, {0: {}, 1: {}, 2: {}}, [], False,
is_release=False, docs=True)
assert CP.dashcamOnly, candidate
with self.subTest(candidate=candidate):
CP = CarInterface.get_params(candidate, {0: {}, 1: {}, 2: {}}, [], False,
is_release=False, docs=True)
self.assertTrue(CP.dashcamOnly)
35 changes: 18 additions & 17 deletions opendbc/car/mazda/tests/test_mazda_radar.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
or not at all, except real tracks at exactly -1.0 m/s.
"""
import math

import pytest
import unittest

from opendbc.can import CANPacker
from opendbc.car import gen_empty_fingerprint
Expand Down Expand Up @@ -49,19 +48,20 @@ def burst(ri, t_ns, overrides=None):
return rr


class TestRadarSentinels:
class TestRadarSentinels(unittest.TestCase):
def test_empty_slots_produce_no_points(self):
ri = _radar_interface()
rr = burst(ri, 0)
assert len(rr.points) == 0
self.assertEqual(len(rr.points), 0)

def test_real_track_parses(self):
ri = _radar_interface()
rr = burst(ri, 0, {0x361: track_msg(0x361, dist=40.0, ang=2.0, relv=-5.0)})
assert len(rr.points) == 1
self.assertEqual(len(rr.points), 1)
pt = rr.points[0]
assert pt.dRel == pytest.approx(math.cos(math.radians(2.0)) * 40.0, rel=1e-6)
assert pt.vRel == -5.0
expected_drel = math.cos(math.radians(2.0)) * 40.0
self.assertAlmostEqual(pt.dRel, expected_drel, delta=abs(expected_drel) * 1e-6)
self.assertEqual(pt.vRel, -5.0)

def test_track_at_exactly_minus_one_mps_is_kept(self):
"""-1.0 m/s with a real distance is a live lead, not an empty slot. Dropping it
Expand All @@ -73,34 +73,35 @@ def test_track_at_exactly_minus_one_mps_is_kept(self):

# lead decelerates through exactly -1.0 m/s: the track must survive with its identity
rr = burst(ri, int(0.1e9), {0x361: track_msg(0x361, dist=39.875, ang=0.0, relv=SENTINEL_RELV)})
assert len(rr.points) == 1
assert rr.points[0].vRel == SENTINEL_RELV
assert rr.points[0].trackId == track_id
self.assertEqual(len(rr.points), 1)
self.assertEqual(rr.points[0].vRel, SENTINEL_RELV)
self.assertEqual(rr.points[0].trackId, track_id)

rr = burst(ri, int(0.2e9), {0x361: track_msg(0x361, dist=39.75, ang=0.0, relv=-0.9375)})
assert rr.points[0].trackId == track_id
self.assertEqual(rr.points[0].trackId, track_id)

def test_track_at_max_range_is_kept(self):
ri = _radar_interface()
rr = burst(ri, 0, {0x362: track_msg(0x362, dist=SENTINEL_DIST, ang=0.0, relv=-10.0)})
assert len(rr.points) == 1
assert rr.points[0].dRel == SENTINEL_DIST
self.assertEqual(len(rr.points), 1)
self.assertEqual(rr.points[0].dRel, SENTINEL_DIST)

def test_track_at_sentinel_angle_is_kept(self):
ri = _radar_interface()
rr = burst(ri, 0, {0x363: track_msg(0x363, dist=40.0, ang=SENTINEL_ANG, relv=-5.0)})
assert len(rr.points) == 1
assert rr.points[0].yRel == pytest.approx(-math.sin(math.radians(SENTINEL_ANG)) * 40.0, rel=1e-6)
self.assertEqual(len(rr.points), 1)
expected_yrel = -math.sin(math.radians(SENTINEL_ANG)) * 40.0
self.assertAlmostEqual(rr.points[0].yRel, expected_yrel, delta=abs(expected_yrel) * 1e-6)

def test_all_sentinel_slot_deletes_a_prior_track(self):
ri = _radar_interface()
burst(ri, 0, {0x361: track_msg(0x361, dist=40.0, ang=0.0, relv=-5.0)})
rr = burst(ri, int(0.1e9)) # slot empties: all three sentinels
assert len(rr.points) == 0
self.assertEqual(len(rr.points), 0)

def test_undecoded_relv_addrs_never_produce_points(self):
ri = _radar_interface()
overrides = {addr: track_msg(addr, dist=40.0, ang=0.0, relv=-5.0)
for addr in RADAR_TRACK_ADDRS if addr not in RADAR_USABLE_ADDRS}
rr = burst(ri, 0, overrides)
assert len(rr.points) == 0
self.assertEqual(len(rr.points), 0)
3 changes: 3 additions & 0 deletions opendbc/car/mazda/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ class CAR(Platforms):
MAZDA_CX9 = MazdaPlatformConfig(
[MazdaCarDocs("Mazda CX-9 2016-20")],
MazdaCarSpecs(mass=4217 * CV.LB_TO_KG, wheelbase=2.93, steerRatio=17.6),
# no radar bus: this is the one Mazda whose radar does not put the 0x361-0x366 tracks on
# bus 0, so claiming one would leave radard waiting on a parser that never goes valid
dbc_dict={Bus.pt: 'mazda_2017'},
wmis={WMI.JAPAN_CROSSOVER}, chassis_codes={'TC'}, years={'G', 'H', 'J', 'K', 'L'}, # 2016-20
)
MAZDA_3 = MazdaPlatformConfig(
Expand Down
37 changes: 18 additions & 19 deletions opendbc/sunnypilot/car/tests/test_speed_dep_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
This file is part of zoompilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pytest
import unittest

from opendbc.car.structs import CarParams
from opendbc.sunnypilot.car.interfaces import get_speed_dep_config_for_car, get_steer_max_schedule
from opendbc.sunnypilot.car.interfaces import get_speed_dep_config_for_car, get_steer_max_schedule, get_steer_rail_schedule


def _cx5_cp():
Expand All @@ -18,57 +18,56 @@ def _cx5_cp():
return cp


class TestSteerMaxSchedule:
class TestSteerMaxSchedule(unittest.TestCase):
def test_mazda_steer_to_zero_returns_lookup(self):
schedule = get_steer_max_schedule(_cx5_cp())
assert schedule == ([0.0, 14.2, 14.5], [1200.0, 1200.0, 800.0])
self.assertEqual(schedule, ([0.0, 14.2, 14.5], [1200.0, 1200.0, 800.0]))

def test_flat_steer_max_brand_returns_none(self):
cp = CarParams()
cp.carFingerprint = 'TOYOTA_RAV4_TSS2'
cp.brand = 'toyota'
assert get_steer_max_schedule(cp) is None
self.assertIsNone(get_steer_max_schedule(cp))

def test_unknown_brand_returns_none(self):
cp = CarParams()
cp.brand = 'notabrand'
assert get_steer_max_schedule(cp) is None
self.assertIsNone(get_steer_max_schedule(cp))

def test_schedule_attached_to_active_entry(self):
cfg = get_speed_dep_config_for_car(_cx5_cp())
assert cfg['steer_max_schedule'] == ([0.0, 14.2, 14.5], [1200.0, 1200.0, 800.0])
self.assertEqual(cfg['steer_max_schedule'], ([0.0, 14.2, 14.5], [1200.0, 1200.0, 800.0]))
# the schedule's step must sit inside a bin span, or per-count interp cannot place it
assert cfg['speed_bp'][2] < 14.2 < 14.5 < cfg['speed_bp'][3]
self.assertLess(cfg['speed_bp'][2], 14.2)
self.assertLess(14.5, cfg['speed_bp'][3])

def test_inactive_entry_stays_empty(self):
cp = CarParams()
cp.carFingerprint = 'MAZDA_CX9_2021'
cp.brand = 'mazda'
cp.minSteerSpeed = 20.0 # stock EPS, requires_steer_to_zero suppresses the entry
assert get_speed_dep_config_for_car(cp) == {}
self.assertEqual(get_speed_dep_config_for_car(cp), {})

def test_config_copy_not_cached_dict(self):
a = get_speed_dep_config_for_car(_cx5_cp())
a['steer_max_schedule'] = 'mutated'
assert get_speed_dep_config_for_car(_cx5_cp())['steer_max_schedule'] != 'mutated'
self.assertNotEqual(get_speed_dep_config_for_car(_cx5_cp())['steer_max_schedule'], 'mutated')


class TestSteerRailSchedule:
class TestSteerRailSchedule(unittest.TestCase):
def test_mazda_steer_to_zero_rail(self):
from opendbc.sunnypilot.car.interfaces import get_steer_rail_schedule
bp, rail = get_steer_rail_schedule(_cx5_cp())
assert all(0.0 < r <= 1.0 for r in rail)
self.assertTrue(all(0.0 < r <= 1.0 for r in rail))
# the rail bottoms out just below the cliff (648/1200) and recovers above it (620/800)
assert rail[bp.index(14.2)] == min(rail)
assert rail[bp.index(14.2)] == pytest.approx(0.54, abs=0.01)
assert rail[bp.index(14.5)] == pytest.approx(620.0 / 800.0, abs=1e-6)
self.assertEqual(rail[bp.index(14.2)], min(rail))
self.assertAlmostEqual(rail[bp.index(14.2)], 0.54, delta=0.01)
self.assertAlmostEqual(rail[bp.index(14.5)], 620.0 / 800.0, delta=1e-6)

def test_no_ceiling_returns_none(self):
from opendbc.sunnypilot.car.interfaces import get_steer_rail_schedule
cp = CarParams()
cp.brand = 'mazda'
cp.minSteerSpeed = 20.0 # stock EPS params have no ceiling lookup
assert get_steer_rail_schedule(cp) is None
self.assertIsNone(get_steer_rail_schedule(cp))
cp2 = CarParams()
cp2.brand = 'toyota'
assert get_steer_rail_schedule(cp2) is None
self.assertIsNone(get_steer_rail_schedule(cp2))
Loading