Merge PR #90: Rivian Angle Support

Original PR by TonyJOM (Anthony Orta).

Co-authored-by: TonyJOM <anthonyorta20@icloud.com>
This commit is contained in:
firestar5683
2026-08-16 11:17:21 -05:00
48 changed files with 3676 additions and 346 deletions
+7
View File
@@ -189,6 +189,8 @@ def get_selected_panda_firmware_name(app_fn, remote_start, hkg_remote_start, ign
def flash_panda(params_memory):
from openpilot.selfdrive.pandad.rivian_long_flasher import is_rivian_bridge_panda, is_rivian_vehicle
params = Params()
try:
remote_start = params.get_bool("RemoteStartBootsComma")
@@ -203,9 +205,14 @@ def flash_panda(params_memory):
except Exception:
ignore_ignition_line = False
rivian = is_rivian_vehicle()
usb_serials = set(Panda.usb_list())
for serial in Panda.list():
try:
with Panda(serial=serial) as panda:
if serial in usb_serials and is_rivian_bridge_panda(panda, rivian):
print(f"Skipping Rivian harness bridge {serial}")
continue
print(f"Flashing Panda {serial}")
flash_fn = None
app_fn = panda.get_mcu_type().config.app_fn
+5
View File
@@ -330,6 +330,9 @@ def get_starpilot_toggles(sm=messaging.SubMaster(["starpilotPlan"]), *, read_per
# Controller selection happens before the first live StarPilot broadcast. Do
# not let a cached CarParams/controller type hide the persisted user request.
toggles.force_torque_controller = get_starpilot_toggles._params.get_bool("ForceTorqueController")
# Controller selection happens before the first live StarPilot broadcast.
# Realtime callers use the serialized value to avoid blocking reads.
toggles.rivian_angle_control = get_starpilot_toggles._params.get_bool("RivianAngleControl")
return toggles
@cache
@@ -563,6 +566,7 @@ class StarPilotVariables:
toggle = self.starpilot_toggles
# CarParams uses this value to select the matching Panda safety configuration.
toggle.tesla_cooperative_steering = self.params.get_bool("TeslaCoopSteering")
toggle.rivian_angle_control = self.params.get_bool("RivianAngleControl")
fallback_platform = GM_CAR.CHEVROLET_BOLT_ACC_2022_2023 if HARDWARE.get_device_type() == "pc" else MOCK.MOCK
@@ -1422,6 +1426,7 @@ class StarPilotVariables:
"TeslaCoopSteering",
condition=toggle.car_make == "tesla" and toggle.car_model == TESLA_CAR.TESLA_MODEL_3,
)
toggle.rivian_angle_control = self.get_value("RivianAngleControl", condition=toggle.car_make == "rivian")
toggle.tethering_config = self.get_value("TetheringEnabled", cast=float)
@@ -64,6 +64,19 @@ def test_get_starpilot_toggles_realtime_path_does_not_read_persisted_force_param
assert toggles.force_torque_controller is False
def test_get_starpilot_toggles_uses_live_rivian_angle_request(monkeypatch):
params = SimpleNamespace(get_bool=lambda key: key == "RivianAngleControl")
monkeypatch.setattr(spv.get_starpilot_toggles, "_params", params, raising=False)
payload = '{"rivian_angle_control": false}'
toggles = spv.get_starpilot_toggles(
{"starpilotPlan": SimpleNamespace(starpilotToggles=payload)},
read_persisted_force_params=True,
)
assert toggles.rivian_angle_control is True
class _FakeParams:
def __init__(self, floats=None, ints=None, bools=None):
self.floats = dict(floats or {})
@@ -15,6 +15,7 @@ const HIDDEN_SETTING_KEYS = new Set(["HumanAcceleration", "ReverseCruise"])
const GM_MAKES = ["Buick", "Cadillac", "Chevrolet", "GMC", "Holden"]
const HKG_MAKES = ["Genesis", "Hyundai", "Kia"]
const VEHICLE_SETTING_MAKES = {
RivianAngleControl: ["Rivian"],
TeslaCoopSteering: ["Tesla"],
NAPRadarEnabled: ["Tesla"],
NAPRadarBehindNosecone: ["Tesla"],
@@ -105,6 +106,7 @@ function isVehicleSettingVisible(section, param) {
function isSettingVisible(section, param) {
// This policy controls Galaxy rendering only; hidden params retain their stored values.
if (HIDDEN_SETTING_KEYS.has(param.key) || !isVehicleSettingVisible(section, param)) return false
if (param.requires_capability && !state.values[param.requires_capability]) return false
if (RADAR_REQUIRED_KEYS.has(param.key) && !state.values.HasRadar) return false
if (param.key === "AlphaLongitudinalEnabled" && !state.values.AlphaLongitudinalAvailable) return false
if (state.values[GALAXY_DEVELOPER_MODE_KEY]) return true
@@ -2703,6 +2703,16 @@
"options_endpoint": "/api/fingerprints/models?make={CarMake}",
"settings_tier": "simple"
},
{
"key": "RivianAngleControl",
"label": "Rivian Angle Steering",
"description": "Use the Extreme harness angle channel with cooperative torque fallback.",
"data_type": "bool",
"ui_type": "toggle",
"favorite_eligible": true,
"requires_capability": "HasRivianAngleHarness",
"settings_tier": "simple"
},
{
"key": "ForceFingerprint",
"label": "Disable Automatic Fingerprint Detection",
@@ -172,6 +172,17 @@ def test_human_acceleration_param_is_removed():
assert '{"HumanAcceleration",' not in params_source
def test_rivian_angle_control_is_live_favorite_and_harness_gated():
sections = _params_by_section(_layout())
setting = sections["Vehicle"]["RivianAngleControl"]
assert setting["ui_type"] == "toggle"
assert setting["favorite_eligible"] is True
assert setting["requires_capability"] == "HasRivianAngleHarness"
assert "reboot" not in setting["description"].lower()
assert _declared_default("RivianAngleControl") == "0"
def test_vasm_is_default_off_and_configured_only_in_galaxy():
sections = _params_by_section(_layout())
lateral = sections["Lateral (Steering)"]
@@ -249,6 +249,23 @@ def test_favorite_slot_options_include_virtual_cruise_actions(monkeypatch):
assert "__starpilot_favorite_action__:distance_increase" in option_keys
def test_rivian_angle_favorite_requires_detected_extreme_harness(monkeypatch):
options = [
{"key": "RivianAngleControl", "requiresCapability": "HasRivianAngleHarness"},
{"key": "NonGatedFavorite", "requiresCapability": ""},
]
monkeypatch.setattr(the_galaxy, "_get_favorite_slot_options", lambda: options)
monkeypatch.setattr(the_galaxy, "_get_has_rivian_angle_harness", lambda: False)
assert [option["key"] for option in the_galaxy._get_available_favorite_slot_options()] == ["NonGatedFavorite"]
monkeypatch.setattr(the_galaxy, "_get_has_rivian_angle_harness", lambda: True)
assert [option["key"] for option in the_galaxy._get_available_favorite_slot_options()] == [
"RivianAngleControl",
"NonGatedFavorite",
]
def test_favorite_action_endpoint_increments_virtual_button_counter(monkeypatch):
client, _ = _params_client(monkeypatch, {}, "tici")
fake_memory = WritableFakeParams()
+29 -3
View File
@@ -99,6 +99,8 @@ from openpilot.starpilot.system.the_galaxy import flm_workspace, utilities
from openpilot.starpilot.system.the_galaxy.update_recovery import inspect_interrupted_update, public_recovery_status, recover_interrupted_update
DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
# Keep Galaxy independent of opendbc's generated car bindings while matching RivianFlags.ANGLE_HARNESS.
RIVIAN_ANGLE_HARNESS_FLAG = 1 << 0
GITLAB_API = "https://gitlab.com/api/v4"
GITLAB_SUBMISSIONS_PROJECT_ID = "71992109"
@@ -2721,6 +2723,7 @@ def _get_favorite_slot_options():
"label": str(param_data.get("label") or key),
"description": str(param_data.get("description") or ""),
"section": section_name,
"requiresCapability": str(param_data.get("requires_capability") or ""),
})
except Exception:
options = []
@@ -2729,6 +2732,15 @@ def _get_favorite_slot_options():
_favorite_slot_options = options
return _favorite_slot_options
def _get_available_favorite_slot_options():
capabilities = {
"HasRivianAngleHarness": _get_has_rivian_angle_harness(),
}
return [
option for option in _get_favorite_slot_options()
if not option.get("requiresCapability") or capabilities.get(option["requiresCapability"], False)
]
def _favorite_slot_values(options):
return {
option["key"]: _safe_params_get_bool(option["key"])
@@ -3517,6 +3529,17 @@ def _get_alpha_longitudinal_available():
except Exception:
return False
def _get_has_rivian_angle_harness():
cp_bytes = _safe_params_get_live_raw("CarParamsPersistent")
if not cp_bytes:
return False
try:
with car.CarParams.from_bytes(cp_bytes) as cp:
return cp.brand == "rivian" and bool(int(getattr(cp, "flags", 0)) & RIVIAN_ANGLE_HARNESS_FLAG)
except Exception:
return False
def _get_hardware_snapshot_items():
starpilot_toggles = _get_starpilot_toggles_snapshot()
@@ -4655,7 +4678,7 @@ def setup(app):
@app.route("/api/favorites/slots", methods=["GET", "PUT"])
def favorite_slots():
options = _get_favorite_slot_options()
options = _get_available_favorite_slot_options()
option_by_key = {option["key"]: option for option in options}
eligible_keys = set(option_by_key)
@@ -4705,7 +4728,7 @@ def setup(app):
@app.route("/api/favorites/values", methods=["GET"])
def favorite_values():
options = _get_favorite_slot_options()
options = _get_available_favorite_slot_options()
eligible_keys = {option["key"] for option in options}
slots = normalize_favorite_slots(params.get(FAVORITE_SLOTS_PARAM), params=params, eligible_keys=eligible_keys)
return jsonify({"values": _configured_favorite_slot_values(slots)}), 200
@@ -4736,7 +4759,7 @@ def setup(app):
if not isinstance(raw_slots, list):
return jsonify({"error": "Favorite slots must be configured with the Favorites editor."}), 400
options = _get_favorite_slot_options()
options = _get_available_favorite_slot_options()
option_by_key = {option["key"]: option for option in options}
eligible_keys = set(option_by_key)
slots = normalize_favorite_slots(raw_slots, params=params, eligible_keys=eligible_keys)
@@ -5099,6 +5122,8 @@ def setup(app):
update_starpilot_toggles()
response = {"message": f"Parameter '{key}' updated successfully."}
if key == "RivianAngleControl":
response["message"] = "Rivian steering mode updated. The safe channel handoff is in progress."
updated = {}
if key in PANDA_FIRMWARE_TOGGLE_KEYS:
threading.Thread(target=_flash_panda_then_reboot, daemon=True).start()
@@ -5178,6 +5203,7 @@ def setup(app):
result["HasRadar"] = _get_has_radar()
result["VehicleParked"] = _get_vehicle_parked()
result["AlphaLongitudinalAvailable"] = _get_alpha_longitudinal_available()
result["HasRivianAngleHarness"] = _get_has_rivian_angle_harness()
return jsonify(_sanitize_json_value(result)), 200